From 0a6ccbcd1ef8d210304a16ea0acc3486be9f1f4f Mon Sep 17 00:00:00 2001 From: Eric Deandrea Date: Fri, 11 May 2018 11:42:02 -0400 Subject: [PATCH 001/226] Add AuthenticationConverter interface - Adding an AuthenticationConverter interface - Retro-fitting ServerFormLoginAuthenticationConverter, ServerHttpBasicAuthenticationConverter, and ServerOAuth2LoginAuthenticationTokenConverter to implement AuthenticationConverter - Deprecate existing AuthenticationWebFilter.setAuthenticationConverter and add overloaded one which takes AuthenticationConverter Fixes gh-5338 --- ...uth2LoginAuthenticationTokenConverter.java | 8 ++--- ...erverFormLoginAuthenticationConverter.java | 11 +++--- ...erverHttpBasicAuthenticationConverter.java | 4 +-- .../AuthenticationConverter.java | 36 +++++++++++++++++++ .../AuthenticationWebFilter.java | 19 ++++++++-- .../AuthenticationWebFilterTests.java | 21 +++++------ 6 files changed, 71 insertions(+), 28 deletions(-) create mode 100644 web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationConverter.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/ServerOAuth2LoginAuthenticationTokenConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/ServerOAuth2LoginAuthenticationTokenConverter.java index 80d62789ece..9173de2e4fc 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/ServerOAuth2LoginAuthenticationTokenConverter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/ServerOAuth2LoginAuthenticationTokenConverter.java @@ -16,8 +16,6 @@ package org.springframework.security.oauth2.client.web; -import java.util.function.Function; - import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; @@ -27,6 +25,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.web.server.authentication.AuthenticationConverter; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; import org.springframework.web.server.ServerWebExchange; @@ -40,10 +39,9 @@ * converter does not validate any errors it only performs a conversion. * @author Rob Winch * @since 5.1 - * @see org.springframework.security.web.server.authentication.AuthenticationWebFilter#setAuthenticationConverter(Function) + * @see org.springframework.security.web.server.authentication.AuthenticationWebFilter#setAuthenticationConverter(AuthenticationConverter) */ -public class ServerOAuth2LoginAuthenticationTokenConverter implements - Function> { +public class ServerOAuth2LoginAuthenticationTokenConverter implements AuthenticationConverter { static final String AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE = "authorization_request_not_found"; diff --git a/web/src/main/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverter.java b/web/src/main/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverter.java index 33ff40774d9..6bef328b926 100644 --- a/web/src/main/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverter.java +++ b/web/src/main/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverter.java @@ -15,16 +15,15 @@ */ package org.springframework.security.web.server; -import java.util.function.Function; - -import org.springframework.util.Assert; -import reactor.core.publisher.Mono; - import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.authentication.AuthenticationConverter; +import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + /** * Converts a ServerWebExchange into a UsernamePasswordAuthenticationToken from the form * data HTTP parameters. @@ -32,7 +31,7 @@ * @author Rob Winch * @since 5.0 */ -public class ServerFormLoginAuthenticationConverter implements Function> { +public class ServerFormLoginAuthenticationConverter implements AuthenticationConverter { private String usernameParameter = "username"; diff --git a/web/src/main/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverter.java b/web/src/main/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverter.java index faaa19ed49d..6829f53bde8 100644 --- a/web/src/main/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverter.java +++ b/web/src/main/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverter.java @@ -16,12 +16,12 @@ package org.springframework.security.web.server; import java.util.Base64; -import java.util.function.Function; import org.springframework.http.HttpHeaders; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.authentication.AuthenticationConverter; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @@ -32,7 +32,7 @@ * @author Rob Winch * @since 5.0 */ -public class ServerHttpBasicAuthenticationConverter implements Function> { +public class ServerHttpBasicAuthenticationConverter implements AuthenticationConverter { public static final String BASIC = "Basic "; diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationConverter.java b/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationConverter.java new file mode 100644 index 00000000000..73c473d2028 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationConverter.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.authentication; + +import java.util.function.Function; + +import org.springframework.security.core.Authentication; +import org.springframework.web.server.ServerWebExchange; + +import reactor.core.publisher.Mono; + +/** + * A strategy used for converting from a {@link ServerWebExchange} to an {@link Authentication} used for + * authenticating with a provided {@link org.springframework.security.authentication.ReactiveAuthenticationManager}. + * If the result is {@link Mono#empty()}, then it signals that no authentication attempt should be made. + * + * @author Eric Deandrea + * @since 5.1 + */ +@FunctionalInterface +public interface AuthenticationConverter extends Function> { +} diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java b/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java index bac03405799..b7970a57bdf 100644 --- a/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java +++ b/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java @@ -67,7 +67,7 @@ public class AuthenticationWebFilter implements WebFilter { private ServerAuthenticationSuccessHandler authenticationSuccessHandler = new WebFilterChainServerAuthenticationSuccessHandler(); - private Function> authenticationConverter = new ServerHttpBasicAuthenticationConverter(); + private AuthenticationConverter authenticationConverter = new ServerHttpBasicAuthenticationConverter(); private ServerAuthenticationFailureHandler authenticationFailureHandler = new ServerAuthenticationEntryPointFailureHandler(new HttpBasicServerAuthenticationEntryPoint()); @@ -138,8 +138,23 @@ public void setAuthenticationSuccessHandler(ServerAuthenticationSuccessHandler a * that no authentication attempt should be made. The default converter is * {@link ServerHttpBasicAuthenticationConverter} * @param authenticationConverter the converter to use + * @deprecated As of 5.1 in favor of {@link #setAuthenticationConverter(AuthenticationConverter)} + * @see #setAuthenticationConverter(AuthenticationConverter) */ + @Deprecated public void setAuthenticationConverter(Function> authenticationConverter) { + setAuthenticationConverter((AuthenticationConverter) authenticationConverter); + } + + /** + * Sets the strategy used for converting from a {@link ServerWebExchange} to an {@link Authentication} used for + * authenticating with the provided {@link ReactiveAuthenticationManager}. If the result is empty, then it signals + * that no authentication attempt should be made. The default converter is + * {@link ServerHttpBasicAuthenticationConverter} + * @param authenticationConverter the converter to use + * @since 5.1 + */ + public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) { Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); this.authenticationConverter = authenticationConverter; } @@ -156,7 +171,7 @@ public void setAuthenticationFailureHandler( /** * Sets the matcher used to determine when creating an {@link Authentication} from - * {@link #setAuthenticationConverter(Function)} to be authentication. If the converter returns an empty + * {@link #setAuthenticationConverter(AuthenticationConverter)} to be authentication. If the converter returns an empty * result, then no authentication is attempted. The default is any request * @param requiresAuthenticationMatcher the matcher to use. Cannot be null. */ diff --git a/web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationWebFilterTests.java b/web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationWebFilterTests.java index b026040ef10..34354bc152f 100644 --- a/web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationWebFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationWebFilterTests.java @@ -16,14 +16,18 @@ package org.springframework.security.web.server.authentication; -import java.util.function.Function; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.*; +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import reactor.core.publisher.Mono; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.ReactiveAuthenticationManager; @@ -34,17 +38,8 @@ import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.test.web.reactive.server.EntityExchangeResult; import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.server.ServerWebExchange; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; +import reactor.core.publisher.Mono; /** @@ -56,7 +51,7 @@ public class AuthenticationWebFilterTests { @Mock private ServerAuthenticationSuccessHandler successHandler; @Mock - private Function> authenticationConverter; + private AuthenticationConverter authenticationConverter; @Mock private ReactiveAuthenticationManager authenticationManager; @Mock From 56d25516bf869be2dd6f41b9108a2c492311809f Mon Sep 17 00:00:00 2001 From: Eric Deandrea Date: Thu, 16 Aug 2018 21:52:39 -0400 Subject: [PATCH 002/226] Cleanup based on review comments Fixes gh-5338 --- ...rOAuth2LoginAuthenticationTokenConverter.java | 12 ++++++------ ...th2LoginAuthenticationTokenConverterTest.java | 2 +- .../ServerFormLoginAuthenticationConverter.java | 6 +++--- .../ServerHttpBasicAuthenticationConverter.java | 6 +++--- .../authentication/AuthenticationWebFilter.java | 14 +++++++------- ...r.java => ServerAuthenticationConverter.java} | 10 +++++++--- ...verFormLoginAuthenticationConverterTests.java | 6 +++--- ...verHttpBasicAuthenticationConverterTests.java | 16 ++++++++-------- .../AuthenticationWebFilterTests.java | 14 +++++++------- 9 files changed, 45 insertions(+), 41 deletions(-) rename web/src/main/java/org/springframework/security/web/server/authentication/{AuthenticationConverter.java => ServerAuthenticationConverter.java} (80%) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/ServerOAuth2LoginAuthenticationTokenConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/ServerOAuth2LoginAuthenticationTokenConverter.java index 9173de2e4fc..1011a41e7a9 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/ServerOAuth2LoginAuthenticationTokenConverter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/ServerOAuth2LoginAuthenticationTokenConverter.java @@ -25,7 +25,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; -import org.springframework.security.web.server.authentication.AuthenticationConverter; +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; import org.springframework.web.server.ServerWebExchange; @@ -39,9 +39,9 @@ * converter does not validate any errors it only performs a conversion. * @author Rob Winch * @since 5.1 - * @see org.springframework.security.web.server.authentication.AuthenticationWebFilter#setAuthenticationConverter(AuthenticationConverter) + * @see org.springframework.security.web.server.authentication.AuthenticationWebFilter#setAuthenticationConverter(ServerAuthenticationConverter) */ -public class ServerOAuth2LoginAuthenticationTokenConverter implements AuthenticationConverter { +public class ServerOAuth2LoginAuthenticationTokenConverter implements ServerAuthenticationConverter { static final String AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE = "authorization_request_not_found"; @@ -70,7 +70,7 @@ public void setAuthorizationRequestRepository( } @Override - public Mono apply(ServerWebExchange serverWebExchange) { + public Mono convert(ServerWebExchange serverWebExchange) { return this.authorizationRequestRepository.removeAuthorizationRequest(serverWebExchange) .switchIfEmpty(oauth2AuthenticationException(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE)) .flatMap(authorizationRequest -> authenticationRequest(serverWebExchange, authorizationRequest)); @@ -95,14 +95,14 @@ private Mono authenticationRequest(ServerWebExch }) .switchIfEmpty(oauth2AuthenticationException(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE)) .map(clientRegistration -> { - OAuth2AuthorizationResponse authorizationResponse = convert(exchange); + OAuth2AuthorizationResponse authorizationResponse = convertToResponse(exchange); OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken( clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse)); return authenticationRequest; }); } - private static OAuth2AuthorizationResponse convert(ServerWebExchange exchange) { + private static OAuth2AuthorizationResponse convertToResponse(ServerWebExchange exchange) { MultiValueMap queryParams = exchange.getRequest() .getQueryParams(); String redirectUri = UriComponentsBuilder.fromUri(exchange.getRequest().getURI()) diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/ServerOAuth2LoginAuthenticationTokenConverterTest.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/ServerOAuth2LoginAuthenticationTokenConverterTest.java index 8ceb5b17275..ac20d8a508e 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/ServerOAuth2LoginAuthenticationTokenConverterTest.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/ServerOAuth2LoginAuthenticationTokenConverterTest.java @@ -141,6 +141,6 @@ public void applyWhenCodeParameterFoundThenCode() { private OAuth2LoginAuthenticationToken applyConverter() { MockServerWebExchange exchange = MockServerWebExchange.from(this.request); - return (OAuth2LoginAuthenticationToken) this.converter.apply(exchange).block(); + return (OAuth2LoginAuthenticationToken) this.converter.convert(exchange).block(); } } diff --git a/web/src/main/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverter.java b/web/src/main/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverter.java index 6bef328b926..a1a52a4b45b 100644 --- a/web/src/main/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverter.java +++ b/web/src/main/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverter.java @@ -17,7 +17,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.web.server.authentication.AuthenticationConverter; +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; import org.springframework.web.server.ServerWebExchange; @@ -31,14 +31,14 @@ * @author Rob Winch * @since 5.0 */ -public class ServerFormLoginAuthenticationConverter implements AuthenticationConverter { +public class ServerFormLoginAuthenticationConverter implements ServerAuthenticationConverter { private String usernameParameter = "username"; private String passwordParameter = "password"; @Override - public Mono apply(ServerWebExchange exchange) { + public Mono convert(ServerWebExchange exchange) { return exchange.getFormData() .map( data -> createAuthentication(data)); } diff --git a/web/src/main/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverter.java b/web/src/main/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverter.java index 6829f53bde8..7f431f09e80 100644 --- a/web/src/main/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverter.java +++ b/web/src/main/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverter.java @@ -21,7 +21,7 @@ import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.web.server.authentication.AuthenticationConverter; +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @@ -32,12 +32,12 @@ * @author Rob Winch * @since 5.0 */ -public class ServerHttpBasicAuthenticationConverter implements AuthenticationConverter { +public class ServerHttpBasicAuthenticationConverter implements ServerAuthenticationConverter { public static final String BASIC = "Basic "; @Override - public Mono apply(ServerWebExchange exchange) { + public Mono convert(ServerWebExchange exchange) { ServerHttpRequest request = exchange.getRequest(); String authorization = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java b/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java index b7970a57bdf..d3a5d28ac93 100644 --- a/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java +++ b/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java @@ -67,7 +67,7 @@ public class AuthenticationWebFilter implements WebFilter { private ServerAuthenticationSuccessHandler authenticationSuccessHandler = new WebFilterChainServerAuthenticationSuccessHandler(); - private AuthenticationConverter authenticationConverter = new ServerHttpBasicAuthenticationConverter(); + private ServerAuthenticationConverter authenticationConverter = new ServerHttpBasicAuthenticationConverter(); private ServerAuthenticationFailureHandler authenticationFailureHandler = new ServerAuthenticationEntryPointFailureHandler(new HttpBasicServerAuthenticationEntryPoint()); @@ -88,7 +88,7 @@ public AuthenticationWebFilter(ReactiveAuthenticationManager authenticationManag public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { return this.requiresAuthenticationMatcher.matches(exchange) .filter( matchResult -> matchResult.isMatch()) - .flatMap( matchResult -> this.authenticationConverter.apply(exchange)) + .flatMap( matchResult -> this.authenticationConverter.convert(exchange)) .switchIfEmpty(chain.filter(exchange).then(Mono.empty())) .flatMap( token -> authenticate(exchange, chain, token)); } @@ -138,12 +138,12 @@ public void setAuthenticationSuccessHandler(ServerAuthenticationSuccessHandler a * that no authentication attempt should be made. The default converter is * {@link ServerHttpBasicAuthenticationConverter} * @param authenticationConverter the converter to use - * @deprecated As of 5.1 in favor of {@link #setAuthenticationConverter(AuthenticationConverter)} - * @see #setAuthenticationConverter(AuthenticationConverter) + * @deprecated As of 5.1 in favor of {@link #setAuthenticationConverter(ServerAuthenticationConverter)} + * @see #setAuthenticationConverter(ServerAuthenticationConverter) */ @Deprecated public void setAuthenticationConverter(Function> authenticationConverter) { - setAuthenticationConverter((AuthenticationConverter) authenticationConverter); + setAuthenticationConverter((ServerAuthenticationConverter) authenticationConverter); } /** @@ -154,7 +154,7 @@ public void setAuthenticationConverter(Function> { +public interface ServerAuthenticationConverter { + /** + * Converts a {@link ServerWebExchange} to an {@link Authentication} + * @param exchange The {@link ServerWebExchange} + * @return A {@link Mono} representing an {@link Authentication} + */ + Mono convert(ServerWebExchange exchange); } diff --git a/web/src/test/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverterTests.java b/web/src/test/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverterTests.java index b10c1027eb3..294b532079d 100644 --- a/web/src/test/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverterTests.java @@ -55,7 +55,7 @@ public void applyWhenUsernameAndPasswordThenCreatesTokenSuccess() { this.data.add("username", username); this.data.add("password", password); - Authentication authentication = this.converter.apply(this.exchange).block(); + Authentication authentication = this.converter.convert(this.exchange).block(); assertThat(authentication.getName()).isEqualTo(username); assertThat(authentication.getCredentials()).isEqualTo(password); @@ -73,7 +73,7 @@ public void applyWhenCustomParametersAndUsernameAndPasswordThenCreatesTokenSucce this.data.add(usernameParameter, username); this.data.add(passwordParameter, password); - Authentication authentication = this.converter.apply(this.exchange).block(); + Authentication authentication = this.converter.convert(this.exchange).block(); assertThat(authentication.getName()).isEqualTo(username); assertThat(authentication.getCredentials()).isEqualTo(password); @@ -82,7 +82,7 @@ public void applyWhenCustomParametersAndUsernameAndPasswordThenCreatesTokenSucce @Test public void applyWhenNoDataThenCreatesTokenSuccess() { - Authentication authentication = this.converter.apply(this.exchange).block(); + Authentication authentication = this.converter.convert(this.exchange).block(); assertThat(authentication.getName()).isNullOrEmpty(); assertThat(authentication.getCredentials()).isNull(); diff --git a/web/src/test/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverterTests.java b/web/src/test/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverterTests.java index 22561ad5861..63f5c796f4d 100644 --- a/web/src/test/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverterTests.java @@ -37,49 +37,49 @@ public class ServerHttpBasicAuthenticationConverterTests { @Test public void applyWhenNoAuthorizationHeaderThenEmpty() { - Mono result = apply(this.request); + Mono result = convert(this.request); assertThat(result.block()).isNull(); } @Test public void applyWhenEmptyAuthorizationHeaderThenEmpty() { - Mono result = apply(this.request.header(HttpHeaders.AUTHORIZATION, "")); + Mono result = convert(this.request.header(HttpHeaders.AUTHORIZATION, "")); assertThat(result.block()).isNull(); } @Test public void applyWhenOnlyBasicAuthorizationHeaderThenEmpty() { - Mono result = apply(this.request.header(HttpHeaders.AUTHORIZATION, "Basic ")); + Mono result = convert(this.request.header(HttpHeaders.AUTHORIZATION, "Basic ")); assertThat(result.block()).isNull(); } @Test public void applyWhenNotBase64ThenEmpty() { - Mono result = apply(this.request.header(HttpHeaders.AUTHORIZATION, "Basic z")); + Mono result = convert(this.request.header(HttpHeaders.AUTHORIZATION, "Basic z")); assertThat(result.block()).isNull(); } @Test public void applyWhenNoSemicolonThenEmpty() { - Mono result = apply(this.request.header(HttpHeaders.AUTHORIZATION, "Basic dXNlcg==")); + Mono result = convert(this.request.header(HttpHeaders.AUTHORIZATION, "Basic dXNlcg==")); assertThat(result.block()).isNull(); } @Test public void applyWhenUserPasswordThenAuthentication() { - Mono result = apply(this.request.header(HttpHeaders.AUTHORIZATION, "Basic dXNlcjpwYXNzd29yZA==")); + Mono result = convert(this.request.header(HttpHeaders.AUTHORIZATION, "Basic dXNlcjpwYXNzd29yZA==")); UsernamePasswordAuthenticationToken authentication = result.cast(UsernamePasswordAuthenticationToken.class).block(); assertThat(authentication.getPrincipal()).isEqualTo("user"); assertThat(authentication.getCredentials()).isEqualTo("password"); } - private Mono apply(MockServerHttpRequest.BaseBuilder request) { - return this.converter.apply(MockServerWebExchange.from(this.request.build())); + private Mono convert(MockServerHttpRequest.BaseBuilder request) { + return this.converter.convert(MockServerWebExchange.from(this.request.build())); } } diff --git a/web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationWebFilterTests.java b/web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationWebFilterTests.java index 34354bc152f..137149e14ec 100644 --- a/web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationWebFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationWebFilterTests.java @@ -51,7 +51,7 @@ public class AuthenticationWebFilterTests { @Mock private ServerAuthenticationSuccessHandler successHandler; @Mock - private AuthenticationConverter authenticationConverter; + private ServerAuthenticationConverter authenticationConverter; @Mock private ReactiveAuthenticationManager authenticationManager; @Mock @@ -135,7 +135,7 @@ public void filterWhenDefaultsAndAuthenticationFailThenUnauthorized() { @Test public void filterWhenConvertEmptyThenOk() { - when(this.authenticationConverter.apply(any())).thenReturn(Mono.empty()); + when(this.authenticationConverter.convert(any())).thenReturn(Mono.empty()); WebTestClient client = WebTestClientBuilder .bindToWebFilters(this.filter) @@ -156,7 +156,7 @@ public void filterWhenConvertEmptyThenOk() { @Test public void filterWhenConvertErrorThenServerError() { - when(this.authenticationConverter.apply(any())).thenReturn(Mono.error(new RuntimeException("Unexpected"))); + when(this.authenticationConverter.convert(any())).thenReturn(Mono.error(new RuntimeException("Unexpected"))); WebTestClient client = WebTestClientBuilder .bindToWebFilters(this.filter) @@ -177,7 +177,7 @@ public void filterWhenConvertErrorThenServerError() { @Test public void filterWhenConvertAndAuthenticationSuccessThenSuccess() { Mono authentication = Mono.just(new TestingAuthenticationToken("test", "this", "ROLE_USER")); - when(this.authenticationConverter.apply(any())).thenReturn(authentication); + when(this.authenticationConverter.convert(any())).thenReturn(authentication); when(this.authenticationManager.authenticate(any())).thenReturn(authentication); when(this.successHandler.onAuthenticationSuccess(any(), any())).thenReturn(Mono.empty()); when(this.securityContextRepository.save(any(), any())).thenAnswer( a -> Mono.just(a.getArguments()[0])); @@ -202,7 +202,7 @@ public void filterWhenConvertAndAuthenticationSuccessThenSuccess() { @Test public void filterWhenConvertAndAuthenticationEmptyThenServerError() { Mono authentication = Mono.just(new TestingAuthenticationToken("test", "this", "ROLE_USER")); - when(this.authenticationConverter.apply(any())).thenReturn(authentication); + when(this.authenticationConverter.convert(any())).thenReturn(authentication); when(this.authenticationManager.authenticate(any())).thenReturn(Mono.empty()); WebTestClient client = WebTestClientBuilder @@ -245,7 +245,7 @@ public void filterWhenNotMatchAndConvertAndAuthenticationSuccessThenContinues() @Test public void filterWhenConvertAndAuthenticationFailThenEntryPoint() { Mono authentication = Mono.just(new TestingAuthenticationToken("test", "this", "ROLE_USER")); - when(this.authenticationConverter.apply(any())).thenReturn(authentication); + when(this.authenticationConverter.convert(any())).thenReturn(authentication); when(this.authenticationManager.authenticate(any())).thenReturn(Mono.error(new BadCredentialsException("Failed"))); when(this.failureHandler.onAuthenticationFailure(any(), any())).thenReturn(Mono.empty()); @@ -268,7 +268,7 @@ public void filterWhenConvertAndAuthenticationFailThenEntryPoint() { @Test public void filterWhenConvertAndAuthenticationExceptionThenServerError() { Mono authentication = Mono.just(new TestingAuthenticationToken("test", "this", "ROLE_USER")); - when(this.authenticationConverter.apply(any())).thenReturn(authentication); + when(this.authenticationConverter.convert(any())).thenReturn(authentication); when(this.authenticationManager.authenticate(any())).thenReturn(Mono.error(new RuntimeException("Failed"))); WebTestClient client = WebTestClientBuilder From 58fa211590553f6af47427e27fd321a3454894ed Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 14 May 2018 11:48:31 -0500 Subject: [PATCH 003/226] LoginPageGeneratingWebFilter conditionally renders formLogin Issue: gh-4807 --- config/spring-security-config.gradle | 1 + .../WebFluxSecurityConfiguration.java | 19 ++-- .../config/web/server/ServerHttpSecurity.java | 31 +++-- .../config/web/server/FormLoginTests.java | 36 +++++- .../config/web/server/OAuth2LoginTests.java | 106 ++++++++++++++++++ .../ui/LoginPageGeneratingWebFilter.java | 76 +++++++------ 6 files changed, 215 insertions(+), 54 deletions(-) create mode 100644 config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index 23fa96f840a..fc56abf254f 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -35,6 +35,7 @@ dependencies { testCompile powerMock2Dependencies testCompile spockDependencies testCompile 'ch.qos.logback:logback-classic' + testCompile 'io.projectreactor.ipc:reactor-netty' testCompile 'javax.annotation:jsr250-api:1.0' testCompile 'javax.xml.bind:jaxb-api' testCompile 'ldapsdk:ldapsdk:4.1' diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java index bed91d4f973..30b2b44b7f4 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java @@ -87,13 +87,14 @@ private SecurityWebFilterChain springSecurityFilterChain() { private SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http .authorizeExchange() - .anyExchange().authenticated() - .and() - .httpBasic().and() - .formLogin(); + .anyExchange().authenticated(); - if (isOAuth2Present) { + if (isOAuth2Present && OAuth2ClasspathGuard.shouldConfigure(this.context)) { OAuth2ClasspathGuard.configure(this.context, http); + } else { + http + .httpBasic().and() + .formLogin(); } SecurityWebFilterChain result = http.build(); @@ -102,11 +103,13 @@ private SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http private static class OAuth2ClasspathGuard { static void configure(ApplicationContext context, ServerHttpSecurity http) { + http.oauth2Login(); + } + + static boolean shouldConfigure(ApplicationContext context) { ClassLoader loader = context.getClassLoader(); Class reactiveClientRegistrationRepositoryClass = ClassUtils.resolveClassName(REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME, loader); - if (context.getBeanNamesForType(reactiveClientRegistrationRepositoryClass).length == 1) { - http.oauth2Login(); - } + return context.getBeanNamesForType(reactiveClientRegistrationRepositoryClass).length == 1; } } } diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 6c8c40622cf..d54aed8296d 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -41,6 +41,7 @@ import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2LoginReactiveAuthenticationManager; import org.springframework.security.oauth2.client.endpoint.NimbusReactiveAuthorizationCodeTokenResponseClient; @@ -361,11 +362,7 @@ public OAuth2LoginSpec authorizedClientService(ReactiveOAuth2AuthorizedClientSer return this; } - protected void configure(LoginPageGeneratingWebFilter loginPageFilter, ServerHttpSecurity http) { - if (loginPageFilter != null) { - loginPageFilter.setOauth2AuthenticationUrlToClientName(getLinks()); - } - + protected void configure(ServerHttpSecurity http) { ReactiveClientRegistrationRepository clientRegistrationRepository = getClientRegistrationRepository(); ReactiveOAuth2AuthorizedClientService authorizedClientService = getAuthorizedClientService(); OAuth2AuthorizationRequestRedirectWebFilter oauthRedirectFilter = new OAuth2AuthorizationRequestRedirectWebFilter(clientRegistrationRepository); @@ -417,6 +414,9 @@ private ReactiveOAuth2AuthorizedClientService getAuthorizedClientService() { if (this.authorizedClientService == null) { this.authorizedClientService = getBeanOrNull(ReactiveOAuth2AuthorizedClientService.class); } + if (this.authorizedClientService == null) { + this.authorizedClientService = new InMemoryReactiveOAuth2AuthorizedClientService(getClientRegistrationRepository()); + } return this.authorizedClientService; } @@ -616,15 +616,24 @@ public SecurityWebFilterChain build() { if(this.securityContextRepository != null) { this.formLogin.securityContextRepository(this.securityContextRepository); } - if(this.formLogin.authenticationEntryPoint == null) { + if (this.authenticationEntryPoint == null) { loginPageFilter = new LoginPageGeneratingWebFilter(); - this.webFilters.add(new OrderedWebFilter(loginPageFilter, SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING.getOrder())); - this.webFilters.add(new OrderedWebFilter(new LogoutPageGeneratingWebFilter(), SecurityWebFiltersOrder.LOGOUT_PAGE_GENERATING.getOrder())); + loginPageFilter.setFormLoginEnabled(true); + this.authenticationEntryPoint = this.formLogin.authenticationEntryPoint; } this.formLogin.configure(this); } if (this.oauth2Login != null) { - this.oauth2Login.configure(loginPageFilter, this); + if (this.authenticationEntryPoint == null) { + loginPageFilter = new LoginPageGeneratingWebFilter(); + loginPageFilter.setOauth2AuthenticationUrlToClientName(this.oauth2Login.getLinks()); + } + this.oauth2Login.configure(this); + } + if (loginPageFilter != null) { + this.authenticationEntryPoint = new RedirectServerAuthenticationEntryPoint("/login"); + this.webFilters.add(new OrderedWebFilter(loginPageFilter, SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING.getOrder())); + this.webFilters.add(new OrderedWebFilter(new LogoutPageGeneratingWebFilter(), SecurityWebFiltersOrder.LOGOUT_PAGE_GENERATING.getOrder())); } if(this.logout != null) { this.logout.configure(this); @@ -638,8 +647,8 @@ public SecurityWebFilterChain build() { exceptionTranslationWebFilter.setAuthenticationEntryPoint( authenticationEntryPoint); } - if(accessDeniedHandler != null) { - exceptionTranslationWebFilter.setAccessDeniedHandler(accessDeniedHandler); + if(this.accessDeniedHandler != null) { + exceptionTranslationWebFilter.setAccessDeniedHandler(this.accessDeniedHandler); } this.addFilterAt(exceptionTranslationWebFilter, SecurityWebFiltersOrder.EXCEPTION_TRANSLATION); this.authorizeExchange.configure(this); diff --git a/config/src/test/java/org/springframework/security/config/web/server/FormLoginTests.java b/config/src/test/java/org/springframework/security/config/web/server/FormLoginTests.java index 70b0fa60f85..501c9fa22b2 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/FormLoginTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/FormLoginTests.java @@ -17,6 +17,8 @@ package org.springframework.security.config.web.server; import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; @@ -36,6 +38,8 @@ import reactor.core.publisher.Mono; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * @author Rob Winch @@ -204,9 +208,10 @@ public static class DefaultLoginPage { private LoginForm loginForm; + private OAuth2Login oauth2Login = new OAuth2Login(); + public DefaultLoginPage(WebDriver webDriver) { this.driver = webDriver; - this.loginForm = PageFactory.initElements(webDriver, LoginForm.class); } static DefaultLoginPage create(WebDriver driver) { @@ -228,10 +233,23 @@ public DefaultLoginPage assertLogout() { return this; } + public DefaultLoginPage assertLoginFormNotPresent() { + assertThatThrownBy(() -> loginForm().username("")) + .isInstanceOf(NoSuchElementException.class); + return this; + } + public LoginForm loginForm() { + if (this.loginForm == null) { + this.loginForm = PageFactory.initElements(this.driver, LoginForm.class); + } return this.loginForm; } + public OAuth2Login oauth2Login() { + return this.oauth2Login; + } + static DefaultLoginPage to(WebDriver driver) { driver.get("http://localhost/login"); return PageFactory.initElements(driver, DefaultLoginPage.class); @@ -263,6 +281,22 @@ public T submit(Class page) { return PageFactory.initElements(this.driver, page); } } + + public class OAuth2Login { + public WebElement findClientRegistrationByName(String clientName) { + return DefaultLoginPage.this.driver.findElement(By.linkText(clientName)); + } + + public OAuth2Login assertClientRegistrationByName(String clientName) { + assertThatCode(() -> findClientRegistrationByName(clientName)) + .doesNotThrowAnyException(); + return this; + } + + public DefaultLoginPage and() { + return DefaultLoginPage.this; + } + } } public static class DefaultLogoutPage { diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java new file mode 100644 index 00000000000..2ff2724f6c5 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Rule; +import org.junit.Test; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; +import org.openqa.selenium.support.PageFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.annotation.web.reactive.ServerHttpSecurityConfigurationBuilder; +import org.springframework.security.config.oauth2.client.CommonOAuth2Provider; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverBuilder; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; +import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.WebFilterChainProxy; +import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler; +import org.springframework.security.web.server.csrf.CsrfToken; +import org.springframework.stereotype.Controller; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.server.ServerWebExchange; + +import reactor.core.publisher.Mono; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class OAuth2LoginTests { + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + private WebFilterChainProxy springSecurity; + + private ClientRegistration github = CommonOAuth2Provider.GITHUB + .getBuilder("github") + .clientId("client") + .clientSecret("secret") + .build(); + + @Test + public void defaultLoginPageWithMultipleClientRegistrationsThenLinks() { + this.spring.register(OAuth2LoginWithMulitpleClientRegistrations.class).autowire(); + + WebTestClient webTestClient = WebTestClientBuilder + .bindToWebFilters(this.springSecurity) + .build(); + + WebDriver driver = WebTestClientHtmlUnitDriverBuilder + .webTestClientSetup(webTestClient) + .build(); + + FormLoginTests.DefaultLoginPage loginPage = FormLoginTests.HomePage + .to(driver, FormLoginTests.DefaultLoginPage.class) + .assertAt() + .assertLoginFormNotPresent() + .oauth2Login() + .assertClientRegistrationByName(this.github.getClientName()) + .and(); + } + + @EnableWebFluxSecurity + static class OAuth2LoginWithMulitpleClientRegistrations { + @Bean + InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() { + ClientRegistration github = CommonOAuth2Provider.GITHUB + .getBuilder("github") + .clientId("client") + .clientSecret("secret") + .build(); + ClientRegistration google = CommonOAuth2Provider.GOOGLE + .getBuilder("google") + .clientId("client") + .clientSecret("secret") + .build(); + return new InMemoryReactiveClientRegistrationRepository(github, google); + } + } +} diff --git a/web/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java b/web/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java index 373ee21e4b9..74f63f806ef 100644 --- a/web/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java +++ b/web/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java @@ -50,6 +50,12 @@ public class LoginPageGeneratingWebFilter implements WebFilter { private Map oauth2AuthenticationUrlToClientName = new HashMap<>(); + private boolean formLoginEnabled; + + public void setFormLoginEnabled(boolean enabled) { + this.formLoginEnabled = enabled; + } + public void setOauth2AuthenticationUrlToClientName( Map oauth2AuthenticationUrlToClientName) { Assert.notNull(oauth2AuthenticationUrlToClientName, "oauth2AuthenticationUrlToClientName cannot be null"); @@ -87,45 +93,47 @@ private Mono createBuffer(ServerWebExchange exchange) { private byte[] createPage(ServerWebExchange exchange, String csrfTokenHtmlInput) { MultiValueMap queryParams = exchange.getRequest() .getQueryParams(); - boolean isError = queryParams.containsKey("error"); - boolean isLogoutSuccess = queryParams.containsKey("logout"); String contextPath = exchange.getRequest().getPath().contextPath().value(); - String page = "\n" - + "\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Please sign in\n" - + " \n" - + " \n" - + " \n" - + " \n" - + "
\n" - + "
\n" - + " \n" - + createError(isError) - + createLogoutSuccess(isLogoutSuccess) - + "

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

\n" - + "

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

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

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

\n" + "

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

\n" + csrfTokenHtmlInput + + " \n" + + "
\n"; + } + private static String oauth2LoginLinks(String contextPath, Map oauth2AuthenticationUrlToClientName) { if (oauth2AuthenticationUrlToClientName.isEmpty()) { return ""; From 96eaaec041366178808638be72b37968ccd5bb2f Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 14 May 2018 15:37:02 -0500 Subject: [PATCH 004/226] Single ClientRegistration redirects by default Fixes: gh-5339 --- .../config/web/server/ServerHttpSecurity.java | 62 ++++++++++++++----- .../config/web/server/OAuth2LoginTests.java | 56 ++++++++++++----- .../server/HtmlUnitWebTestClient.java | 8 ++- 3 files changed, 93 insertions(+), 33 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index d54aed8296d..0de6031ee14 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -183,6 +183,8 @@ public class ServerHttpSecurity { private LogoutSpec logout = new LogoutSpec(); + private LoginPageSpec loginPage = new LoginPageSpec(); + private ReactiveAuthenticationManager authenticationManager; private ServerSecurityContextRepository securityContextRepository = new WebSessionServerSecurityContextRepository(); @@ -387,6 +389,16 @@ public Mono onAuthenticationFailure(WebFilterExchange webFilterExchange, }); authenticationFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository()); + MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher( + MediaType.TEXT_HTML); + htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); + Map urlToText = http.oauth2Login.getLinks(); + if (urlToText.size() == 1) { + http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, new RedirectServerAuthenticationEntryPoint(urlToText.keySet().iterator().next()))); + } else { + http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, new RedirectServerAuthenticationEntryPoint("/login"))); + } + http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC); http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION); } @@ -610,31 +622,17 @@ public SecurityWebFilterChain build() { this.httpBasic.authenticationManager(this.authenticationManager); this.httpBasic.configure(this); } - LoginPageGeneratingWebFilter loginPageFilter = null; if(this.formLogin != null) { this.formLogin.authenticationManager(this.authenticationManager); if(this.securityContextRepository != null) { this.formLogin.securityContextRepository(this.securityContextRepository); } - if (this.authenticationEntryPoint == null) { - loginPageFilter = new LoginPageGeneratingWebFilter(); - loginPageFilter.setFormLoginEnabled(true); - this.authenticationEntryPoint = this.formLogin.authenticationEntryPoint; - } this.formLogin.configure(this); } if (this.oauth2Login != null) { - if (this.authenticationEntryPoint == null) { - loginPageFilter = new LoginPageGeneratingWebFilter(); - loginPageFilter.setOauth2AuthenticationUrlToClientName(this.oauth2Login.getLinks()); - } this.oauth2Login.configure(this); } - if (loginPageFilter != null) { - this.authenticationEntryPoint = new RedirectServerAuthenticationEntryPoint("/login"); - this.webFilters.add(new OrderedWebFilter(loginPageFilter, SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING.getOrder())); - this.webFilters.add(new OrderedWebFilter(new LogoutPageGeneratingWebFilter(), SecurityWebFiltersOrder.LOGOUT_PAGE_GENERATING.getOrder())); - } + this.loginPage.configure(this); if(this.logout != null) { this.logout.configure(this); } @@ -1084,6 +1082,8 @@ public class FormLoginSpec { private ServerAuthenticationEntryPoint authenticationEntryPoint; + private boolean isEntryPointExplicit; + private ServerWebExchangeMatcher requiresAuthenticationMatcher; private ServerAuthenticationFailureHandler authenticationFailureHandler; @@ -1206,7 +1206,10 @@ public ServerHttpSecurity disable() { protected void configure(ServerHttpSecurity http) { if(this.authenticationEntryPoint == null) { + this.isEntryPointExplicit = false; loginPage("/login"); + } else { + this.isEntryPointExplicit = true; } if(http.requestCache != null) { ServerRequestCache requestCache = http.requestCache.requestCache; @@ -1233,6 +1236,35 @@ private FormLoginSpec() { } } + private class LoginPageSpec { + protected void configure(ServerHttpSecurity http) { + if (http.authenticationEntryPoint != null) { + return; + } + if (http.formLogin != null && http.formLogin.isEntryPointExplicit) { + return; + } + LoginPageGeneratingWebFilter loginPage = null; + if (http.formLogin != null && !http.formLogin.isEntryPointExplicit) { + loginPage = new LoginPageGeneratingWebFilter(); + loginPage.setFormLoginEnabled(true); + } + if (http.oauth2Login != null) { + Map urlToText = http.oauth2Login.getLinks(); + if (loginPage == null) { + loginPage = new LoginPageGeneratingWebFilter(); + } + loginPage.setOauth2AuthenticationUrlToClientName(urlToText); + } + if (loginPage != null) { + http.addFilterAt(loginPage, SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING); + http.addFilterAt(new LogoutPageGeneratingWebFilter(), SecurityWebFiltersOrder.LOGOUT_PAGE_GENERATING); + } + } + + private LoginPageSpec() {} + } + /** * Configures HTTP Response Headers. * diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java index 2ff2724f6c5..a2b4e1f73f4 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java @@ -21,29 +21,20 @@ import org.junit.Rule; import org.junit.Test; import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.PageFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; -import org.springframework.security.config.annotation.web.reactive.ServerHttpSecurityConfigurationBuilder; import org.springframework.security.config.oauth2.client.CommonOAuth2Provider; import org.springframework.security.config.test.SpringTestRule; import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverBuilder; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; -import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; -import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.WebFilterChainProxy; -import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler; -import org.springframework.security.web.server.csrf.CsrfToken; -import org.springframework.stereotype.Controller; import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; @@ -59,7 +50,7 @@ public class OAuth2LoginTests { @Autowired private WebFilterChainProxy springSecurity; - private ClientRegistration github = CommonOAuth2Provider.GITHUB + private static ClientRegistration github = CommonOAuth2Provider.GITHUB .getBuilder("github") .clientId("client") .clientSecret("secret") @@ -90,11 +81,6 @@ public void defaultLoginPageWithMultipleClientRegistrationsThenLinks() { static class OAuth2LoginWithMulitpleClientRegistrations { @Bean InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() { - ClientRegistration github = CommonOAuth2Provider.GITHUB - .getBuilder("github") - .clientId("client") - .clientSecret("secret") - .build(); ClientRegistration google = CommonOAuth2Provider.GOOGLE .getBuilder("google") .clientId("client") @@ -103,4 +89,40 @@ InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() { return new InMemoryReactiveClientRegistrationRepository(github, google); } } + + @Test + public void defaultLoginPageWithSingleClientRegistrationThenRedirect() { + this.spring.register(OAuth2LoginWithSingleClientRegistrations.class).autowire(); + + WebTestClient webTestClient = WebTestClientBuilder + .bindToWebFilters(new GitHubWebFilter(), this.springSecurity) + .build(); + + WebDriver driver = WebTestClientHtmlUnitDriverBuilder + .webTestClientSetup(webTestClient) + .build(); + + driver.get("http://localhost/"); + + assertThat(driver.getCurrentUrl()).startsWith("https://github.com/login/oauth/authorize"); + } + + @EnableWebFluxSecurity + static class OAuth2LoginWithSingleClientRegistrations { + @Bean + InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryReactiveClientRegistrationRepository(github); + } + } + + static class GitHubWebFilter implements WebFilter { + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + if (exchange.getRequest().getURI().getHost().equals("github.com")) { + return exchange.getResponse().setComplete(); + } + return chain.filter(exchange); + } + } } diff --git a/config/src/test/java/org/springframework/security/htmlunit/server/HtmlUnitWebTestClient.java b/config/src/test/java/org/springframework/security/htmlunit/server/HtmlUnitWebTestClient.java index f9dfab20929..111bd62c31f 100644 --- a/config/src/test/java/org/springframework/security/htmlunit/server/HtmlUnitWebTestClient.java +++ b/config/src/test/java/org/springframework/security/htmlunit/server/HtmlUnitWebTestClient.java @@ -151,8 +151,14 @@ public Mono filter(ClientRequest request, ExchangeFunction next) private Mono redirectIfNecessary(ClientRequest request, ExchangeFunction next, ClientResponse response) { URI location = response.headers().asHttpHeaders().getLocation(); + String host = request.url().getHost(); + String scheme = request.url().getScheme(); if(location != null) { - ClientRequest redirect = ClientRequest.method(HttpMethod.GET, URI.create("http://localhost" + location.toASCIIString())) + String redirectUrl = location.toASCIIString(); + if (location.getHost() == null) { + redirectUrl = scheme+ "://" + host + location.toASCIIString(); + } + ClientRequest redirect = ClientRequest.method(HttpMethod.GET, URI.create(redirectUrl)) .headers(headers -> headers.addAll(request.headers())) .cookies(cookies -> cookies.addAll(request.cookies())) .attributes(attributes -> attributes.putAll(request.attributes())) From ae6f3244fb77029e47a543b10154f8657517090d Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 14 May 2018 21:44:09 -0500 Subject: [PATCH 005/226] Release 5.1.0.M1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f92573b2f0c..1fa3b1714b6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ gaeVersion=1.9.63 springBootVersion=2.0.1.RELEASE -version=5.1.0.BUILD-SNAPSHOT +version=5.1.0.M1 From 41be145b25bae9fbea1bbc82d636273aa6eebd1f Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 14 May 2018 21:57:08 -0500 Subject: [PATCH 006/226] Disable SNAPSHOT tests for release --- Jenkinsfile | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 8703b270ffc..11385ea6dba 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -54,19 +54,6 @@ try { } } }, - snapshots: { - stage('Snapshot Tests') { - node { - checkout scm - try { - sh "./gradlew clean test -PspringVersion='5.+' -PreactorVersion=Bismuth-BUILD-SNAPSHOT -PspringDataVersion=Kay-BUILD-SNAPSHOT --refresh-dependencies --no-daemon --stacktrace" - } catch(Exception e) { - currentBuild.result = 'FAILED: snapshots' - throw e - } - } - } - }, jdk9: { stage('JDK 9') { node { From 2f9cd49351c41d5a0affa3a9eb93347cf8ef4fc1 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 14 May 2018 21:58:27 -0500 Subject: [PATCH 007/226] Next Development Version --- Jenkinsfile | 13 +++++++++++++ gradle.properties | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 11385ea6dba..8703b270ffc 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -54,6 +54,19 @@ try { } } }, + snapshots: { + stage('Snapshot Tests') { + node { + checkout scm + try { + sh "./gradlew clean test -PspringVersion='5.+' -PreactorVersion=Bismuth-BUILD-SNAPSHOT -PspringDataVersion=Kay-BUILD-SNAPSHOT --refresh-dependencies --no-daemon --stacktrace" + } catch(Exception e) { + currentBuild.result = 'FAILED: snapshots' + throw e + } + } + } + }, jdk9: { stage('JDK 9') { node { diff --git a/gradle.properties b/gradle.properties index 1fa3b1714b6..f92573b2f0c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ gaeVersion=1.9.63 springBootVersion=2.0.1.RELEASE -version=5.1.0.M1 +version=5.1.0.BUILD-SNAPSHOT From d406a9259345c8c41158d2be54fc2927e6b3b154 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 15 May 2018 08:10:55 -0600 Subject: [PATCH 008/226] HttpConfigTests groovy->java Issue: gh-4939 --- .../config/http/HttpConfigTests.groovy | 79 ------------ .../security/config/http/HttpConfigTests.java | 114 ++++++++++++++++++ .../config/http/HttpConfigTests-Minimal.xml | 32 +++++ 3 files changed, 146 insertions(+), 79 deletions(-) delete mode 100644 config/src/test/groovy/org/springframework/security/config/http/HttpConfigTests.groovy create mode 100644 config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-Minimal.xml diff --git a/config/src/test/groovy/org/springframework/security/config/http/HttpConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/HttpConfigTests.groovy deleted file mode 100644 index 00545fb508b..00000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/HttpConfigTests.groovy +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2002-2012 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ -package org.springframework.security.config.http - -import static org.mockito.Matchers.any -import static org.mockito.Matchers.eq -import static org.mockito.Mockito.* - -import javax.servlet.http.HttpServletResponse -import javax.servlet.http.HttpServletResponseWrapper - -import org.springframework.mock.web.MockFilterChain -import org.springframework.mock.web.MockHttpServletRequest -import org.springframework.mock.web.MockHttpServletResponse - -/** - * - * @author Rob Winch - */ -class HttpConfigTests extends AbstractHttpConfigTests { - MockHttpServletRequest request = new MockHttpServletRequest('GET','/secure') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - - def 'http minimal configuration works'() { - setup: - xml.http() {} - createAppContext(""" - - """) - when: 'request protected URL' - springSecurityFilterChain.doFilter(request,response,chain) - then: 'sent to login page' - response.status == HttpServletResponse.SC_MOVED_TEMPORARILY - response.redirectedUrl == 'http://localhost/login' - } - - def 'http disable-url-rewriting defaults to true'() { - setup: - xml.http() {} - createAppContext(""" - - """) - HttpServletResponse testResponse = new HttpServletResponseWrapper(response) { - public String encodeURL(String url) { - throw new RuntimeException("Unexpected invocation of encodeURL") - } - public String encodeRedirectURL(String url) { - throw new RuntimeException("Unexpected invocation of encodeURL") - } - public String encodeUrl(String url) { - throw new RuntimeException("Unexpected invocation of encodeURL") - } - public String encodeRedirectUrl(String url) { - throw new RuntimeException("Unexpected invocation of encodeURL") - } - } - when: 'request protected URL' - springSecurityFilterChain.doFilter(request,testResponse,{ request,response-> - response.encodeURL("/url") - response.encodeRedirectURL("/url") - response.encodeUrl("/url") - response.encodeRedirectUrl("/url") - }) - then: 'sent to login page' - response.status == HttpServletResponse.SC_MOVED_TEMPORARILY - response.redirectedUrl == 'http://localhost/login' - } -} \ No newline at end of file diff --git a/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java b/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java new file mode 100644 index 00000000000..dfc2e7a34d9 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.http; + +import org.apache.http.HttpStatus; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.test.web.servlet.MockMvc; + +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * + * @author Rob Winch + * @author Josh Cummings + */ +public class HttpConfigTests { + + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/HttpConfigTests"; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + MockMvc mvc; + + @Test + public void getWhenUsingMinimalConfigurationThenRedirectsToLogin() + throws Exception { + + this.spring.configLocations(this.xml("Minimal")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login")); + } + + @Test + public void getWhenUsingMinimalConfigurationThenPreventsSessionAsUrlParameter() + throws Exception { + + this.spring.configLocations(this.xml("Minimal")).autowire(); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + FilterChainProxy proxy = this.spring.getContext().getBean(FilterChainProxy.class); + + proxy.doFilter( + request, + new EncodeUrlDenyingHttpServletResponseWrapper(response), + (req, resp) -> {}); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY); + assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/login"); + } + + private static class EncodeUrlDenyingHttpServletResponseWrapper + extends HttpServletResponseWrapper { + + public EncodeUrlDenyingHttpServletResponseWrapper(HttpServletResponse response) { + super(response); + } + + @Override + public String encodeURL(String url) { + throw new RuntimeException("Unexpected invocation of encodeURL"); + } + + @Override + public String encodeRedirectURL(String url) { + throw new RuntimeException("Unexpected invocation of encodeURL"); + } + + @Override + public String encodeUrl(String url) { + throw new RuntimeException("Unexpected invocation of encodeURL"); + } + + @Override + public String encodeRedirectUrl(String url) { + throw new RuntimeException("Unexpected invocation of encodeURL"); + } + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-Minimal.xml b/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-Minimal.xml new file mode 100644 index 00000000000..1db9eff2276 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-Minimal.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + From 1db0782fde2e087f0f0c137675777012c32cafed Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 15 May 2018 08:11:46 -0600 Subject: [PATCH 009/226] HttpCorsConfigTests groovy->java Issue: gh-4939 --- .../config/http/HttpCorsConfigTests.groovy | 171 ------------------ .../config/http/HttpCorsConfigTests.java | 168 +++++++++++++++++ .../http/HttpCorsConfigTests-RequiresMvc.xml | 31 ++++ .../http/HttpCorsConfigTests-WithCors.xml | 45 +++++ ...onfigTests-WithCorsConfigurationSource.xml | 39 ++++ .../HttpCorsConfigTests-WithCorsFilter.xml | 43 +++++ 6 files changed, 326 insertions(+), 171 deletions(-) delete mode 100644 config/src/test/groovy/org/springframework/security/config/http/HttpCorsConfigTests.groovy create mode 100644 config/src/test/java/org/springframework/security/config/http/HttpCorsConfigTests.java create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-RequiresMvc.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCors.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsConfigurationSource.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsFilter.xml diff --git a/config/src/test/groovy/org/springframework/security/config/http/HttpCorsConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/HttpCorsConfigTests.groovy deleted file mode 100644 index f18f142173f..00000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/HttpCorsConfigTests.groovy +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright 2002-2016 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ -package org.springframework.security.config.http - -import org.springframework.beans.factory.BeanCreationException - -import javax.servlet.http.HttpServletResponse - -import org.springframework.http.* -import org.springframework.mock.web.* -import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint -import org.springframework.web.bind.annotation.* -import org.springframework.web.filter.CorsFilter -import org.springframework.web.cors.CorsConfiguration -import org.springframework.web.cors.UrlBasedCorsConfigurationSource - -/** - * - * @author Rob Winch - * @author Tim Ysewyn - */ -class HttpCorsConfigTests extends AbstractHttpConfigTests { - MockHttpServletRequest request - MockHttpServletResponse response - MockFilterChain chain - - def setup() { - request = new MockHttpServletRequest(method:"GET") - response = new MockHttpServletResponse() - chain = new MockFilterChain() - } - - def "No MVC throws meaningful error"() { - when: - xml.http('entry-point-ref' : 'ep') { - 'cors'() - 'intercept-url'(pattern:'/**', access: 'authenticated') - } - bean('ep', Http403ForbiddenEntryPoint) - createAppContext() - then: - BeanCreationException success = thrown() - success.message.contains("Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext") - } - - def "HandlerMappingIntrospector explicit"() { - setup: - xml.http('entry-point-ref' : 'ep') { - 'cors'() - 'intercept-url'(pattern:'/**', access: 'authenticated') - } - bean('ep', Http403ForbiddenEntryPoint) - bean('controller', CorsController) - xml.'mvc:annotation-driven'() - createAppContext() - when: - addCors() - springSecurityFilterChain.doFilter(request,response,chain) - then: 'Ensure we a CORS response w/ Spring Security headers too' - responseHeaders['Access-Control-Allow-Origin'] - responseHeaders['X-Content-Type-Options'] - when: - setup() - addCors(true) - springSecurityFilterChain.doFilter(request,response,chain) - then: 'Ensure we a CORS response w/ Spring Security headers too' - responseHeaders['Access-Control-Allow-Origin'] - responseHeaders['X-Content-Type-Options'] - response.status == HttpServletResponse.SC_OK - } - - def "CorsConfigurationSource"() { - setup: - xml.http('entry-point-ref' : 'ep') { - 'cors'('configuration-source-ref':'ccs') - 'intercept-url'(pattern:'/**', access: 'authenticated') - } - bean('ep', Http403ForbiddenEntryPoint) - bean('ccs', MyCorsConfigurationSource) - createAppContext() - when: - addCors() - springSecurityFilterChain.doFilter(request,response,chain) - then: 'Ensure we a CORS response w/ Spring Security headers too' - responseHeaders['Access-Control-Allow-Origin'] - responseHeaders['X-Content-Type-Options'] - when: - setup() - addCors(true) - springSecurityFilterChain.doFilter(request,response,chain) - then: 'Ensure we a CORS response w/ Spring Security headers too' - responseHeaders['Access-Control-Allow-Origin'] - responseHeaders['X-Content-Type-Options'] - response.status == HttpServletResponse.SC_OK - } - - def "CorsFilter"() { - setup: - xml.http('entry-point-ref' : 'ep') { - 'cors'('ref' : 'cf') - 'intercept-url'(pattern:'/**', access: 'authenticated') - } - xml.'b:bean'(id: 'cf', 'class': CorsFilter.name) { - 'b:constructor-arg'(ref: 'ccs') - } - bean('ep', Http403ForbiddenEntryPoint) - bean('ccs', MyCorsConfigurationSource) - createAppContext() - when: - addCors() - springSecurityFilterChain.doFilter(request,response,chain) - then: 'Ensure we a CORS response w/ Spring Security headers too' - responseHeaders['Access-Control-Allow-Origin'] - responseHeaders['X-Content-Type-Options'] - when: - setup() - addCors(true) - springSecurityFilterChain.doFilter(request,response,chain) - then: 'Ensure we a CORS response w/ Spring Security headers too' - responseHeaders['Access-Control-Allow-Origin'] - responseHeaders['X-Content-Type-Options'] - response.status == HttpServletResponse.SC_OK - } - - def addCors(boolean isPreflight=false) { - request.addHeader(HttpHeaders.ORIGIN,"https://example.com") - if(!isPreflight) { - return - } - request.method = HttpMethod.OPTIONS.name() - request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name()) - } - - def getResponseHeaders() { - def headers = [:] - response.headerNames.each { name -> - headers.put(name, response.getHeaderValues(name).join(',')) - } - return headers - } - - @RestController - @CrossOrigin(methods = [ - RequestMethod.GET, RequestMethod.POST - ]) - static class CorsController { - @RequestMapping("/") - String hello() { - "Hello" - } - } - - static class MyCorsConfigurationSource extends UrlBasedCorsConfigurationSource { - MyCorsConfigurationSource() { - registerCorsConfiguration('/**', new CorsConfiguration(allowedOrigins : ['*'], allowedMethods : [ - RequestMethod.GET.name(), - RequestMethod.POST.name() - ])) - } - } -} diff --git a/config/src/test/java/org/springframework/security/config/http/HttpCorsConfigTests.java b/config/src/test/java/org/springframework/security/config/http/HttpCorsConfigTests.java new file mode 100644 index 00000000000..52c3245a120 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/HttpCorsConfigTests.java @@ -0,0 +1,168 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.http; + +import org.junit.Rule; +import org.junit.Test; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * + * @author Rob Winch + * @author Tim Ysewyn + * @author Josh Cummings + */ +public class HttpCorsConfigTests { + + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/HttpCorsConfigTests"; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + MockMvc mvc; + + @Test + public void autowireWhenMissingMvcThenGivesInformativeError() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("RequiresMvc")).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasMessageContaining("Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext"); + } + + @Test + public void getWhenUsingCorsThenDoesSpringSecurityCorsHandshake() + throws Exception { + + this.spring.configLocations(this.xml("WithCors")).autowire(); + + this.mvc.perform(get("/").with(this.approved())) + .andExpect(corsResponseHeaders()) + .andExpect((status().isIAmATeapot())); + + this.mvc.perform(options("/").with(this.preflight())) + .andExpect(corsResponseHeaders()) + .andExpect(status().isOk()); + } + + @Test + public void getWhenUsingCustomCorsConfigurationSourceThenDoesSpringSecurityCorsHandshake() + throws Exception { + + this.spring.configLocations(this.xml("WithCorsConfigurationSource")).autowire(); + + this.mvc.perform(get("/").with(this.approved())) + .andExpect(corsResponseHeaders()) + .andExpect((status().isIAmATeapot())); + + this.mvc.perform(options("/").with(this.preflight())) + .andExpect(corsResponseHeaders()) + .andExpect(status().isOk()); + } + + @Test + public void getWhenUsingCustomCorsFilterThenDoesSPringSecurityCorsHandshake() + throws Exception { + + this.spring.configLocations(this.xml("WithCorsFilter")).autowire(); + + this.mvc.perform(get("/").with(this.approved())) + .andExpect(corsResponseHeaders()) + .andExpect((status().isIAmATeapot())); + + this.mvc.perform(options("/").with(this.preflight())) + .andExpect(corsResponseHeaders()) + .andExpect(status().isOk()); + } + + @RestController + @CrossOrigin(methods = { + RequestMethod.GET, RequestMethod.POST + }) + static class CorsController { + @RequestMapping("/") + String hello() { + return "Hello"; + } + } + + static class MyCorsConfigurationSource extends UrlBasedCorsConfigurationSource { + MyCorsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList("*")); + configuration.setAllowedMethods(Arrays.asList(RequestMethod.GET.name(), RequestMethod.POST.name())); + + super.registerCorsConfiguration( + "/**", + configuration); + } + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } + + private RequestPostProcessor preflight() { + return cors(true); + } + + private RequestPostProcessor approved() { + return cors(false); + } + + private RequestPostProcessor cors(boolean preflight) { + return (request) -> { + request.addHeader(HttpHeaders.ORIGIN, "https://example.com"); + + if ( preflight ) { + request.setMethod(HttpMethod.OPTIONS.name()); + request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name()); + } + + return request; + }; + } + + private ResultMatcher corsResponseHeaders() { + return result -> { + header().exists("Access-Control-Allow-Origin").match(result); + header().exists("X-Content-Type-Options").match(result); + }; + } + +} diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-RequiresMvc.xml b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-RequiresMvc.xml new file mode 100644 index 00000000000..9a66ab3a5b5 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-RequiresMvc.xml @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCors.xml b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCors.xml new file mode 100644 index 00000000000..ac57e95b45b --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCors.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsConfigurationSource.xml b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsConfigurationSource.xml new file mode 100644 index 00000000000..b73f227634d --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsConfigurationSource.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsFilter.xml b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsFilter.xml new file mode 100644 index 00000000000..28daed34b1f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsFilter.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + From a759195e64997ce86f280fbb832e6e0975eba0aa Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 15 May 2018 08:12:25 -0600 Subject: [PATCH 010/226] PlaceHolderAndELConfigTests groovy->java Issue: gh-4939 --- .../http/PlaceHolderAndELConfigTests.groovy | 156 ------------- .../http/PlaceHolderAndELConfigTests.java | 212 ++++++++++++++++++ ...olderAndELConfigTests-AccessDeniedPage.xml | 37 +++ ...ELConfigTests-AccessDeniedPageWithSpEL.xml | 37 +++ ...ELConfigTests-InterceptUrlAndFormLogin.xml | 42 ++++ ...Tests-InterceptUrlAndFormLoginWithSpEL.xml | 45 ++++ ...laceHolderAndELConfigTests-PortMapping.xml | 41 ++++ ...HolderAndELConfigTests-RequiresChannel.xml | 36 +++ ...olderAndELConfigTests-UnsecuredPattern.xml | 38 ++++ 9 files changed, 488 insertions(+), 156 deletions(-) delete mode 100644 config/src/test/groovy/org/springframework/security/config/http/PlaceHolderAndELConfigTests.groovy create mode 100644 config/src/test/java/org/springframework/security/config/http/PlaceHolderAndELConfigTests.java create mode 100644 config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPage.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPageWithSpEL.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLogin.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLoginWithSpEL.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-PortMapping.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-RequiresChannel.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-UnsecuredPattern.xml diff --git a/config/src/test/groovy/org/springframework/security/config/http/PlaceHolderAndELConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/PlaceHolderAndELConfigTests.groovy deleted file mode 100644 index 13db6f17244..00000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/PlaceHolderAndELConfigTests.groovy +++ /dev/null @@ -1,156 +0,0 @@ -package org.springframework.security.config.http - -import java.text.MessageFormat - -import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer -import org.springframework.mock.web.MockFilterChain -import org.springframework.mock.web.MockHttpServletRequest -import org.springframework.mock.web.MockHttpServletResponse -import org.springframework.security.access.SecurityConfig -import org.springframework.security.config.BeanIds -import org.springframework.security.util.FieldUtils -import org.springframework.security.web.PortMapperImpl -import org.springframework.security.web.access.ExceptionTranslationFilter -import org.springframework.security.web.access.channel.ChannelProcessingFilter -import org.springframework.security.web.access.intercept.FilterSecurityInterceptor -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter - -class PlaceHolderAndELConfigTests extends AbstractHttpConfigTests { - - def setup() { - // Add a PropertyPlaceholderConfigurer to the context for all the tests - bean(PropertyPlaceholderConfigurer.class.name, PropertyPlaceholderConfigurer.class) - } - - def unsecuredPatternSupportsPlaceholderForPattern() { - System.setProperty("pattern.nofilters", "/unprotected"); - - xml.http(pattern: '${pattern.nofilters}', security: 'none') - httpAutoConfig() { - interceptUrl('/**', 'ROLE_A') - } - createAppContext() - - List filters = getFilters("/unprotected"); - - expect: - filters.size() == 0 - } - - // SEC-1201 - def interceptUrlsAndFormLoginSupportPropertyPlaceholders() { - System.setProperty("secure.Url", "/secure"); - System.setProperty("secure.role", "ROLE_A"); - System.setProperty("login.page", "/loginPage"); - System.setProperty("default.target", "/defaultTarget"); - System.setProperty("auth.failure", "/authFailure"); - - xml.http(pattern: '${login.page}', security: 'none') - xml.http('use-expressions':false) { - interceptUrl('${secure.Url}', '${secure.role}') - 'form-login'('login-page':'${login.page}', 'default-target-url': '${default.target}', - 'authentication-failure-url':'${auth.failure}'); - } - createAppContext(); - - expect: - propertyValuesMatchPlaceholders() - getFilters("/loginPage").size() == 0 - } - - // SEC-1309 - def interceptUrlsAndFormLoginSupportEL() { - System.setProperty("secure.url", "/secure"); - System.setProperty("secure.role", "ROLE_A"); - System.setProperty("login.page", "/loginPage"); - System.setProperty("default.target", "/defaultTarget"); - System.setProperty("auth.failure", "/authFailure"); - - xml.http('use-expressions':false) { - interceptUrl("#{systemProperties['secure.url']}", "#{systemProperties['secure.role']}") - 'form-login'('login-page':"#{systemProperties['login.page']}", 'default-target-url': "#{systemProperties['default.target']}", - 'authentication-failure-url':"#{systemProperties['auth.failure']}"); - } - createAppContext() - - expect: - propertyValuesMatchPlaceholders() - } - - private void propertyValuesMatchPlaceholders() { - // Check the security attribute - def fis = getFilter(FilterSecurityInterceptor); - def fids = fis.getSecurityMetadataSource(); - Collection attrs = fids.getAttributes(createFilterinvocation("/secure", null)); - assert attrs.size() == 1 - assert attrs.contains(new SecurityConfig("ROLE_A")) - - // Check the form login properties are set - def apf = getFilter(UsernamePasswordAuthenticationFilter) - assert FieldUtils.getFieldValue(apf, "successHandler.defaultTargetUrl") == '/defaultTarget' - assert "/authFailure" == FieldUtils.getFieldValue(apf, "failureHandler.defaultFailureUrl") - - def etf = getFilter(ExceptionTranslationFilter) - assert "/loginPage"== etf.authenticationEntryPoint.loginFormUrl - } - - def portMappingsWorkWithPlaceholdersAndEL() { - System.setProperty("http", "9080"); - System.setProperty("https", "9443"); - - httpAutoConfig { - 'port-mappings'() { - 'port-mapping'(http: '#{systemProperties.http}', https: '${https}') - } - } - createAppContext(); - - def pm = (appContext.getBeansOfType(PortMapperImpl).values() as List)[0]; - - expect: - pm.getTranslatedPortMappings().size() == 1 - pm.lookupHttpPort(9443) == 9080 - pm.lookupHttpsPort(9080) == 9443 - } - - def requiresChannelSupportsPlaceholder() { - System.setProperty("secure.url", "/secure"); - System.setProperty("required.channel", "https"); - - httpAutoConfig { - 'intercept-url'(pattern: '${secure.url}', 'requires-channel': '${required.channel}') - } - createAppContext(); - List filters = getFilters("/secure"); - - expect: - filters.size() == AUTO_CONFIG_FILTERS + 1 - filters[0] instanceof ChannelProcessingFilter - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setServletPath("/secure"); - MockHttpServletResponse response = new MockHttpServletResponse(); - filters[0].doFilter(request, response, new MockFilterChain()); - response.getRedirectedUrl().startsWith("https") - } - - def accessDeniedPageWorksWithPlaceholders() { - System.setProperty("accessDenied", "/go-away"); - xml.http('auto-config': 'true') { - 'access-denied-handler'('error-page' : '${accessDenied}') {} - } - createAppContext(); - - expect: - FieldUtils.getFieldValue(getFilter(ExceptionTranslationFilter.class), "accessDeniedHandler.errorPage") == '/go-away' - } - - def accessDeniedHandlerPageWorksWithEL() { - httpAutoConfig { - 'access-denied-handler'('error-page': "#{'/go' + '-away'}") - } - createAppContext() - - expect: - getFilter(ExceptionTranslationFilter).accessDeniedHandler.errorPage == '/go-away' - } -} diff --git a/config/src/test/java/org/springframework/security/config/http/PlaceHolderAndELConfigTests.java b/config/src/test/java/org/springframework/security/config/http/PlaceHolderAndELConfigTests.java new file mode 100644 index 00000000000..070cf441b7f --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/PlaceHolderAndELConfigTests.java @@ -0,0 +1,212 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.http; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Josh Cummings + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SecurityTestExecutionListeners +public class PlaceHolderAndELConfigTests { + + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/PlaceHolderAndELConfigTests"; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + MockMvc mvc; + + @Test + public void getWhenUsingPlaceholderThenUnsecuredPatternCorrectlyConfigured() + throws Exception { + + System.setProperty("pattern.nofilters", "/unsecured"); + + this.spring.configLocations(this.xml("UnsecuredPattern")).autowire(); + + this.mvc.perform(get("/unsecured")) + .andExpect(status().isOk()); + } + + /** + * SEC-1201 + */ + @Test + public void loginWhenUsingPlaceholderThenInterceptUrlsAndFormLoginWorks() + throws Exception { + + System.setProperty("secure.Url", "/secured"); + System.setProperty("secure.role", "ROLE_NUNYA"); + System.setProperty("login.page", "/loginPage"); + System.setProperty("default.target", "/defaultTarget"); + System.setProperty("auth.failure", "/authFailure"); + + this.spring.configLocations(this.xml("InterceptUrlAndFormLogin")).autowire(); + + // login-page setting + + this.mvc.perform(get("/secured")) + .andExpect(redirectedUrl("http://localhost/loginPage")); + + // login-processing-url setting + // default-target-url setting + + this.mvc.perform(post("/loginPage") + .param("username", "user") + .param("password", "password")) + .andExpect(redirectedUrl("/defaultTarget")); + + // authentication-failure-url setting + + this.mvc.perform(post("/loginPage") + .param("username", "user") + .param("password", "wrong")) + .andExpect(redirectedUrl("/authFailure")); + } + + /** + * SEC-1309 + */ + @Test + public void loginWhenUsingSpELThenInterceptUrlsAndFormLoginWorks() + throws Exception { + + System.setProperty("secure.url", "/secured"); + System.setProperty("secure.role", "ROLE_NUNYA"); + System.setProperty("login.page", "/loginPage"); + System.setProperty("default.target", "/defaultTarget"); + System.setProperty("auth.failure", "/authFailure"); + + this.spring.configLocations( + this.xml("InterceptUrlAndFormLoginWithSpEL")).autowire(); + + // login-page setting + + this.mvc.perform(get("/secured")) + .andExpect(redirectedUrl("http://localhost/loginPage")); + + // login-processing-url setting + // default-target-url setting + + this.mvc.perform(post("/loginPage") + .param("username", "user") + .param("password", "password")) + .andExpect(redirectedUrl("/defaultTarget")); + + // authentication-failure-url setting + + this.mvc.perform(post("/loginPage") + .param("username", "user") + .param("password", "wrong")) + .andExpect(redirectedUrl("/authFailure")); + + } + + @Test + @WithMockUser + public void requestWhenUsingPlaceholderOrSpELThenPortMapperWorks() + throws Exception { + + System.setProperty("http", "9080"); + System.setProperty("https", "9443"); + + this.spring.configLocations(this.xml("PortMapping")).autowire(); + + this.mvc.perform(get("http://localhost:9080/secured")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("https://localhost:9443/secured")); + + this.mvc.perform(get("https://localhost:9443/unsecured")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost:9080/unsecured")); + } + + @Test + @WithMockUser + public void requestWhenUsingPlaceholderThenRequiresChannelWorks() + throws Exception { + + System.setProperty("secure.url", "/secured"); + System.setProperty("required.channel", "https"); + + this.spring.configLocations(this.xml("RequiresChannel")).autowire(); + + this.mvc.perform(get("http://localhost/secured")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("https://localhost/secured")); + } + + @Test + @WithMockUser + public void requestWhenUsingPlaceholderThenAccessDeniedPageWorks() + throws Exception { + + System.setProperty("accessDenied", "/go-away"); + + this.spring.configLocations(this.xml("AccessDeniedPage")).autowire(); + + this.mvc.perform(get("/secured")) + .andExpect(forwardedUrl("/go-away")); + } + + @Test + @WithMockUser + public void requestWhenUsingSpELThenAccessDeniedPageWorks() + throws Exception { + + this.spring.configLocations(this.xml("AccessDeniedPageWithSpEL")).autowire(); + + this.mvc.perform(get("/secured")) + .andExpect(forwardedUrl("/go-away")); + } + + @RestController + static class SimpleController { + @GetMapping("/unsecured") + String unsecured() { + return "unsecured"; + } + + @GetMapping("/secured") + String secured() { + return "secured"; + } + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPage.xml b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPage.xml new file mode 100644 index 00000000000..04f10d3d008 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPage.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPageWithSpEL.xml b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPageWithSpEL.xml new file mode 100644 index 00000000000..d4f67486e0d --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPageWithSpEL.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLogin.xml b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLogin.xml new file mode 100644 index 00000000000..be48c5b85d0 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLogin.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLoginWithSpEL.xml b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLoginWithSpEL.xml new file mode 100644 index 00000000000..93e955f7561 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLoginWithSpEL.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-PortMapping.xml b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-PortMapping.xml new file mode 100644 index 00000000000..c135a75b08b --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-PortMapping.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-RequiresChannel.xml b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-RequiresChannel.xml new file mode 100644 index 00000000000..1d9c90488d6 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-RequiresChannel.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-UnsecuredPattern.xml b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-UnsecuredPattern.xml new file mode 100644 index 00000000000..139a459c764 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-UnsecuredPattern.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + From 49f86bce1b4d7e70575a772fbf64b24c86ef05ab Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 15 May 2018 17:05:31 -0500 Subject: [PATCH 011/226] Update to spring-build-conventions:0.0.17.RELEASE Fixes: gh-5352 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 53de5b415a0..40fed3c1e7b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { dependencies { - classpath 'io.spring.gradle:spring-build-conventions:0.0.15.RELEASE' + classpath 'io.spring.gradle:spring-build-conventions:0.0.16.RELEASE' classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion" } repositories { From 9791b8f952b83cd345e33e2c1c135b2cf70fbd96 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 16 May 2018 12:28:31 -0500 Subject: [PATCH 012/226] Add ClientRegistration from OpenID Connect Discovery Fixes: gh-4413 --- config/spring-security-config.gradle | 1 + .../client/OidcConfigurationProvider.java | 118 ++++++++++ .../OidcConfigurationProviderTests.java | 209 ++++++++++++++++++ .../registration/ClientRegistration.java | 15 ++ 4 files changed, 343 insertions(+) create mode 100644 config/src/main/java/org/springframework/security/config/oauth2/client/OidcConfigurationProvider.java create mode 100644 config/src/test/java/org/springframework/security/config/oauth2/client/OidcConfigurationProviderTests.java diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index fc56abf254f..e59f23fb099 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -34,6 +34,7 @@ dependencies { testCompile apachedsDependencies testCompile powerMock2Dependencies testCompile spockDependencies + testCompile 'com.squareup.okhttp3:mockwebserver' testCompile 'ch.qos.logback:logback-classic' testCompile 'io.projectreactor.ipc:reactor-netty' testCompile 'javax.annotation:jsr250-api:1.0' diff --git a/config/src/main/java/org/springframework/security/config/oauth2/client/OidcConfigurationProvider.java b/config/src/main/java/org/springframework/security/config/oauth2/client/OidcConfigurationProvider.java new file mode 100644 index 00000000000..77984669c56 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/oauth2/client/OidcConfigurationProvider.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.oauth2.client; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; + +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; +import org.springframework.web.client.RestTemplate; + +import com.nimbusds.oauth2.sdk.GrantType; +import com.nimbusds.oauth2.sdk.ParseException; +import com.nimbusds.oauth2.sdk.Scope; +import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; + +/** + * Allows creating a {@link ClientRegistration.Builder} from an + * OpenID Provider Configuration. + * + * @author Rob Winch + * @since 5.1 + */ +public final class OidcConfigurationProvider { + + /** + * Given the Issuer creates a + * {@link ClientRegistration.Builder} by making an + * OpenID Provider + * Configuration Request and using the values in the + * OpenID + * Provider Configuration Response to initialize the {@link ClientRegistration.Builder}. + * + *

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

+ * + *

+ * Example usage: + *

+ *
+	 * ClientRegistration registration = OidcConfigurationProvider.issuer("https://example.com")
+	 *     .clientId("client-id")
+	 *     .clientSecret("client-secret")
+	 *     .build();
+	 * 
+ * @param issuer the Issuer + * @return a {@link ClientRegistration.Builder} that was initialized by the OpenID Provider Configuration. + */ + public static ClientRegistration.Builder issuer(String issuer) { + RestTemplate rest = new RestTemplate(); + String openidConfiguration = rest.getForObject(issuer + "/.well-known/openid-configuration", String.class); + OIDCProviderMetadata metadata = parse(openidConfiguration); + String name = URI.create(issuer).getHost(); + List metadataAuthMethods = metadata.getTokenEndpointAuthMethods(); + // if null, the default includes client_secret_basic + if (metadataAuthMethods != null && !metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) { + throw new IllegalArgumentException("Only ClientAuthenticationMethod.BASIC is supported. The issuer \"" + issuer + "\" returned a configuration of " + metadataAuthMethods); + } + List grantTypes = metadata.getGrantTypes(); + // If null, the default includes authorization_code + if (grantTypes != null && !grantTypes.contains(GrantType.AUTHORIZATION_CODE)) { + throw new IllegalArgumentException("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \"" + issuer + "\" returned a configuration of " + grantTypes); + } + List scopes = getScopes(metadata); + return ClientRegistration.withRegistrationId(name) + .userNameAttributeName(IdTokenClaimNames.SUB) + .scope(scopes) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}") + .authorizationUri(metadata.getAuthorizationEndpointURI().toASCIIString()) + .jwkSetUri(metadata.getJWKSetURI().toASCIIString()) + .userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString()) + .tokenUri(metadata.getTokenEndpointURI().toASCIIString()) + .clientName(issuer); + } + + private static List getScopes(OIDCProviderMetadata metadata) { + Scope scope = metadata.getScopes(); + if (scope == null) { + // If null, default to "openid" which must be supported + return Arrays.asList("openid"); + } else { + return scope.toStringList(); + } + } + + private static OIDCProviderMetadata parse(String body) { + try { + return OIDCProviderMetadata.parse(body); + } + catch (ParseException e) { + throw new RuntimeException(e); + } + } + + private OidcConfigurationProvider() {} +} diff --git a/config/src/test/java/org/springframework/security/config/oauth2/client/OidcConfigurationProviderTests.java b/config/src/test/java/org/springframework/security/config/oauth2/client/OidcConfigurationProviderTests.java new file mode 100644 index 00000000000..bd967c58ed5 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/oauth2/client/OidcConfigurationProviderTests.java @@ -0,0 +1,209 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.oauth2.client; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; + +import java.util.Arrays; +import java.util.Map; + +import static org.assertj.core.api.Assertions.*; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class OidcConfigurationProviderTests { + + /** + * Contains all optional parameters that are found in ClientRegistration + */ + private static final String DEFAULT_RESPONSE = + "{\n" + + " \"authorization_endpoint\": \"https://example.com/o/oauth2/v2/auth\", \n" + + " \"claims_supported\": [\n" + + " \"aud\", \n" + + " \"email\", \n" + + " \"email_verified\", \n" + + " \"exp\", \n" + + " \"family_name\", \n" + + " \"given_name\", \n" + + " \"iat\", \n" + + " \"iss\", \n" + + " \"locale\", \n" + + " \"name\", \n" + + " \"picture\", \n" + + " \"sub\"\n" + + " ], \n" + + " \"code_challenge_methods_supported\": [\n" + + " \"plain\", \n" + + " \"S256\"\n" + + " ], \n" + + " \"id_token_signing_alg_values_supported\": [\n" + + " \"RS256\"\n" + + " ], \n" + + " \"issuer\": \"https://example.com\", \n" + + " \"jwks_uri\": \"https://example.com/oauth2/v3/certs\", \n" + + " \"response_types_supported\": [\n" + + " \"code\", \n" + + " \"token\", \n" + + " \"id_token\", \n" + + " \"code token\", \n" + + " \"code id_token\", \n" + + " \"token id_token\", \n" + + " \"code token id_token\", \n" + + " \"none\"\n" + + " ], \n" + + " \"revocation_endpoint\": \"https://example.com/o/oauth2/revoke\", \n" + + " \"scopes_supported\": [\n" + + " \"openid\", \n" + + " \"email\", \n" + + " \"profile\"\n" + + " ], \n" + + " \"subject_types_supported\": [\n" + + " \"public\"\n" + + " ], \n" + + " \"grant_types_supported\" : [\"authorization_code\"], \n" + + " \"token_endpoint\": \"https://example.com/oauth2/v4/token\", \n" + + " \"token_endpoint_auth_methods_supported\": [\n" + + " \"client_secret_post\", \n" + + " \"client_secret_basic\"\n" + + " ], \n" + + " \"userinfo_endpoint\": \"https://example.com/oauth2/v3/userinfo\"\n" + + "}"; + + private MockWebServer server; + + private ObjectMapper mapper = new ObjectMapper(); + + private Map response; + + private String issuer; + + @Before + public void setup() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + this.response = this.mapper.readValue(DEFAULT_RESPONSE, new TypeReference>(){}); + } + + @After + public void cleanup() throws Exception { + this.server.shutdown(); + } + + @Test + public void issuerWhenAllInformationThenSuccess() throws Exception { + ClientRegistration registration = registration(""); + ClientRegistration.ProviderDetails provider = registration.getProviderDetails(); + + assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); + assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + assertThat(registration.getRegistrationId()).isEqualTo(this.server.getHostName()); + assertThat(registration.getClientName()).isEqualTo(this.issuer); + assertThat(registration.getScopes()).containsOnly("openid", "email", "profile"); + assertThat(provider.getAuthorizationUri()).isEqualTo("https://example.com/o/oauth2/v2/auth"); + assertThat(provider.getTokenUri()).isEqualTo("https://example.com/oauth2/v4/token"); + assertThat(provider.getJwkSetUri()).isEqualTo("https://example.com/oauth2/v3/certs"); + assertThat(provider.getUserInfoEndpoint().getUri()).isEqualTo("https://example.com/oauth2/v3/userinfo"); + } + + /** + * https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + * + * RECOMMENDED. JSON array containing a list of the OAuth 2.0 [RFC6749] scope values that this server supports. The + * server MUST support the openid scope value. + * @throws Exception + */ + @Test + public void issuerWhenScopesNullThenScopesDefaulted() throws Exception { + this.response.remove("scopes_supported"); + + ClientRegistration registration = registration(""); + + assertThat(registration.getScopes()).containsOnly("openid"); + } + + @Test + public void issuerWhenGrantTypesSupportedNullThenDefaulted() throws Exception { + this.response.remove("grant_types_supported"); + + ClientRegistration registration = registration(""); + + assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + } + + /** + * We currently only support authorization_code, so verify we have a meaningful error until we add support. + * @throws Exception + */ + @Test + public void issuerWhenGrantTypesSupportedInvalidThenException() throws Exception { + this.response.put("grant_types_supported", Arrays.asList("implicit")); + + assertThatThrownBy(() -> registration("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \"" + this.issuer + "\" returned a configuration of [implicit]"); + } + + @Test + public void issuerWhenTokenEndpointAuthMethodsNullThenDefaulted() throws Exception { + this.response.remove("token_endpoint_auth_methods_supported"); + + ClientRegistration registration = registration(""); + + assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); + } + + /** + * We currently only support client_secret_basic, so verify we have a meaningful error until we add support. + * @throws Exception + */ + @Test + public void issuerWhenTokenEndpointAuthMethodsInvalidThenException() throws Exception { + this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_post")); + + assertThatThrownBy(() -> registration("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Only ClientAuthenticationMethod.BASIC is supported. The issuer \"" + this.issuer + "\" returned a configuration of [client_secret_post]"); + } + + private ClientRegistration registration(String path) throws Exception { + String body = this.mapper.writeValueAsString(this.response); + MockResponse mockResponse = new MockResponse() + .setBody(body) + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + this.server.enqueue(mockResponse); + this.issuer = this.server.url(path).toString(); + + return OidcConfigurationProvider.issuer(this.issuer) + .clientId("client-id") + .clientSecret("client-secret") + .build(); + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java index 54e730717ed..080dde9d692 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java @@ -21,6 +21,7 @@ import org.springframework.util.Assert; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; @@ -324,6 +325,20 @@ public Builder scope(String... scope) { return this; } + /** + * Sets the scope(s) used for the client. + * + * @param scope the scope(s) used for the client + * @return the {@link Builder} + */ + public Builder scope(Collection scope) { + if (scope != null && !scope.isEmpty()) { + this.scopes = Collections.unmodifiableSet( + new LinkedHashSet<>(scope)); + } + return this; + } + /** * Sets the uri for the authorization endpoint. * From bb38e05de1ebee89293234cfd838f3e948d671d1 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 18 May 2018 08:50:08 -0500 Subject: [PATCH 013/226] Update to Gradle 4.7 --- gradle/wrapper/gradle-wrapper.jar | Bin 54329 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a5fe1cb94b9ee5ce57e6113458225bcba12d83e3..f6b961fd5a86aa5fbfe90f707c3138408be7c718 100644 GIT binary patch delta 63 zcmdnFf_di(<_YF38otMVOtg+<`&jg`h>>A})W$9U4stOw1bDM^ytJG0dGek^{$Mqm P`48U{U;zt#yzBu0axWc6 delta 63 zcmdnFf_di(<_YF3Kg`qiPqdC?`&9I?h>>A})W$9U4sx+F1bDM^1n1Aqn!M+bKUmFX P{=@eKSinLbFM9w0VoDpr diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ea720f986f3..16d28051c9c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip From 33310c6f70ec39bcc4e4f7f048d6b9f97c34d910 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 18 May 2018 09:20:51 -0500 Subject: [PATCH 014/226] Revert "Add ClientRegistration from OpenID Connect Discovery" This reverts commit 0598d4773257d96ed323f98cbc7e78b55dfd516c. --- config/spring-security-config.gradle | 1 - .../client/OidcConfigurationProvider.java | 118 ---------- .../OidcConfigurationProviderTests.java | 209 ------------------ .../registration/ClientRegistration.java | 15 -- 4 files changed, 343 deletions(-) delete mode 100644 config/src/main/java/org/springframework/security/config/oauth2/client/OidcConfigurationProvider.java delete mode 100644 config/src/test/java/org/springframework/security/config/oauth2/client/OidcConfigurationProviderTests.java diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index e59f23fb099..fc56abf254f 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -34,7 +34,6 @@ dependencies { testCompile apachedsDependencies testCompile powerMock2Dependencies testCompile spockDependencies - testCompile 'com.squareup.okhttp3:mockwebserver' testCompile 'ch.qos.logback:logback-classic' testCompile 'io.projectreactor.ipc:reactor-netty' testCompile 'javax.annotation:jsr250-api:1.0' diff --git a/config/src/main/java/org/springframework/security/config/oauth2/client/OidcConfigurationProvider.java b/config/src/main/java/org/springframework/security/config/oauth2/client/OidcConfigurationProvider.java deleted file mode 100644 index 77984669c56..00000000000 --- a/config/src/main/java/org/springframework/security/config/oauth2/client/OidcConfigurationProvider.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2002-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.oauth2.client; - -import java.net.URI; -import java.util.Arrays; -import java.util.List; - -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.core.ClientAuthenticationMethod; -import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; -import org.springframework.web.client.RestTemplate; - -import com.nimbusds.oauth2.sdk.GrantType; -import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.Scope; -import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; - -/** - * Allows creating a {@link ClientRegistration.Builder} from an - * OpenID Provider Configuration. - * - * @author Rob Winch - * @since 5.1 - */ -public final class OidcConfigurationProvider { - - /** - * Given the Issuer creates a - * {@link ClientRegistration.Builder} by making an - * OpenID Provider - * Configuration Request and using the values in the - * OpenID - * Provider Configuration Response to initialize the {@link ClientRegistration.Builder}. - * - *

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

- * - *

- * Example usage: - *

- *
-	 * ClientRegistration registration = OidcConfigurationProvider.issuer("https://example.com")
-	 *     .clientId("client-id")
-	 *     .clientSecret("client-secret")
-	 *     .build();
-	 * 
- * @param issuer the Issuer - * @return a {@link ClientRegistration.Builder} that was initialized by the OpenID Provider Configuration. - */ - public static ClientRegistration.Builder issuer(String issuer) { - RestTemplate rest = new RestTemplate(); - String openidConfiguration = rest.getForObject(issuer + "/.well-known/openid-configuration", String.class); - OIDCProviderMetadata metadata = parse(openidConfiguration); - String name = URI.create(issuer).getHost(); - List metadataAuthMethods = metadata.getTokenEndpointAuthMethods(); - // if null, the default includes client_secret_basic - if (metadataAuthMethods != null && !metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) { - throw new IllegalArgumentException("Only ClientAuthenticationMethod.BASIC is supported. The issuer \"" + issuer + "\" returned a configuration of " + metadataAuthMethods); - } - List grantTypes = metadata.getGrantTypes(); - // If null, the default includes authorization_code - if (grantTypes != null && !grantTypes.contains(GrantType.AUTHORIZATION_CODE)) { - throw new IllegalArgumentException("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \"" + issuer + "\" returned a configuration of " + grantTypes); - } - List scopes = getScopes(metadata); - return ClientRegistration.withRegistrationId(name) - .userNameAttributeName(IdTokenClaimNames.SUB) - .scope(scopes) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) - .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}") - .authorizationUri(metadata.getAuthorizationEndpointURI().toASCIIString()) - .jwkSetUri(metadata.getJWKSetURI().toASCIIString()) - .userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString()) - .tokenUri(metadata.getTokenEndpointURI().toASCIIString()) - .clientName(issuer); - } - - private static List getScopes(OIDCProviderMetadata metadata) { - Scope scope = metadata.getScopes(); - if (scope == null) { - // If null, default to "openid" which must be supported - return Arrays.asList("openid"); - } else { - return scope.toStringList(); - } - } - - private static OIDCProviderMetadata parse(String body) { - try { - return OIDCProviderMetadata.parse(body); - } - catch (ParseException e) { - throw new RuntimeException(e); - } - } - - private OidcConfigurationProvider() {} -} diff --git a/config/src/test/java/org/springframework/security/config/oauth2/client/OidcConfigurationProviderTests.java b/config/src/test/java/org/springframework/security/config/oauth2/client/OidcConfigurationProviderTests.java deleted file mode 100644 index bd967c58ed5..00000000000 --- a/config/src/test/java/org/springframework/security/config/oauth2/client/OidcConfigurationProviderTests.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright 2002-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.oauth2.client; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.core.ClientAuthenticationMethod; - -import java.util.Arrays; -import java.util.Map; - -import static org.assertj.core.api.Assertions.*; - -/** - * @author Rob Winch - * @since 5.1 - */ -public class OidcConfigurationProviderTests { - - /** - * Contains all optional parameters that are found in ClientRegistration - */ - private static final String DEFAULT_RESPONSE = - "{\n" - + " \"authorization_endpoint\": \"https://example.com/o/oauth2/v2/auth\", \n" - + " \"claims_supported\": [\n" - + " \"aud\", \n" - + " \"email\", \n" - + " \"email_verified\", \n" - + " \"exp\", \n" - + " \"family_name\", \n" - + " \"given_name\", \n" - + " \"iat\", \n" - + " \"iss\", \n" - + " \"locale\", \n" - + " \"name\", \n" - + " \"picture\", \n" - + " \"sub\"\n" - + " ], \n" - + " \"code_challenge_methods_supported\": [\n" - + " \"plain\", \n" - + " \"S256\"\n" - + " ], \n" - + " \"id_token_signing_alg_values_supported\": [\n" - + " \"RS256\"\n" - + " ], \n" - + " \"issuer\": \"https://example.com\", \n" - + " \"jwks_uri\": \"https://example.com/oauth2/v3/certs\", \n" - + " \"response_types_supported\": [\n" - + " \"code\", \n" - + " \"token\", \n" - + " \"id_token\", \n" - + " \"code token\", \n" - + " \"code id_token\", \n" - + " \"token id_token\", \n" - + " \"code token id_token\", \n" - + " \"none\"\n" - + " ], \n" - + " \"revocation_endpoint\": \"https://example.com/o/oauth2/revoke\", \n" - + " \"scopes_supported\": [\n" - + " \"openid\", \n" - + " \"email\", \n" - + " \"profile\"\n" - + " ], \n" - + " \"subject_types_supported\": [\n" - + " \"public\"\n" - + " ], \n" - + " \"grant_types_supported\" : [\"authorization_code\"], \n" - + " \"token_endpoint\": \"https://example.com/oauth2/v4/token\", \n" - + " \"token_endpoint_auth_methods_supported\": [\n" - + " \"client_secret_post\", \n" - + " \"client_secret_basic\"\n" - + " ], \n" - + " \"userinfo_endpoint\": \"https://example.com/oauth2/v3/userinfo\"\n" - + "}"; - - private MockWebServer server; - - private ObjectMapper mapper = new ObjectMapper(); - - private Map response; - - private String issuer; - - @Before - public void setup() throws Exception { - this.server = new MockWebServer(); - this.server.start(); - this.response = this.mapper.readValue(DEFAULT_RESPONSE, new TypeReference>(){}); - } - - @After - public void cleanup() throws Exception { - this.server.shutdown(); - } - - @Test - public void issuerWhenAllInformationThenSuccess() throws Exception { - ClientRegistration registration = registration(""); - ClientRegistration.ProviderDetails provider = registration.getProviderDetails(); - - assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); - assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); - assertThat(registration.getRegistrationId()).isEqualTo(this.server.getHostName()); - assertThat(registration.getClientName()).isEqualTo(this.issuer); - assertThat(registration.getScopes()).containsOnly("openid", "email", "profile"); - assertThat(provider.getAuthorizationUri()).isEqualTo("https://example.com/o/oauth2/v2/auth"); - assertThat(provider.getTokenUri()).isEqualTo("https://example.com/oauth2/v4/token"); - assertThat(provider.getJwkSetUri()).isEqualTo("https://example.com/oauth2/v3/certs"); - assertThat(provider.getUserInfoEndpoint().getUri()).isEqualTo("https://example.com/oauth2/v3/userinfo"); - } - - /** - * https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata - * - * RECOMMENDED. JSON array containing a list of the OAuth 2.0 [RFC6749] scope values that this server supports. The - * server MUST support the openid scope value. - * @throws Exception - */ - @Test - public void issuerWhenScopesNullThenScopesDefaulted() throws Exception { - this.response.remove("scopes_supported"); - - ClientRegistration registration = registration(""); - - assertThat(registration.getScopes()).containsOnly("openid"); - } - - @Test - public void issuerWhenGrantTypesSupportedNullThenDefaulted() throws Exception { - this.response.remove("grant_types_supported"); - - ClientRegistration registration = registration(""); - - assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); - } - - /** - * We currently only support authorization_code, so verify we have a meaningful error until we add support. - * @throws Exception - */ - @Test - public void issuerWhenGrantTypesSupportedInvalidThenException() throws Exception { - this.response.put("grant_types_supported", Arrays.asList("implicit")); - - assertThatThrownBy(() -> registration("")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \"" + this.issuer + "\" returned a configuration of [implicit]"); - } - - @Test - public void issuerWhenTokenEndpointAuthMethodsNullThenDefaulted() throws Exception { - this.response.remove("token_endpoint_auth_methods_supported"); - - ClientRegistration registration = registration(""); - - assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); - } - - /** - * We currently only support client_secret_basic, so verify we have a meaningful error until we add support. - * @throws Exception - */ - @Test - public void issuerWhenTokenEndpointAuthMethodsInvalidThenException() throws Exception { - this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_post")); - - assertThatThrownBy(() -> registration("")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Only ClientAuthenticationMethod.BASIC is supported. The issuer \"" + this.issuer + "\" returned a configuration of [client_secret_post]"); - } - - private ClientRegistration registration(String path) throws Exception { - String body = this.mapper.writeValueAsString(this.response); - MockResponse mockResponse = new MockResponse() - .setBody(body) - .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); - this.server.enqueue(mockResponse); - this.issuer = this.server.url(path).toString(); - - return OidcConfigurationProvider.issuer(this.issuer) - .clientId("client-id") - .clientSecret("client-secret") - .build(); - } -} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java index 080dde9d692..54e730717ed 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java @@ -21,7 +21,6 @@ import org.springframework.util.Assert; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; @@ -325,20 +324,6 @@ public Builder scope(String... scope) { return this; } - /** - * Sets the scope(s) used for the client. - * - * @param scope the scope(s) used for the client - * @return the {@link Builder} - */ - public Builder scope(Collection scope) { - if (scope != null && !scope.isEmpty()) { - this.scopes = Collections.unmodifiableSet( - new LinkedHashSet<>(scope)); - } - return this; - } - /** * Sets the uri for the authorization endpoint. * From a1e40650a138556391872f6bd042e10a56272ca3 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 18 May 2018 09:40:43 -0500 Subject: [PATCH 015/226] Revert "Revert "Add ClientRegistration from OpenID Connect Discovery"" This reverts commit 9fe0f50e3ced98357bfaceee88c4539f03d11e45. The original commit was accidentally pushed prior to PR. We attempted to revert the commit hoping the PR would open again. This did not work. We are going to do a Polish commit instead. Issue: gh-5355 --- config/spring-security-config.gradle | 1 + .../client/OidcConfigurationProvider.java | 118 ++++++++++ .../OidcConfigurationProviderTests.java | 209 ++++++++++++++++++ .../registration/ClientRegistration.java | 15 ++ 4 files changed, 343 insertions(+) create mode 100644 config/src/main/java/org/springframework/security/config/oauth2/client/OidcConfigurationProvider.java create mode 100644 config/src/test/java/org/springframework/security/config/oauth2/client/OidcConfigurationProviderTests.java diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index fc56abf254f..e59f23fb099 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -34,6 +34,7 @@ dependencies { testCompile apachedsDependencies testCompile powerMock2Dependencies testCompile spockDependencies + testCompile 'com.squareup.okhttp3:mockwebserver' testCompile 'ch.qos.logback:logback-classic' testCompile 'io.projectreactor.ipc:reactor-netty' testCompile 'javax.annotation:jsr250-api:1.0' diff --git a/config/src/main/java/org/springframework/security/config/oauth2/client/OidcConfigurationProvider.java b/config/src/main/java/org/springframework/security/config/oauth2/client/OidcConfigurationProvider.java new file mode 100644 index 00000000000..77984669c56 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/oauth2/client/OidcConfigurationProvider.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.oauth2.client; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; + +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; +import org.springframework.web.client.RestTemplate; + +import com.nimbusds.oauth2.sdk.GrantType; +import com.nimbusds.oauth2.sdk.ParseException; +import com.nimbusds.oauth2.sdk.Scope; +import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; + +/** + * Allows creating a {@link ClientRegistration.Builder} from an + * OpenID Provider Configuration. + * + * @author Rob Winch + * @since 5.1 + */ +public final class OidcConfigurationProvider { + + /** + * Given the Issuer creates a + * {@link ClientRegistration.Builder} by making an + * OpenID Provider + * Configuration Request and using the values in the + * OpenID + * Provider Configuration Response to initialize the {@link ClientRegistration.Builder}. + * + *

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

+ * + *

+ * Example usage: + *

+ *
+	 * ClientRegistration registration = OidcConfigurationProvider.issuer("https://example.com")
+	 *     .clientId("client-id")
+	 *     .clientSecret("client-secret")
+	 *     .build();
+	 * 
+ * @param issuer the Issuer + * @return a {@link ClientRegistration.Builder} that was initialized by the OpenID Provider Configuration. + */ + public static ClientRegistration.Builder issuer(String issuer) { + RestTemplate rest = new RestTemplate(); + String openidConfiguration = rest.getForObject(issuer + "/.well-known/openid-configuration", String.class); + OIDCProviderMetadata metadata = parse(openidConfiguration); + String name = URI.create(issuer).getHost(); + List metadataAuthMethods = metadata.getTokenEndpointAuthMethods(); + // if null, the default includes client_secret_basic + if (metadataAuthMethods != null && !metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) { + throw new IllegalArgumentException("Only ClientAuthenticationMethod.BASIC is supported. The issuer \"" + issuer + "\" returned a configuration of " + metadataAuthMethods); + } + List grantTypes = metadata.getGrantTypes(); + // If null, the default includes authorization_code + if (grantTypes != null && !grantTypes.contains(GrantType.AUTHORIZATION_CODE)) { + throw new IllegalArgumentException("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \"" + issuer + "\" returned a configuration of " + grantTypes); + } + List scopes = getScopes(metadata); + return ClientRegistration.withRegistrationId(name) + .userNameAttributeName(IdTokenClaimNames.SUB) + .scope(scopes) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}") + .authorizationUri(metadata.getAuthorizationEndpointURI().toASCIIString()) + .jwkSetUri(metadata.getJWKSetURI().toASCIIString()) + .userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString()) + .tokenUri(metadata.getTokenEndpointURI().toASCIIString()) + .clientName(issuer); + } + + private static List getScopes(OIDCProviderMetadata metadata) { + Scope scope = metadata.getScopes(); + if (scope == null) { + // If null, default to "openid" which must be supported + return Arrays.asList("openid"); + } else { + return scope.toStringList(); + } + } + + private static OIDCProviderMetadata parse(String body) { + try { + return OIDCProviderMetadata.parse(body); + } + catch (ParseException e) { + throw new RuntimeException(e); + } + } + + private OidcConfigurationProvider() {} +} diff --git a/config/src/test/java/org/springframework/security/config/oauth2/client/OidcConfigurationProviderTests.java b/config/src/test/java/org/springframework/security/config/oauth2/client/OidcConfigurationProviderTests.java new file mode 100644 index 00000000000..bd967c58ed5 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/oauth2/client/OidcConfigurationProviderTests.java @@ -0,0 +1,209 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.oauth2.client; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; + +import java.util.Arrays; +import java.util.Map; + +import static org.assertj.core.api.Assertions.*; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class OidcConfigurationProviderTests { + + /** + * Contains all optional parameters that are found in ClientRegistration + */ + private static final String DEFAULT_RESPONSE = + "{\n" + + " \"authorization_endpoint\": \"https://example.com/o/oauth2/v2/auth\", \n" + + " \"claims_supported\": [\n" + + " \"aud\", \n" + + " \"email\", \n" + + " \"email_verified\", \n" + + " \"exp\", \n" + + " \"family_name\", \n" + + " \"given_name\", \n" + + " \"iat\", \n" + + " \"iss\", \n" + + " \"locale\", \n" + + " \"name\", \n" + + " \"picture\", \n" + + " \"sub\"\n" + + " ], \n" + + " \"code_challenge_methods_supported\": [\n" + + " \"plain\", \n" + + " \"S256\"\n" + + " ], \n" + + " \"id_token_signing_alg_values_supported\": [\n" + + " \"RS256\"\n" + + " ], \n" + + " \"issuer\": \"https://example.com\", \n" + + " \"jwks_uri\": \"https://example.com/oauth2/v3/certs\", \n" + + " \"response_types_supported\": [\n" + + " \"code\", \n" + + " \"token\", \n" + + " \"id_token\", \n" + + " \"code token\", \n" + + " \"code id_token\", \n" + + " \"token id_token\", \n" + + " \"code token id_token\", \n" + + " \"none\"\n" + + " ], \n" + + " \"revocation_endpoint\": \"https://example.com/o/oauth2/revoke\", \n" + + " \"scopes_supported\": [\n" + + " \"openid\", \n" + + " \"email\", \n" + + " \"profile\"\n" + + " ], \n" + + " \"subject_types_supported\": [\n" + + " \"public\"\n" + + " ], \n" + + " \"grant_types_supported\" : [\"authorization_code\"], \n" + + " \"token_endpoint\": \"https://example.com/oauth2/v4/token\", \n" + + " \"token_endpoint_auth_methods_supported\": [\n" + + " \"client_secret_post\", \n" + + " \"client_secret_basic\"\n" + + " ], \n" + + " \"userinfo_endpoint\": \"https://example.com/oauth2/v3/userinfo\"\n" + + "}"; + + private MockWebServer server; + + private ObjectMapper mapper = new ObjectMapper(); + + private Map response; + + private String issuer; + + @Before + public void setup() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + this.response = this.mapper.readValue(DEFAULT_RESPONSE, new TypeReference>(){}); + } + + @After + public void cleanup() throws Exception { + this.server.shutdown(); + } + + @Test + public void issuerWhenAllInformationThenSuccess() throws Exception { + ClientRegistration registration = registration(""); + ClientRegistration.ProviderDetails provider = registration.getProviderDetails(); + + assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); + assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + assertThat(registration.getRegistrationId()).isEqualTo(this.server.getHostName()); + assertThat(registration.getClientName()).isEqualTo(this.issuer); + assertThat(registration.getScopes()).containsOnly("openid", "email", "profile"); + assertThat(provider.getAuthorizationUri()).isEqualTo("https://example.com/o/oauth2/v2/auth"); + assertThat(provider.getTokenUri()).isEqualTo("https://example.com/oauth2/v4/token"); + assertThat(provider.getJwkSetUri()).isEqualTo("https://example.com/oauth2/v3/certs"); + assertThat(provider.getUserInfoEndpoint().getUri()).isEqualTo("https://example.com/oauth2/v3/userinfo"); + } + + /** + * https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + * + * RECOMMENDED. JSON array containing a list of the OAuth 2.0 [RFC6749] scope values that this server supports. The + * server MUST support the openid scope value. + * @throws Exception + */ + @Test + public void issuerWhenScopesNullThenScopesDefaulted() throws Exception { + this.response.remove("scopes_supported"); + + ClientRegistration registration = registration(""); + + assertThat(registration.getScopes()).containsOnly("openid"); + } + + @Test + public void issuerWhenGrantTypesSupportedNullThenDefaulted() throws Exception { + this.response.remove("grant_types_supported"); + + ClientRegistration registration = registration(""); + + assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + } + + /** + * We currently only support authorization_code, so verify we have a meaningful error until we add support. + * @throws Exception + */ + @Test + public void issuerWhenGrantTypesSupportedInvalidThenException() throws Exception { + this.response.put("grant_types_supported", Arrays.asList("implicit")); + + assertThatThrownBy(() -> registration("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \"" + this.issuer + "\" returned a configuration of [implicit]"); + } + + @Test + public void issuerWhenTokenEndpointAuthMethodsNullThenDefaulted() throws Exception { + this.response.remove("token_endpoint_auth_methods_supported"); + + ClientRegistration registration = registration(""); + + assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); + } + + /** + * We currently only support client_secret_basic, so verify we have a meaningful error until we add support. + * @throws Exception + */ + @Test + public void issuerWhenTokenEndpointAuthMethodsInvalidThenException() throws Exception { + this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_post")); + + assertThatThrownBy(() -> registration("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Only ClientAuthenticationMethod.BASIC is supported. The issuer \"" + this.issuer + "\" returned a configuration of [client_secret_post]"); + } + + private ClientRegistration registration(String path) throws Exception { + String body = this.mapper.writeValueAsString(this.response); + MockResponse mockResponse = new MockResponse() + .setBody(body) + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + this.server.enqueue(mockResponse); + this.issuer = this.server.url(path).toString(); + + return OidcConfigurationProvider.issuer(this.issuer) + .clientId("client-id") + .clientSecret("client-secret") + .build(); + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java index 54e730717ed..080dde9d692 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java @@ -21,6 +21,7 @@ import org.springframework.util.Assert; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; @@ -324,6 +325,20 @@ public Builder scope(String... scope) { return this; } + /** + * Sets the scope(s) used for the client. + * + * @param scope the scope(s) used for the client + * @return the {@link Builder} + */ + public Builder scope(Collection scope) { + if (scope != null && !scope.isEmpty()) { + this.scopes = Collections.unmodifiableSet( + new LinkedHashSet<>(scope)); + } + return this; + } + /** * Sets the uri for the authorization endpoint. * From f61c842c80e9e33c1391bdae887b6b87efa5fb1b Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 18 May 2018 09:57:12 -0500 Subject: [PATCH 016/226] Move OidcConfigurationProvider to .oidc package Issue: gh-5355 --- .../oauth2/client/{ => oidc}/OidcConfigurationProvider.java | 2 +- .../client/{ => oidc}/OidcConfigurationProviderTests.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) rename config/src/main/java/org/springframework/security/config/oauth2/client/{ => oidc}/OidcConfigurationProvider.java (98%) rename config/src/test/java/org/springframework/security/config/oauth2/client/{ => oidc}/OidcConfigurationProviderTests.java (98%) diff --git a/config/src/main/java/org/springframework/security/config/oauth2/client/OidcConfigurationProvider.java b/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java similarity index 98% rename from config/src/main/java/org/springframework/security/config/oauth2/client/OidcConfigurationProvider.java rename to config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java index 77984669c56..3046b5d85de 100644 --- a/config/src/main/java/org/springframework/security/config/oauth2/client/OidcConfigurationProvider.java +++ b/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.security.config.oauth2.client; +package org.springframework.security.config.oauth2.client.oidc; import java.net.URI; import java.util.Arrays; diff --git a/config/src/test/java/org/springframework/security/config/oauth2/client/OidcConfigurationProviderTests.java b/config/src/test/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProviderTests.java similarity index 98% rename from config/src/test/java/org/springframework/security/config/oauth2/client/OidcConfigurationProviderTests.java rename to config/src/test/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProviderTests.java index bd967c58ed5..b2b1420efa1 100644 --- a/config/src/test/java/org/springframework/security/config/oauth2/client/OidcConfigurationProviderTests.java +++ b/config/src/test/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProviderTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.security.config.oauth2.client; +package org.springframework.security.config.oauth2.client.oidc; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -25,6 +25,7 @@ import org.junit.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.security.config.oauth2.client.oidc.OidcConfigurationProvider; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; From 3cca5ad31c853e44d4ea294ca85b5d037178a4a1 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 18 May 2018 10:02:07 -0500 Subject: [PATCH 017/226] Polish OidcConfigurationProvider Javadoc Issue: gh-5355 --- .../oauth2/client/oidc/OidcConfigurationProvider.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java b/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java index 3046b5d85de..d45dde61a54 100644 --- a/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java +++ b/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java @@ -41,15 +41,15 @@ public final class OidcConfigurationProvider { /** - * Given the Issuer creates a - * {@link ClientRegistration.Builder} by making an + * Creates a {@link ClientRegistration.Builder} using the provided + * Issuer by making an * OpenID Provider * Configuration Request and using the values in the * OpenID * Provider Configuration Response to initialize the {@link ClientRegistration.Builder}. * *

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

From 5cafdbb3377154b2c341f73bb58578fd369cc4ea Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 18 May 2018 10:03:36 -0500 Subject: [PATCH 018/226] OidcConfigurationProvider uses OidcScopes.OPENID Issue: gh-5355 --- .../config/oauth2/client/oidc/OidcConfigurationProvider.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java b/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java index d45dde61a54..92c2417fac7 100644 --- a/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java +++ b/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java @@ -24,6 +24,7 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; +import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.web.client.RestTemplate; import com.nimbusds.oauth2.sdk.GrantType; @@ -99,7 +100,7 @@ private static List getScopes(OIDCProviderMetadata metadata) { Scope scope = metadata.getScopes(); if (scope == null) { // If null, default to "openid" which must be supported - return Arrays.asList("openid"); + return Arrays.asList(OidcScopes.OPENID); } else { return scope.toStringList(); } From b3f789ee4152091a46cc147f752b097a764ee042 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 18 May 2018 10:35:53 -0500 Subject: [PATCH 019/226] Add OidcConfigurationProvider ClientAuthenticationMethod.POST support Issue: gh-5355 --- .../oidc/OidcConfigurationProvider.java | 22 +++++++++++++------ .../oidc/OidcConfigurationProviderTests.java | 14 +++++++++--- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java b/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java index 92c2417fac7..98e9793814f 100644 --- a/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java +++ b/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java @@ -72,13 +72,9 @@ public static ClientRegistration.Builder issuer(String issuer) { String openidConfiguration = rest.getForObject(issuer + "/.well-known/openid-configuration", String.class); OIDCProviderMetadata metadata = parse(openidConfiguration); String name = URI.create(issuer).getHost(); - List metadataAuthMethods = metadata.getTokenEndpointAuthMethods(); - // if null, the default includes client_secret_basic - if (metadataAuthMethods != null && !metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) { - throw new IllegalArgumentException("Only ClientAuthenticationMethod.BASIC is supported. The issuer \"" + issuer + "\" returned a configuration of " + metadataAuthMethods); - } + ClientAuthenticationMethod method = getClientAuthenticationMethod(issuer, metadata.getTokenEndpointAuthMethods()); List grantTypes = metadata.getGrantTypes(); - // If null, the default includes authorization_code + // If null, the default includes authorization_code if (grantTypes != null && !grantTypes.contains(GrantType.AUTHORIZATION_CODE)) { throw new IllegalArgumentException("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \"" + issuer + "\" returned a configuration of " + grantTypes); } @@ -87,7 +83,7 @@ public static ClientRegistration.Builder issuer(String issuer) { .userNameAttributeName(IdTokenClaimNames.SUB) .scope(scopes) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .clientAuthenticationMethod(method) .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}") .authorizationUri(metadata.getAuthorizationEndpointURI().toASCIIString()) .jwkSetUri(metadata.getJWKSetURI().toASCIIString()) @@ -96,6 +92,18 @@ public static ClientRegistration.Builder issuer(String issuer) { .clientName(issuer); } + + private static ClientAuthenticationMethod getClientAuthenticationMethod(String issuer, List metadataAuthMethods) { + if (metadataAuthMethods == null || metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) { + // If null, the default includes client_secret_basic + return ClientAuthenticationMethod.BASIC; + } + if (metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_POST)) { + return ClientAuthenticationMethod.POST; + } + throw new IllegalArgumentException("Only ClientAuthenticationMethod.BASIC and ClientAuthenticationMethod.POST are supported. The issuer \"" + issuer + "\" returned a configuration of " + metadataAuthMethods); + } + private static List getScopes(OIDCProviderMetadata metadata) { Scope scope = metadata.getScopes(); if (scope == null) { diff --git a/config/src/test/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProviderTests.java b/config/src/test/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProviderTests.java index b2b1420efa1..fc0fe965f35 100644 --- a/config/src/test/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProviderTests.java +++ b/config/src/test/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProviderTests.java @@ -25,7 +25,6 @@ import org.junit.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.security.config.oauth2.client.oidc.OidcConfigurationProvider; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; @@ -181,17 +180,26 @@ public void issuerWhenTokenEndpointAuthMethodsNullThenDefaulted() throws Excepti assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); } + @Test + public void issuerWhenTokenEndpointAuthMethodsPostThenMethodIsPost() throws Exception { + this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_post")); + + ClientRegistration registration = registration(""); + + assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.POST); + } + /** * We currently only support client_secret_basic, so verify we have a meaningful error until we add support. * @throws Exception */ @Test public void issuerWhenTokenEndpointAuthMethodsInvalidThenException() throws Exception { - this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_post")); + this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("tls_client_auth")); assertThatThrownBy(() -> registration("")) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Only ClientAuthenticationMethod.BASIC is supported. The issuer \"" + this.issuer + "\" returned a configuration of [client_secret_post]"); + .hasMessageContaining("Only ClientAuthenticationMethod.BASIC and ClientAuthenticationMethod.POST are supported. The issuer \"" + this.issuer + "\" returned a configuration of [tls_client_auth]"); } private ClientRegistration registration(String path) throws Exception { From e9eb7a23b3910cf30fc8085f3823f881148c3e46 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 18 May 2018 11:21:45 -0500 Subject: [PATCH 020/226] OidcConfigurationProvider improve invalid issuer error Issue: gh-5355 --- .../oauth2/client/oidc/OidcConfigurationProvider.java | 11 +++++++++-- .../client/oidc/OidcConfigurationProviderTests.java | 6 ++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java b/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java index 98e9793814f..203c4d6c186 100644 --- a/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java +++ b/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java @@ -68,8 +68,7 @@ public final class OidcConfigurationProvider { * @return a {@link ClientRegistration.Builder} that was initialized by the OpenID Provider Configuration. */ public static ClientRegistration.Builder issuer(String issuer) { - RestTemplate rest = new RestTemplate(); - String openidConfiguration = rest.getForObject(issuer + "/.well-known/openid-configuration", String.class); + String openidConfiguration = getOpenidConfiguration(issuer); OIDCProviderMetadata metadata = parse(openidConfiguration); String name = URI.create(issuer).getHost(); ClientAuthenticationMethod method = getClientAuthenticationMethod(issuer, metadata.getTokenEndpointAuthMethods()); @@ -92,6 +91,14 @@ public static ClientRegistration.Builder issuer(String issuer) { .clientName(issuer); } + private static String getOpenidConfiguration(String issuer) { + RestTemplate rest = new RestTemplate(); + try { + return rest.getForObject(issuer + "/.well-known/openid-configuration", String.class); + } catch(RuntimeException e) { + throw new IllegalArgumentException("Unable to resolve the OpenID Configuration with the provided Issuer of \"" + issuer + "\"", e); + } + } private static ClientAuthenticationMethod getClientAuthenticationMethod(String issuer, List metadataAuthMethods) { if (metadataAuthMethods == null || metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) { diff --git a/config/src/test/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProviderTests.java b/config/src/test/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProviderTests.java index fc0fe965f35..28947090bc1 100644 --- a/config/src/test/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProviderTests.java +++ b/config/src/test/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProviderTests.java @@ -202,6 +202,12 @@ public void issuerWhenTokenEndpointAuthMethodsInvalidThenException() throws Exce .hasMessageContaining("Only ClientAuthenticationMethod.BASIC and ClientAuthenticationMethod.POST are supported. The issuer \"" + this.issuer + "\" returned a configuration of [tls_client_auth]"); } + @Test + public void issuerWhenEmptyStringThenMeaningfulErrorMessage() { + assertThatThrownBy(() -> OidcConfigurationProvider.issuer("")) + .hasMessageContaining("Unable to resolve the OpenID Configuration with the provided Issuer of \"\""); + } + private ClientRegistration registration(String path) throws Exception { String body = this.mapper.writeValueAsString(this.response); MockResponse mockResponse = new MockResponse() From 684688b28a07566035b400182fc68cfae28ac726 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 18 May 2018 11:25:40 -0500 Subject: [PATCH 021/226] Exclude build from settings.gradle scanning --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index a2de87930e4..e4d94803735 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,7 +3,7 @@ rootProject.name = 'spring-security' FileTree buildFiles = fileTree(rootDir) { List excludes = gradle.startParameter.projectProperties.get("excludeProjects")?.split(",") include '**/*.gradle' - exclude '**/gradle', 'settings.gradle', 'buildSrc', '/build.gradle', '.*', 'out' + exclude 'build', ''**/gradle', 'settings.gradle', 'buildSrc', '/build.gradle', '.*', 'out' exclude '**/grails3' if(excludes) { exclude excludes From 5a8129a8dc16fa07ad7b3ec1d955160a9b668918 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 18 May 2018 11:27:06 -0500 Subject: [PATCH 022/226] Polish settings.gradle --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index e4d94803735..eeee3f19267 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,7 +3,7 @@ rootProject.name = 'spring-security' FileTree buildFiles = fileTree(rootDir) { List excludes = gradle.startParameter.projectProperties.get("excludeProjects")?.split(",") include '**/*.gradle' - exclude 'build', ''**/gradle', 'settings.gradle', 'buildSrc', '/build.gradle', '.*', 'out' + exclude 'build', '**/gradle', 'settings.gradle', 'buildSrc', '/build.gradle', '.*', 'out' exclude '**/grails3' if(excludes) { exclude excludes From 75dcd63fce16ef7ac9281c126af02fa5c9025fef Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 18 May 2018 13:15:27 -0500 Subject: [PATCH 023/226] OidcConfigurationProvider validate returned issuer Validate the issuer that was returned matches the issuer that was was requested. Issue: gh-5355 --- .../oidc/OidcConfigurationProvider.java | 5 +++++ .../oidc/OidcConfigurationProviderTests.java | 21 +++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java b/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java index 203c4d6c186..e31ae703630 100644 --- a/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java +++ b/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java @@ -70,6 +70,11 @@ public final class OidcConfigurationProvider { public static ClientRegistration.Builder issuer(String issuer) { String openidConfiguration = getOpenidConfiguration(issuer); OIDCProviderMetadata metadata = parse(openidConfiguration); + String metadataIssuer = metadata.getIssuer().getValue(); + if (!issuer.equals(metadataIssuer)) { + throw new IllegalStateException("The Issuer \"" + metadataIssuer + "\" provided in the OpenID Configuration did not match the requested issuer \"" + issuer + "\""); + } + String name = URI.create(issuer).getHost(); ClientAuthenticationMethod method = getClientAuthenticationMethod(issuer, metadata.getTokenEndpointAuthMethods()); List grantTypes = metadata.getGrantTypes(); diff --git a/config/src/test/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProviderTests.java b/config/src/test/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProviderTests.java index 28947090bc1..5b27c451343 100644 --- a/config/src/test/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProviderTests.java +++ b/config/src/test/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProviderTests.java @@ -205,20 +205,37 @@ public void issuerWhenTokenEndpointAuthMethodsInvalidThenException() throws Exce @Test public void issuerWhenEmptyStringThenMeaningfulErrorMessage() { assertThatThrownBy(() -> OidcConfigurationProvider.issuer("")) - .hasMessageContaining("Unable to resolve the OpenID Configuration with the provided Issuer of \"\""); + .hasMessageContaining("Unable to resolve the OpenID Configuration with the provided Issuer of \"\""); + } + + @Test + public void issuerWhenOpenIdConfigurationDoesNotMatchThenMeaningfulErrorMessage() throws Exception { + this.issuer = createIssuerFromServer(""); + String body = this.mapper.writeValueAsString(this.response); + MockResponse mockResponse = new MockResponse() + .setBody(body) + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + this.server.enqueue(mockResponse); + assertThatThrownBy(() -> OidcConfigurationProvider.issuer(this.issuer)) + .hasMessageContaining("The Issuer \"https://example.com\" provided in the OpenID Configuration did not match the requested issuer \"" + this.issuer + "\""); } private ClientRegistration registration(String path) throws Exception { + this.issuer = createIssuerFromServer(path); + this.response.put("issuer", this.issuer); String body = this.mapper.writeValueAsString(this.response); MockResponse mockResponse = new MockResponse() .setBody(body) .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); this.server.enqueue(mockResponse); - this.issuer = this.server.url(path).toString(); return OidcConfigurationProvider.issuer(this.issuer) .clientId("client-id") .clientSecret("client-secret") .build(); } + + private String createIssuerFromServer(String path) { + return this.server.url(path).toString(); + } } From a32d74202a367bc6ce644d590f97cada7996ae9f Mon Sep 17 00:00:00 2001 From: Kazuki Shimizu Date: Sat, 19 May 2018 10:11:18 +0900 Subject: [PATCH 024/226] Polishing the OidcConfigurationProvider See gh-5355 --- .../config/oauth2/client/oidc/OidcConfigurationProvider.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java b/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java index e31ae703630..fdaaf91fa22 100644 --- a/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java +++ b/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java @@ -17,7 +17,7 @@ package org.springframework.security.config.oauth2.client.oidc; import java.net.URI; -import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -120,7 +120,7 @@ private static List getScopes(OIDCProviderMetadata metadata) { Scope scope = metadata.getScopes(); if (scope == null) { // If null, default to "openid" which must be supported - return Arrays.asList(OidcScopes.OPENID); + return Collections.singletonList(OidcScopes.OPENID); } else { return scope.toStringList(); } From 0c653d638a8b1b4ef60a10f4c497f3775d884cf2 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 24 May 2018 09:35:27 -0500 Subject: [PATCH 025/226] Add Cross Site Tracing (XST) & HTTP Method Tampering Protection Fixes: gh-5377 --- .../configurers/CsrfConfigurerTests.groovy | 17 ++++- .../NamespaceHttpFirewallTests.groovy | 2 +- .../NamespaceHttpPortMappingsTests.groovy | 2 +- .../NamespaceRememberMeTests.groovy | 2 +- .../RememberMeConfigurerTests.groovy | 2 +- .../http/AbstractHttpConfigTests.groovy | 2 +- .../config/http/HttpHeadersConfigTests.groovy | 74 +++++++++---------- .../config/http/MiscHttpConfigTests.groovy | 2 +- .../http/MultiHttpBlockConfigTests.groovy | 4 +- .../http/SessionManagementConfigTests.groovy | 12 +-- .../config/FilterChainProxyConfigTests.java | 2 +- .../web/builders/WebSecurityTests.java | 2 +- .../WebSecurityConfigurationTests.java | 2 +- .../configurers/AuthorizeRequestsTests.java | 2 +- .../HttpSecurityAntMatchersTests.java | 2 +- .../configurers/HttpSecurityLogoutTests.java | 2 +- .../HttpSecurityRequestMatchersTests.java | 2 +- ...ionManagementConfigurerServlet31Tests.java | 4 +- .../UrlAuthorizationConfigurerTests.java | 4 +- .../client/OAuth2ClientConfigurerTests.java | 2 +- .../client/OAuth2LoginConfigurerTests.java | 2 +- ...WebSocketMessageBrokerConfigurerTests.java | 2 +- .../core/GrantedAuthorityDefaultsJcTests.java | 2 +- .../GrantedAuthorityDefaultsXmlTests.java | 2 +- ...tadataSourceBeanDefinitionParserTests.java | 2 +- .../config/http/NamespaceHttpBasicTests.java | 2 +- ...SessionManagementConfigServlet31Tests.java | 6 +- .../CustomHttpSecurityConfigurerTests.java | 2 +- .../http/CsrfConfigTests-AutoConfig.xml | 5 ++ .../http/CsrfConfigTests-CsrfEnabled.xml | 5 ++ .../_includes/web/security-filter-chain.adoc | 39 ++++++++++ .../security/web/FilterChainProxy.java | 2 +- .../web/firewall/StrictHttpFirewall.java | 72 ++++++++++++++++++ .../security/web/FilterChainProxyTests.java | 2 +- .../web/firewall/StrictHttpFirewallTests.java | 73 +++++++++++++++--- 35 files changed, 275 insertions(+), 86 deletions(-) diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.groovy index 05ddb22564d..0628529318d 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.groovy @@ -15,7 +15,10 @@ */ package org.springframework.security.config.annotation.web.configurers +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration import org.springframework.security.core.userdetails.PasswordEncodedUser +import org.springframework.security.web.firewall.StrictHttpFirewall import javax.servlet.http.HttpServletResponse @@ -44,7 +47,7 @@ class CsrfConfigurerTests extends BaseSpringSpec { @Unroll def "csrf applied by default"() { setup: - loadConfig(CsrfAppliedDefaultConfig) + loadConfig(CsrfAppliedDefaultConfig, AllowHttpMethodsFirewallConfig) request.method = httpMethod clearCsrfToken() when: @@ -66,11 +69,21 @@ class CsrfConfigurerTests extends BaseSpringSpec { def "csrf default creates CsrfRequestDataValueProcessor"() { when: - loadConfig(CsrfAppliedDefaultConfig) + loadConfig(CsrfAppliedDefaultConfig, AllowHttpMethodsFirewallConfig) then: context.getBean(RequestDataValueProcessor) } + @Configuration + static class AllowHttpMethodsFirewallConfig { + @Bean + StrictHttpFirewall strictHttpFirewall() { + StrictHttpFirewall result = new StrictHttpFirewall(); + result.setAllowedHttpMethods(StrictHttpFirewall.ALLOW_ANY_HTTP_METHOD); + return result; + } + } + @EnableWebSecurity static class CsrfAppliedDefaultConfig extends WebSecurityConfigurerAdapter { diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFirewallTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFirewallTests.groovy index 3ace0a8dc50..54d9e16b979 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFirewallTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFirewallTests.groovy @@ -46,7 +46,7 @@ public class NamespaceHttpFirewallTests extends BaseSpringSpec { MockFilterChain chain def setup() { - request = new MockHttpServletRequest() + request = new MockHttpServletRequest("GET", "") response = new MockHttpServletResponse() chain = new MockFilterChain() } diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpPortMappingsTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpPortMappingsTests.groovy index b36c1501171..2b8dd04b90f 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpPortMappingsTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpPortMappingsTests.groovy @@ -44,7 +44,7 @@ public class NamespaceHttpPortMappingsTests extends BaseSpringSpec { MockFilterChain chain def setup() { - request = new MockHttpServletRequest() + request = new MockHttpServletRequest("GET", "") request.setMethod("GET") response = new MockHttpServletResponse() chain = new MockFilterChain() diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.groovy index fd9a1aecbfb..d73f30c5266 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.groovy @@ -371,7 +371,7 @@ public class NamespaceRememberMeTests extends BaseSpringSpec { } Cookie createRememberMeCookie() { - MockHttpServletRequest request = new MockHttpServletRequest() + MockHttpServletRequest request = new MockHttpServletRequest("GET", "") MockHttpServletResponse response = new MockHttpServletResponse() super.setupCsrf("CSRF_TOKEN", request, response) diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.groovy index 1f2b8e78b30..18520a98554 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.groovy @@ -270,7 +270,7 @@ public class RememberMeConfigurerTests extends BaseSpringSpec { } Cookie createRememberMeCookie() { - MockHttpServletRequest request = new MockHttpServletRequest() + MockHttpServletRequest request = new MockHttpServletRequest("GET", "") MockHttpServletResponse response = new MockHttpServletResponse() super.setupCsrf("CSRF_TOKEN", request, response) diff --git a/config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy index fc3e73baaa3..53ea917d451 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy @@ -67,7 +67,7 @@ abstract class AbstractHttpConfigTests extends AbstractXmlConfigTests { } FilterInvocation createFilterinvocation(String path, String method) { - MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); request.setMethod(method); request.setRequestURI(null); request.setServletPath(path); diff --git a/config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy index de2a887c864..ca3c807cc7e 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy @@ -69,7 +69,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { when: def hf = getFilter(HeaderWriterFilter) MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) + hf.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) then: assertHeaders(response, defaultHeaders) } @@ -83,7 +83,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { when: def hf = getFilter(HeaderWriterFilter) MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) + hf.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) then: assertHeaders(response, defaultHeaders) } @@ -98,7 +98,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def hf = getFilter(HeaderWriterFilter) MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) + hf.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) def expectedHeaders = [:] << defaultHeaders expectedHeaders['X-Frame-Options'] = 'SAMEORIGIN' @@ -131,7 +131,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def hf = getFilter(HeaderWriterFilter) MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) + hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) expect: assertHeaders(response, ['X-Content-Type-Options':'nosniff']) @@ -147,7 +147,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def hf = getFilter(HeaderWriterFilter) MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) + hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) expect: assertHeaders(response, ['X-Frame-Options':'DENY']) @@ -163,7 +163,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def hf = getFilter(HeaderWriterFilter) MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) + hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) expect: assertHeaders(response, ['X-Frame-Options':'DENY']) @@ -179,7 +179,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def hf = getFilter(HeaderWriterFilter) MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) + hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) expect: assertHeaders(response, ['X-Frame-Options':'SAMEORIGIN']) @@ -228,7 +228,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def hf = getFilter(HeaderWriterFilter) MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) + hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) then: assertHeaders(response, ['X-Frame-Options':'ALLOW-FROM https://example.com']) @@ -246,7 +246,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def hf = getFilter(HeaderWriterFilter) MockHttpServletResponse response = new MockHttpServletResponse() - def request = new MockHttpServletRequest() + def request = new MockHttpServletRequest("GET", "") request.setParameter("from", "https://example.com"); hf.doFilter(request, response, new MockFilterChain()) @@ -265,7 +265,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def hf = getFilter(HeaderWriterFilter) MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) + hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) then: assertHeaders(response, ['a':'b']) @@ -283,7 +283,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def hf = getFilter(HeaderWriterFilter) MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) + hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) then: assertHeaders(response , ['a':'b', 'c':'d']) @@ -304,7 +304,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { when: def hf = getFilter(HeaderWriterFilter) MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) + hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) then: assertHeaders(response, ['abc':'def']) } @@ -346,7 +346,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def hf = getFilter(HeaderWriterFilter) MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) + hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) then: assertHeaders(response, ['X-XSS-Protection':'1; mode=block']) @@ -363,7 +363,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def hf = getFilter(HeaderWriterFilter) MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) + hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) then: assertHeaders(response, ['X-XSS-Protection':'1; mode=block']) @@ -380,7 +380,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def hf = getFilter(HeaderWriterFilter) MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) + hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) then: assertHeaders(response, ['X-XSS-Protection':'0']) @@ -413,7 +413,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def springSecurityFilterChain = appContext.getBean(FilterChainProxy) MockHttpServletResponse response = new MockHttpServletResponse() when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) + springSecurityFilterChain.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) then: assertHeaders(response, ['Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate', 'Expires' : '0', @@ -431,7 +431,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def springSecurityFilterChain = appContext.getBean(FilterChainProxy) MockHttpServletResponse response = new MockHttpServletResponse() when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) + springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) then: assertHeaders(response, ['Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains']) } @@ -447,7 +447,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def springSecurityFilterChain = appContext.getBean(FilterChainProxy) MockHttpServletResponse response = new MockHttpServletResponse() when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) + springSecurityFilterChain.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) then: response.headerNames.empty } @@ -465,7 +465,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def springSecurityFilterChain = appContext.getBean(FilterChainProxy) MockHttpServletResponse response = new MockHttpServletResponse() when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) + springSecurityFilterChain.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) then: assertHeaders(response, ['Strict-Transport-Security': 'max-age=1']) } @@ -515,7 +515,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def springSecurityFilterChain = appContext.getBean(FilterChainProxy) MockHttpServletResponse response = new MockHttpServletResponse() when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) + springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) then: assertHeaders(response, ['Public-Key-Pins-Report-Only': 'max-age=5184000 ; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM="']) } @@ -535,7 +535,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def springSecurityFilterChain = appContext.getBean(FilterChainProxy) MockHttpServletResponse response = new MockHttpServletResponse() when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) + springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) then: assertHeaders(response, ['Public-Key-Pins-Report-Only': 'max-age=5184000 ; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM="']) } @@ -555,7 +555,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def springSecurityFilterChain = appContext.getBean(FilterChainProxy) MockHttpServletResponse response = new MockHttpServletResponse() when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) + springSecurityFilterChain.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) then: response.headerNames.empty } @@ -575,7 +575,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def springSecurityFilterChain = appContext.getBean(FilterChainProxy) MockHttpServletResponse response = new MockHttpServletResponse() when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) + springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) then: assertHeaders(response, ['Public-Key-Pins-Report-Only': 'max-age=604800 ; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM="']) } @@ -595,7 +595,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def springSecurityFilterChain = appContext.getBean(FilterChainProxy) MockHttpServletResponse response = new MockHttpServletResponse() when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure: true), response, new MockFilterChain()) + springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure: true, method: "GET"), response, new MockFilterChain()) then: assertHeaders(response, ['Public-Key-Pins': 'max-age=5184000 ; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="']) } @@ -615,7 +615,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def springSecurityFilterChain = appContext.getBean(FilterChainProxy) MockHttpServletResponse response = new MockHttpServletResponse() when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure: true), response, new MockFilterChain()) + springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure: true, method: "GET"), response, new MockFilterChain()) then: assertHeaders(response, ['Public-Key-Pins-Report-Only': 'max-age=5184000 ; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=" ; includeSubDomains']) } @@ -635,7 +635,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def springSecurityFilterChain = appContext.getBean(FilterChainProxy) MockHttpServletResponse response = new MockHttpServletResponse() when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure: true), response, new MockFilterChain()) + springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure: true, method: "GET"), response, new MockFilterChain()) then: assertHeaders(response, ['Public-Key-Pins-Report-Only': 'max-age=5184000 ; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=" ; report-uri="http://example.net/pkp-report"']) } @@ -657,7 +657,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { expectedHeaders.remove('Expires') expectedHeaders.remove('Pragma') when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) + springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) then: assertHeaders(response, expectedHeaders) } @@ -675,7 +675,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def expectedHeaders = [:] << defaultHeaders expectedHeaders.remove('X-Content-Type-Options') when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) + springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) then: assertHeaders(response, expectedHeaders) } @@ -693,7 +693,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def expectedHeaders = [:] << defaultHeaders expectedHeaders.remove('Strict-Transport-Security') when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) + springSecurityFilterChain.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) then: assertHeaders(response, expectedHeaders) } @@ -714,7 +714,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { MockHttpServletResponse response = new MockHttpServletResponse() def expectedHeaders = [:] << defaultHeaders when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) + springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) then: assertHeaders(response, expectedHeaders) } @@ -732,7 +732,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def expectedHeaders = [:] << defaultHeaders expectedHeaders.remove('X-Frame-Options') when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) + springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) then: assertHeaders(response, expectedHeaders) } @@ -750,7 +750,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { def expectedHeaders = [:] << defaultHeaders expectedHeaders.remove('X-XSS-Protection') when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) + springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) then: assertHeaders(response, expectedHeaders) } @@ -853,7 +853,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { when: def hf = getFilter(HeaderWriterFilter) MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) + hf.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) def expectedHeaders = [:] << defaultHeaders expectedHeaders['Content-Security-Policy'] = 'default-src \'self\'' then: @@ -885,7 +885,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { when: def hf = getFilter(HeaderWriterFilter) MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) + hf.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) then: assertHeaders(response, ['Content-Security-Policy':'default-src \'self\'']) } @@ -913,7 +913,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { when: def hf = getFilter(HeaderWriterFilter) MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) + hf.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) def expectedHeaders = [:] << defaultHeaders expectedHeaders['Content-Security-Policy-Report-Only'] = 'default-src https:; report-uri https://example.com/' then: @@ -931,7 +931,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { when: def hf = getFilter(HeaderWriterFilter) MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) + hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) then: assertHeaders(response, ['Referrer-Policy': 'no-referrer']) } @@ -947,7 +947,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { when: def hf = getFilter(HeaderWriterFilter) MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) + hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) then: assertHeaders(response, ['Referrer-Policy': 'same-origin']) } diff --git a/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy index 4c4bab3b992..f84677d3062 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy @@ -142,7 +142,7 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests { createAppContext() then: Filter debugFilter = appContext.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN); - MockHttpServletRequest request = new MockHttpServletRequest() + MockHttpServletRequest request = new MockHttpServletRequest("GET", "") request.setServletPath("/unprotected"); debugFilter.doFilter(request, new MockHttpServletResponse(), new MockFilterChain()); request.setServletPath("/nomatch"); diff --git a/config/src/test/groovy/org/springframework/security/config/http/MultiHttpBlockConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/MultiHttpBlockConfigTests.groovy index 6f06c394113..6d54e6b384b 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/MultiHttpBlockConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/MultiHttpBlockConfigTests.groovy @@ -93,7 +93,7 @@ class MultiHttpBlockConfigTests extends AbstractHttpConfigTests { UserDetailsService uds = appContext.getBean('uds') UserDetailsService uds2 = appContext.getBean('uds2') when: - MockHttpServletRequest request = new MockHttpServletRequest() + MockHttpServletRequest request = new MockHttpServletRequest("GET", "") MockHttpServletResponse response = new MockHttpServletResponse() MockFilterChain chain = new MockFilterChain() request.servletPath = "/first/login" @@ -104,7 +104,7 @@ class MultiHttpBlockConfigTests extends AbstractHttpConfigTests { verify(uds).loadUserByUsername(anyString()) || true verifyZeroInteractions(uds2) || true when: - MockHttpServletRequest request2 = new MockHttpServletRequest() + MockHttpServletRequest request2 = new MockHttpServletRequest("GET", "") MockHttpServletResponse response2 = new MockHttpServletResponse() MockFilterChain chain2 = new MockFilterChain() request2.servletPath = "/login" diff --git a/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy index a4e2e6748ca..4b7908e5e71 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy @@ -115,7 +115,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests { createAppContext() SessionRegistry registry = appContext.getBean(SessionRegistry) registry.registerNewSession("1", new User("user","password",AuthorityUtils.createAuthorityList("ROLE_USER"))) - MockHttpServletRequest request = new MockHttpServletRequest() + MockHttpServletRequest request = new MockHttpServletRequest("GET", "") MockHttpServletResponse response = new MockHttpServletResponse() String credentials = "user:password" request.addHeader("Authorization", "Basic " + credentials.bytes.encodeBase64()) @@ -134,7 +134,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests { } } createAppContext() - MockHttpServletRequest request = new MockHttpServletRequest() + MockHttpServletRequest request = new MockHttpServletRequest("GET", "") MockHttpServletResponse response = new MockHttpServletResponse() String originalSessionId = request.session.id String credentials = "user:password" @@ -282,7 +282,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests { mockBean(SessionAuthenticationStrategy,'ss') createAppContext() - MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); request.getSession(); request.servletPath = "/login" request.setMethod("POST"); @@ -343,15 +343,15 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests { } }; when: "First session is established" - seshFilter.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()); + seshFilter.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()); then: "ok" mockResponse.redirectedUrl == null when: "Second session is established" - seshFilter.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()); + seshFilter.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()); then: "ok" mockResponse.redirectedUrl == null when: "Third session is established" - seshFilter.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()); + seshFilter.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()); then: "Rejected" mockResponse.redirectedUrl == "/max-exceeded"; } diff --git a/config/src/test/java/org/springframework/security/config/FilterChainProxyConfigTests.java b/config/src/test/java/org/springframework/security/config/FilterChainProxyConfigTests.java index ba898c74ae8..2ed1916a9e4 100644 --- a/config/src/test/java/org/springframework/security/config/FilterChainProxyConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/FilterChainProxyConfigTests.java @@ -152,7 +152,7 @@ private void checkPathAndFilterOrder(FilterChainProxy filterChainProxy) } private void doNormalOperation(FilterChainProxy filterChainProxy) throws Exception { - MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); request.setServletPath("/foo/secure/super/somefile.html"); MockHttpServletResponse response = new MockHttpServletResponse(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java index 3cc4c10105c..4adca37d33a 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java @@ -54,7 +54,7 @@ public class WebSecurityTests { @Before public void setup() { - this.request = new MockHttpServletRequest(); + this.request = new MockHttpServletRequest("GET", ""); this.request.setMethod("GET"); this.response = new MockHttpServletResponse(); this.chain = new MockFilterChain(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java index 3ddf21e6d60..51b0a960802 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java @@ -274,7 +274,7 @@ protected void configure(HttpSecurity http) throws Exception { public void securityExpressionHandlerWhenPermissionEvaluatorBeanThenPermissionEvaluatorUsed() throws Exception { this.spring.register(WebSecurityExpressionHandlerPermissionEvaluatorBeanConfig.class).autowire(); TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "notused"); - FilterInvocation invocation = new FilterInvocation(new MockHttpServletRequest(), new MockHttpServletResponse(), new MockFilterChain()); + FilterInvocation invocation = new FilterInvocation(new MockHttpServletRequest("GET", ""), new MockHttpServletResponse(), new MockFilterChain()); AbstractSecurityExpressionHandler handler = this.spring.getContext().getBean(AbstractSecurityExpressionHandler.class); EvaluationContext evaluationContext = handler.createEvaluationContext(authentication, invocation); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java index e05f6e7a015..697ddc29373 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java @@ -68,7 +68,7 @@ public class AuthorizeRequestsTests { @Before public void setup() { this.servletContext = spy(new MockServletContext()); - this.request = new MockHttpServletRequest(); + this.request = new MockHttpServletRequest("GET", ""); this.request.setMethod("GET"); this.response = new MockHttpServletResponse(); this.chain = new MockFilterChain(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityAntMatchersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityAntMatchersTests.java index 3a5d338b2e2..fdb6ce5e967 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityAntMatchersTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityAntMatchersTests.java @@ -51,7 +51,7 @@ public class HttpSecurityAntMatchersTests { @Before public void setup() { - request = new MockHttpServletRequest(); + request = new MockHttpServletRequest("GET", ""); response = new MockHttpServletResponse(); chain = new MockFilterChain(); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityLogoutTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityLogoutTests.java index b327926ab69..30200a83c09 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityLogoutTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityLogoutTests.java @@ -52,7 +52,7 @@ public class HttpSecurityLogoutTests { @Before public void setup() { - request = new MockHttpServletRequest(); + request = new MockHttpServletRequest("GET", ""); response = new MockHttpServletResponse(); chain = new MockFilterChain(); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java index 6835425ddf6..1c451a737ff 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java @@ -55,7 +55,7 @@ public class HttpSecurityRequestMatchersTests { @Before public void setup() { - this.request = new MockHttpServletRequest(); + this.request = new MockHttpServletRequest("GET", ""); this.request.setMethod("GET"); this.response = new MockHttpServletResponse(); this.chain = new MockFilterChain(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerServlet31Tests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerServlet31Tests.java index 441a0175465..6d5113c9bad 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerServlet31Tests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerServlet31Tests.java @@ -72,7 +72,7 @@ public class SessionManagementConfigurerServlet31Tests { @Before public void setup() { - request = new MockHttpServletRequest(); + request = new MockHttpServletRequest("GET", ""); response = new MockHttpServletResponse(); chain = new MockFilterChain(); } @@ -88,7 +88,7 @@ public void teardown() { public void changeSessionIdDefaultsInServlet31Plus() throws Exception { spy(ReflectionUtils.class); Method method = mock(Method.class); - MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); request.getSession(); request.setServletPath("/login"); request.setMethod("POST"); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java index 0f47ac85602..2a598af47af 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java @@ -55,7 +55,7 @@ public class UrlAuthorizationConfigurerTests { @Before public void setup() { - this.request = new MockHttpServletRequest(); + this.request = new MockHttpServletRequest("GET", ""); this.request.setMethod("GET"); this.response = new MockHttpServletResponse(); this.chain = new MockFilterChain(); @@ -211,4 +211,4 @@ public void loadConfig(Class... configs) { this.context.getAutowireCapableBeanFactory().autowireBean(this); } -} \ No newline at end of file +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java index 08917c1bad2..ca12f7556c6 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java @@ -140,7 +140,7 @@ public void configureWhenAuthorizationCodeResponseSuccessThenAuthorizedClientSav AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository(); - MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); MockHttpServletResponse response = new MockHttpServletResponse(); authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java index 203143fec13..ba66ece9b48 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java @@ -104,7 +104,7 @@ public class OAuth2LoginConfigurerTests { @Before public void setup() { - this.request = new MockHttpServletRequest(); + this.request = new MockHttpServletRequest("GET", ""); this.response = new MockHttpServletResponse(); this.filterChain = new MockFilterChain(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurerTests.java index 40ff4869f40..b37cb59fdca 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurerTests.java @@ -493,7 +493,7 @@ private MockHttpServletRequest websocketHttpRequest(String mapping) { } private MockHttpServletRequest sockjsHttpRequest(String mapping) { - MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); request.setMethod("GET"); request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/289/tpyx6mde/websocket"); diff --git a/config/src/test/java/org/springframework/security/config/core/GrantedAuthorityDefaultsJcTests.java b/config/src/test/java/org/springframework/security/config/core/GrantedAuthorityDefaultsJcTests.java index f35139dd99f..8b5ad382245 100644 --- a/config/src/test/java/org/springframework/security/config/core/GrantedAuthorityDefaultsJcTests.java +++ b/config/src/test/java/org/springframework/security/config/core/GrantedAuthorityDefaultsJcTests.java @@ -65,7 +65,7 @@ public class GrantedAuthorityDefaultsJcTests { public void setup() { setup("USER"); - request = new MockHttpServletRequest(); + request = new MockHttpServletRequest("GET", ""); request.setMethod("GET"); response = new MockHttpServletResponse(); chain = new MockFilterChain(); diff --git a/config/src/test/java/org/springframework/security/config/core/GrantedAuthorityDefaultsXmlTests.java b/config/src/test/java/org/springframework/security/config/core/GrantedAuthorityDefaultsXmlTests.java index c36341ab506..6eac7b9254c 100644 --- a/config/src/test/java/org/springframework/security/config/core/GrantedAuthorityDefaultsXmlTests.java +++ b/config/src/test/java/org/springframework/security/config/core/GrantedAuthorityDefaultsXmlTests.java @@ -58,7 +58,7 @@ public class GrantedAuthorityDefaultsXmlTests { public void setup() { setup("USER"); - request = new MockHttpServletRequest(); + request = new MockHttpServletRequest("GET", ""); request.setMethod("GET"); response = new MockHttpServletResponse(); chain = new MockFilterChain(); diff --git a/config/src/test/java/org/springframework/security/config/http/FilterSecurityMetadataSourceBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/FilterSecurityMetadataSourceBeanDefinitionParserTests.java index 7b8e1ccd9d0..23a0691fb95 100644 --- a/config/src/test/java/org/springframework/security/config/http/FilterSecurityMetadataSourceBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/FilterSecurityMetadataSourceBeanDefinitionParserTests.java @@ -123,7 +123,7 @@ public void parsingInterceptUrlServletPathFails() { } private FilterInvocation createFilterInvocation(String path, String method) { - MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); request.setRequestURI(null); request.setMethod(method); diff --git a/config/src/test/java/org/springframework/security/config/http/NamespaceHttpBasicTests.java b/config/src/test/java/org/springframework/security/config/http/NamespaceHttpBasicTests.java index da546e69e1d..449320ba5cb 100644 --- a/config/src/test/java/org/springframework/security/config/http/NamespaceHttpBasicTests.java +++ b/config/src/test/java/org/springframework/security/config/http/NamespaceHttpBasicTests.java @@ -52,7 +52,7 @@ public class NamespaceHttpBasicTests { @Before public void setup() { - this.request = new MockHttpServletRequest(); + this.request = new MockHttpServletRequest("GET", ""); this.request.setMethod("GET"); this.response = new MockHttpServletResponse(); this.chain = new MockFilterChain(); diff --git a/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigServlet31Tests.java b/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigServlet31Tests.java index 19da353fbd7..a1bf8cea93b 100644 --- a/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigServlet31Tests.java +++ b/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigServlet31Tests.java @@ -73,7 +73,7 @@ public class SessionManagementConfigServlet31Tests { @Before public void setup() { - request = new MockHttpServletRequest(); + request = new MockHttpServletRequest("GET", ""); response = new MockHttpServletResponse(); chain = new MockFilterChain(); } @@ -89,7 +89,7 @@ public void teardown() { public void changeSessionIdDefaultsInServlet31Plus() throws Exception { spy(ReflectionUtils.class); Method method = mock(Method.class); - MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); request.getSession(); request.setServletPath("/login"); request.setMethod("POST"); @@ -112,7 +112,7 @@ public void changeSessionIdDefaultsInServlet31Plus() throws Exception { public void changeSessionId() throws Exception { spy(ReflectionUtils.class); Method method = mock(Method.class); - MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); request.getSession(); request.setServletPath("/login"); request.setMethod("POST"); diff --git a/config/src/test/java/org/springframework/security/config/http/customconfigurer/CustomHttpSecurityConfigurerTests.java b/config/src/test/java/org/springframework/security/config/http/customconfigurer/CustomHttpSecurityConfigurerTests.java index beda71be972..3d0bfa7edfb 100644 --- a/config/src/test/java/org/springframework/security/config/http/customconfigurer/CustomHttpSecurityConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/http/customconfigurer/CustomHttpSecurityConfigurerTests.java @@ -55,7 +55,7 @@ public class CustomHttpSecurityConfigurerTests { @Before public void setup() { - request = new MockHttpServletRequest(); + request = new MockHttpServletRequest("GET", ""); response = new MockHttpServletResponse(); chain = new MockFilterChain(); request.setMethod("GET"); diff --git a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-AutoConfig.xml b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-AutoConfig.xml index 44959cf8d9e..644f4a8d744 100644 --- a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-AutoConfig.xml +++ b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-AutoConfig.xml @@ -18,10 +18,15 @@ + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-CsrfEnabled.xml b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-CsrfEnabled.xml index 3a09d6e370e..001214c123b 100644 --- a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-CsrfEnabled.xml +++ b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-CsrfEnabled.xml @@ -18,13 +18,18 @@ + + + diff --git a/docs/manual/src/docs/asciidoc/_includes/web/security-filter-chain.adoc b/docs/manual/src/docs/asciidoc/_includes/web/security-filter-chain.adoc index 5c3d68c874d..4333b182634 100644 --- a/docs/manual/src/docs/asciidoc/_includes/web/security-filter-chain.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/web/security-filter-chain.adoc @@ -179,6 +179,45 @@ public StrictHttpFirewall httpFirewall() { } ---- +The `StrictHttpFirewall` provides a whitelist of valid HTTP methods that are allowed to protect against https://www.owasp.org/index.php/Cross_Site_Tracing[Cross Site Tracing (XST)] and https://www.owasp.org/index.php/Test_HTTP_Methods_(OTG-CONFIG-006)[HTTP Verb Tampering]. +The default valid methods are "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", and "PUT". +If your application needs to modify the valid methods, you can configure a custom `StrictHttpFirewall` bean. +For example, the following will only allow HTTP "GET" and "POST" methods: + + +[source,xml] +---- + + + +---- + +The same thing can be achieved with Java Configuration by exposing a `StrictHttpFirewall` bean. + +[source,java] +---- +@Bean +public StrictHttpFirewall httpFirewall() { + StrictHttpFirewall firewall = new StrictHttpFirewall(); + firewall.setAllowedHttpMethods(Arrays.asList("GET", "POST")); + return firewall; +} +---- + +[TIP] +==== +If you are using `new MockHttpServletRequest()` it currently creates an HTTP method as an empty String "". +This is an invalid HTTP method and will be rejected by Spring Security. +You can resolve this by replacing it with `new MockHttpServletRequest("GET", "")`. +See https://jira.spring.io/browse/SPR-16851[SPR_16851] for an issue requesting to improve this. +==== + +If you must allow any HTTP method (not recommended), you can use `StrictHttpFirewall.setUnsafeAllowAnyHttpMethod(true)`. +This will disable validation of the HTTP method entirely. + + === Use with other Filter-Based Frameworks If you're using some other framework that is also filter-based, then you need to make sure that the Spring Security filters come first. This enables the `SecurityContextHolder` to be populated in time for use by the other filters. diff --git a/web/src/main/java/org/springframework/security/web/FilterChainProxy.java b/web/src/main/java/org/springframework/security/web/FilterChainProxy.java index 040a263bcdd..959f9b0d49e 100644 --- a/web/src/main/java/org/springframework/security/web/FilterChainProxy.java +++ b/web/src/main/java/org/springframework/security/web/FilterChainProxy.java @@ -238,7 +238,7 @@ private List getFilters(HttpServletRequest request) { * @return matching filter list */ public List getFilters(String url) { - return getFilters(firewall.getFirewalledRequest((new FilterInvocation(url, null) + return getFilters(firewall.getFirewalledRequest((new FilterInvocation(url, "GET") .getRequest()))); } diff --git a/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java b/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java index ddfc2e8faa8..03eaeeec164 100644 --- a/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java +++ b/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java @@ -16,6 +16,8 @@ package org.springframework.security.web.firewall; +import org.springframework.http.HttpMethod; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Arrays; @@ -35,6 +37,11 @@ *

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

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

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

    * Determines if semicolon is allowed in the URL (i.e. matrix variables). The default @@ -242,6 +289,7 @@ private void urlBlacklistsRemoveAll(Collection values) { @Override public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException { + rejectForbiddenHttpMethod(request); rejectedBlacklistedUrls(request); if (!isNormalized(request)) { @@ -259,6 +307,18 @@ public void reset() { }; } + private void rejectForbiddenHttpMethod(HttpServletRequest request) { + if (this.allowedHttpMethods == ALLOW_ANY_HTTP_METHOD) { + return; + } + if (!this.allowedHttpMethods.contains(request.getMethod())) { + throw new RequestRejectedException("The request was rejected because the HTTP method \"" + + request.getMethod() + + "\" was not included within the whitelist " + + this.allowedHttpMethods); + } + } + private void rejectedBlacklistedUrls(HttpServletRequest request) { for (String forbidden : this.encodedUrlBlacklist) { if (encodedUrlContains(request, forbidden)) { @@ -277,6 +337,18 @@ public HttpServletResponse getFirewalledResponse(HttpServletResponse response) { return new FirewalledResponse(response); } + private static Set createDefaultAllowedHttpMethods() { + Set result = new HashSet<>(); + result.add(HttpMethod.DELETE.name()); + result.add(HttpMethod.GET.name()); + result.add(HttpMethod.HEAD.name()); + result.add(HttpMethod.OPTIONS.name()); + result.add(HttpMethod.PATCH.name()); + result.add(HttpMethod.POST.name()); + result.add(HttpMethod.PUT.name()); + return result; + } + private static boolean isNormalized(HttpServletRequest request) { if (!isNormalized(request.getRequestURI())) { return false; diff --git a/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java b/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java index 576fda3f75f..62738494247 100644 --- a/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java +++ b/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java @@ -69,7 +69,7 @@ public Object answer(InvocationOnMock inv) throws Throwable { fcp = new FilterChainProxy(new DefaultSecurityFilterChain(matcher, Arrays.asList(filter))); fcp.setFilterChainValidator(mock(FilterChainProxy.FilterChainValidator.class)); - request = new MockHttpServletRequest(); + request = new MockHttpServletRequest("GET", ""); request.setServletPath("/path"); response = new MockHttpServletResponse(); chain = mock(FilterChain.class); diff --git a/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java b/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java index 5d2e57d3e78..27e2c0c3808 100644 --- a/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java +++ b/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java @@ -16,11 +16,17 @@ package org.springframework.security.web.firewall; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; + +import java.util.Arrays; +import java.util.List; + import org.junit.Test; +import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockHttpServletRequest; -import static org.assertj.core.api.Assertions.fail; - /** * @author Rob Winch */ @@ -31,12 +37,61 @@ public class StrictHttpFirewallTests { private StrictHttpFirewall firewall = new StrictHttpFirewall(); - private MockHttpServletRequest request = new MockHttpServletRequest(); + private MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); + + @Test + public void getFirewalledRequestWhenInvalidMethodThenThrowsRequestRejectedException() { + this.request.setMethod("INVALID"); + assertThatThrownBy(() -> this.firewall.getFirewalledRequest(this.request)) + .isInstanceOf(RequestRejectedException.class); + } + + // blocks XST attacks + @Test + public void getFirewalledRequestWhenTraceMethodThenThrowsRequestRejectedException() { + this.request.setMethod(HttpMethod.TRACE.name()); + assertThatThrownBy(() -> this.firewall.getFirewalledRequest(this.request)) + .isInstanceOf(RequestRejectedException.class); + } + + @Test + // blocks XST attack if request is forwarded to a Microsoft IIS web server + public void getFirewalledRequestWhenTrackMethodThenThrowsRequestRejectedException() { + this.request.setMethod("TRACK"); + assertThatThrownBy(() -> this.firewall.getFirewalledRequest(this.request)) + .isInstanceOf(RequestRejectedException.class); + } + + @Test + // HTTP methods are case sensitive + public void getFirewalledRequestWhenLowercaseGetThenThrowsRequestRejectedException() { + this.request.setMethod("get"); + assertThatThrownBy(() -> this.firewall.getFirewalledRequest(this.request)) + .isInstanceOf(RequestRejectedException.class); + } + + @Test + public void getFirewalledRequestWhenAllowedThenNoException() { + List allowedMethods = Arrays.asList("DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"); + for (String allowedMethod : allowedMethods) { + this.request = new MockHttpServletRequest(allowedMethod, ""); + assertThatCode(() -> this.firewall.getFirewalledRequest(this.request)) + .doesNotThrowAnyException(); + } + } + + @Test + public void getFirewalledRequestWhenInvalidMethodAndAnyMethodThenNoException() { + this.firewall.setUnsafeAllowAnyHttpMethod(true); + this.request.setMethod("INVALID"); + assertThatCode(() -> this.firewall.getFirewalledRequest(this.request)) + .doesNotThrowAnyException(); + } @Test public void getFirewalledRequestWhenRequestURINotNormalizedThenThrowsRequestRejectedException() throws Exception { for (String path : this.unnormalizedPaths) { - this.request = new MockHttpServletRequest(); + this.request = new MockHttpServletRequest("GET", ""); this.request.setRequestURI(path); try { this.firewall.getFirewalledRequest(this.request); @@ -49,7 +104,7 @@ public void getFirewalledRequestWhenRequestURINotNormalizedThenThrowsRequestReje @Test public void getFirewalledRequestWhenContextPathNotNormalizedThenThrowsRequestRejectedException() throws Exception { for (String path : this.unnormalizedPaths) { - this.request = new MockHttpServletRequest(); + this.request = new MockHttpServletRequest("GET", ""); this.request.setContextPath(path); try { this.firewall.getFirewalledRequest(this.request); @@ -62,7 +117,7 @@ public void getFirewalledRequestWhenContextPathNotNormalizedThenThrowsRequestRej @Test public void getFirewalledRequestWhenServletPathNotNormalizedThenThrowsRequestRejectedException() throws Exception { for (String path : this.unnormalizedPaths) { - this.request = new MockHttpServletRequest(); + this.request = new MockHttpServletRequest("GET", ""); this.request.setServletPath(path); try { this.firewall.getFirewalledRequest(this.request); @@ -75,7 +130,7 @@ public void getFirewalledRequestWhenServletPathNotNormalizedThenThrowsRequestRej @Test public void getFirewalledRequestWhenPathInfoNotNormalizedThenThrowsRequestRejectedException() throws Exception { for (String path : this.unnormalizedPaths) { - this.request = new MockHttpServletRequest(); + this.request = new MockHttpServletRequest("GET", ""); this.request.setPathInfo(path); try { this.firewall.getFirewalledRequest(this.request); @@ -352,7 +407,7 @@ public void getFirewalledRequestWhenUppercaseEncodedPathThenException() { public void getFirewalledRequestWhenAllowUrlEncodedSlashAndLowercaseEncodedPathThenNoException() { this.firewall.setAllowUrlEncodedSlash(true); this.firewall.setAllowSemicolon(true); - MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); request.setRequestURI("/context-root/a/b;%2f1/c"); request.setContextPath("/context-root"); request.setServletPath(""); @@ -365,7 +420,7 @@ public void getFirewalledRequestWhenAllowUrlEncodedSlashAndLowercaseEncodedPathT public void getFirewalledRequestWhenAllowUrlEncodedSlashAndUppercaseEncodedPathThenNoException() { this.firewall.setAllowUrlEncodedSlash(true); this.firewall.setAllowSemicolon(true); - MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); request.setRequestURI("/context-root/a/b;%2F1/c"); request.setContextPath("/context-root"); request.setServletPath(""); From 2662f768a9dd85e7695da699f8a27b9b87b2009a Mon Sep 17 00:00:00 2001 From: Eric Deandrea Date: Thu, 24 May 2018 09:31:36 -0400 Subject: [PATCH 026/226] DelegatingServerLogoutHandler Create a ServerLogoutHandler which delegates to a group of ServerLogoutHandler implementations. Fixes gh-4839 --- .../logout/DelegatingServerLogoutHandler.java | 49 ++++++++++ .../DelegatingServerLogoutHandlerTests.java | 91 +++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 web/src/main/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandler.java create mode 100644 web/src/test/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandlerTests.java diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandler.java b/web/src/main/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandler.java new file mode 100644 index 00000000000..f5813f4bd3c --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandler.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.authentication.logout; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.WebFilterExchange; +import org.springframework.util.Assert; + +import reactor.core.publisher.Mono; + +/** + * Delegates to a collection of {@link ServerLogoutHandler} implementations. + * + * @author Eric Deandrea + * @since 5.1 + */ +public class DelegatingServerLogoutHandler implements ServerLogoutHandler { + private final List delegates; + + public DelegatingServerLogoutHandler(ServerLogoutHandler... delegates) { + Assert.notEmpty(delegates, "delegates cannot be null or empty"); + this.delegates = Arrays.asList(delegates); + } + + @Override + public Mono logout(WebFilterExchange exchange, Authentication authentication) { + Stream> results = this.delegates.stream().map(delegate -> delegate.logout(exchange, authentication)); + return Mono.when(results.collect(Collectors.toList())); + } +} diff --git a/web/src/test/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandlerTests.java b/web/src/test/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandlerTests.java new file mode 100644 index 00000000000..ef1a0258ae4 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandlerTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.authentication.logout; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.WebFilterExchange; + +import reactor.test.publisher.PublisherProbe; + +/** + * @author Eric Deandrea + * @since 5.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class DelegatingServerLogoutHandlerTests { + @Mock + private ServerLogoutHandler delegate1; + + @Mock + private ServerLogoutHandler delegate2; + private PublisherProbe delegate1Result = PublisherProbe.empty(); + private PublisherProbe delegate2Result = PublisherProbe.empty(); + + @Mock + private WebFilterExchange exchange; + + @Mock + private Authentication authentication; + + @Before + public void setup() { + when(this.delegate1.logout(any(WebFilterExchange.class), any(Authentication.class))).thenReturn(this.delegate1Result.mono()); + when(this.delegate2.logout(any(WebFilterExchange.class), any(Authentication.class))).thenReturn(this.delegate2Result.mono()); + } + + @Test + public void constructorWhenNullThenIllegalArgumentException() { + assertThatThrownBy(() -> new DelegatingServerLogoutHandler((ServerLogoutHandler[]) null)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("delegates cannot be null or empty") + .hasNoCause(); + } + + @Test + public void constructorWhenEmptyThenIllegalArgumentException() { + assertThatThrownBy(() -> new DelegatingServerLogoutHandler(new ServerLogoutHandler[0])) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("delegates cannot be null or empty") + .hasNoCause(); + } + + @Test + public void logoutWhenSingleThenExecuted() { + DelegatingServerLogoutHandler handler = new DelegatingServerLogoutHandler(this.delegate1); + handler.logout(this.exchange, this.authentication).block(); + + this.delegate1Result.assertWasSubscribed(); + } + + @Test + public void logoutWhenMultipleThenExecuted() { + DelegatingServerLogoutHandler handler = new DelegatingServerLogoutHandler(this.delegate1, this.delegate2); + handler.logout(this.exchange, this.authentication).block(); + + this.delegate1Result.assertWasSubscribed(); + this.delegate2Result.assertWasSubscribed(); + } +} From 6d98e3c59e278872287d04f5d474c38c5bf575a4 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 24 May 2018 09:44:29 -0500 Subject: [PATCH 027/226] Add DelegatingServerLogoutHandler(List delegates) Issue: gh-4839 --- .../logout/DelegatingServerLogoutHandler.java | 6 ++++++ .../logout/DelegatingServerLogoutHandlerTests.java | 12 +++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandler.java b/web/src/main/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandler.java index f5813f4bd3c..f809f43f17e 100644 --- a/web/src/main/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandler.java +++ b/web/src/main/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandler.java @@ -16,6 +16,7 @@ package org.springframework.security.web.server.authentication.logout; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -41,6 +42,11 @@ public DelegatingServerLogoutHandler(ServerLogoutHandler... delegates) { this.delegates = Arrays.asList(delegates); } + public DelegatingServerLogoutHandler(List delegates) { + Assert.notEmpty(delegates, "delegates cannot be null or empty"); + this.delegates = new ArrayList<>(delegates); + } + @Override public Mono logout(WebFilterExchange exchange, Authentication authentication) { Stream> results = this.delegates.stream().map(delegate -> delegate.logout(exchange, authentication)); diff --git a/web/src/test/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandlerTests.java b/web/src/test/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandlerTests.java index ef1a0258ae4..4073c636ee7 100644 --- a/web/src/test/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandlerTests.java +++ b/web/src/test/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandlerTests.java @@ -30,6 +30,8 @@ import reactor.test.publisher.PublisherProbe; +import java.util.List; + /** * @author Eric Deandrea * @since 5.1 @@ -57,13 +59,21 @@ public void setup() { } @Test - public void constructorWhenNullThenIllegalArgumentException() { + public void constructorWhenNullVargsThenIllegalArgumentException() { assertThatThrownBy(() -> new DelegatingServerLogoutHandler((ServerLogoutHandler[]) null)) .isExactlyInstanceOf(IllegalArgumentException.class) .hasMessage("delegates cannot be null or empty") .hasNoCause(); } + @Test + public void constructorWhenNullListThenIllegalArgumentException() { + assertThatThrownBy(() -> new DelegatingServerLogoutHandler((List) null)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("delegates cannot be null or empty") + .hasNoCause(); + } + @Test public void constructorWhenEmptyThenIllegalArgumentException() { assertThatThrownBy(() -> new DelegatingServerLogoutHandler(new ServerLogoutHandler[0])) From f2e9a5f3584b6e34e7c14a8019f421ed73f804be Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 24 May 2018 15:03:12 -0500 Subject: [PATCH 028/226] OAuth2AuthorizationRequestRedirectWebFilter handles ClientAuthorizationRequiredException Fixes: gh-5383 --- .../OAuth2AuthorizationRequestRedirectWebFilter.java | 1 + ...h2AuthorizationRequestRedirectWebFilterTests.java | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilter.java index f49eb4d6df0..d6fcad1fea7 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilter.java @@ -136,6 +136,7 @@ public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { .map(ServerWebExchangeMatcher.MatchResult::getVariables) .map(variables -> variables.get(REGISTRATION_ID_URI_VARIABLE_NAME)) .cast(String.class) + .onErrorResume(ClientAuthorizationRequiredException.class, e -> Mono.just(e.getClientRegistrationId())) .flatMap(clientRegistrationId -> this.findByRegistrationId(exchange, clientRegistrationId)) .flatMap(clientRegistration -> sendRedirectForAuthorization(exchange, clientRegistration)); } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilterTests.java index 82fc51de2ae..ce839c68107 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilterTests.java @@ -21,6 +21,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; @@ -133,4 +134,15 @@ public void filterWhenDoesMatchThenClientRegistrationRepositoryNotSubscribed() { }); verify(this.authzRequestRepository).saveAuthorizationRequest(any(), any()); } + + @Test + public void filterWhenExceptionThenRedirected() { + FilteringWebHandler webHandler = new FilteringWebHandler(e -> Mono.error(new ClientAuthorizationRequiredException(this.github.getRegistrationId())), Arrays.asList(this.filter)); + this.client = WebTestClient.bindToWebHandler(webHandler).build(); + FluxExchangeResult result = this.client.get() + .uri("https://example.com/foo").exchange() + .expectStatus() + .is3xxRedirection() + .returnResult(String.class); + } } From 74d42802358d86875c817664c529979d01c5aa32 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 25 May 2018 09:25:26 -0500 Subject: [PATCH 029/226] Add OAuth2AuthorizedClientExchangeFilterFunction Fixes: gh-5386 --- ...uthorizedClientExchangeFilterFunction.java | 84 +++++++++++++++ .../function/client/MockExchangeFunction.java | 47 ++++++++ ...izedClientExchangeFilterFunctionTests.java | 101 ++++++++++++++++++ 3 files changed, 232 insertions(+) create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunction.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/MockExchangeFunction.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunctionTests.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunction.java new file mode 100644 index 00000000000..178868c39c1 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunction.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.web.reactive.function.client; + +import org.springframework.http.HttpHeaders; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeFunction; +import reactor.core.publisher.Mono; + +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; + +/** + * Provides an easy mechanism for using an {@link OAuth2AuthorizedClient} to make OAuth2 requests by including the + * token as a Bearer Token. + * + * @author Rob Winch + * @since 5.1 + */ +public final class OAuth2AuthorizedClientExchangeFilterFunction implements ExchangeFilterFunction { + /** + * The request attribute name used to locate the {@link OAuth2AuthorizedClient}. + */ + private static final String OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME = OAuth2AuthorizedClient.class.getName(); + + /** + * Modifies the {@link ClientRequest#attributes()} to include the {@link OAuth2AuthorizedClient} to be used for + * providing the Bearer Token. Example usage: + * + *

    +	 * Mono response = this.webClient
    +	 *    .get()
    +	 *    .uri(uri)
    +	 *    .attributes(oauth2AuthorizedClient(authorizedClient))
    +	 *    // ...
    +	 *    .retrieve()
    +	 *    .bodyToMono(String.class);
    +	 * 
    + * + * @param authorizedClient the {@link OAuth2AuthorizedClient} to use. + * @return the {@link Consumer} to populate the + */ + public static Consumer> oauth2AuthorizedClient(OAuth2AuthorizedClient authorizedClient) { + return attributes -> attributes.put(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME, authorizedClient); + } + + @Override + public Mono filter(ClientRequest request, ExchangeFunction next) { + Optional attribute = request.attribute(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME) + .map(OAuth2AuthorizedClient.class::cast); + return attribute + .map(authorizedClient -> bearer(request, authorizedClient)) + .map(next::exchange) + .orElseGet(() -> next.exchange(request)); + } + + private ClientRequest bearer(ClientRequest request, OAuth2AuthorizedClient authorizedClient) { + return ClientRequest.from(request) + .headers(bearerToken(authorizedClient.getAccessToken().getTokenValue())) + .build(); + } + + private Consumer bearerToken(String token) { + return headers -> headers.set(HttpHeaders.AUTHORIZATION, "Bearer " + token); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/MockExchangeFunction.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/MockExchangeFunction.java new file mode 100644 index 00000000000..a6cf3b4183e --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/MockExchangeFunction.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.web.reactive.function.client; + +import static org.mockito.Mockito.mock; + +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFunction; + +import reactor.core.publisher.Mono; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class MockExchangeFunction implements ExchangeFunction { + private ClientRequest request; + + private ClientResponse response = mock(ClientResponse.class); + + public ClientRequest getRequest() { + return this.request; + } + + @Override + public Mono exchange(ClientRequest request) { + return Mono.defer(() -> { + this.request = request; + return Mono.just(this.response); + }); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunctionTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunctionTests.java new file mode 100644 index 00000000000..f313e5d42d7 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunctionTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.web.reactive.function.client; + +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.web.reactive.function.client.ClientRequest; + +import java.net.URI; +import java.time.Duration; +import java.time.Instant; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.security.oauth2.client.web.reactive.function.client.OAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class OAuth2AuthorizedClientExchangeFilterFunctionTests { + private OAuth2AuthorizedClientExchangeFilterFunction function = new OAuth2AuthorizedClientExchangeFilterFunction(); + + private MockExchangeFunction exchange = new MockExchangeFunction(); + + private ClientRegistration github = ClientRegistration.withRegistrationId("github") + .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .scope("read:user") + .authorizationUri("https://github.com/login/oauth/authorize") + .tokenUri("https://github.com/login/oauth/access_token") + .userInfoUri("https://api.github.com/user") + .userNameAttributeName("id") + .clientName("GitHub") + .clientId("clientId") + .clientSecret("clientSecret") + .build(); + + private OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + "token", + Instant.now(), + Instant.now().plus(Duration.ofDays(1))); + + @Test + public void filterWhenAuthorizedClientNullThenAuthorizationHeaderNull() { + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .build(); + + this.function.filter(request, this.exchange).block(); + + assertThat(this.exchange.getRequest().headers().getFirst(HttpHeaders.AUTHORIZATION)).isNull(); + } + + @Test + public void filterWhenAuthorizedClientThenAuthorizationHeader() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + this.function.filter(request, this.exchange).block(); + + assertThat(this.exchange.getRequest().headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + this.accessToken.getTokenValue()); + } + + @Test + public void filterWhenExistingAuthorizationThenSingleAuthorizationHeader() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .header(HttpHeaders.AUTHORIZATION, "Existing") + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + this.function.filter(request, this.exchange).block(); + + HttpHeaders headers = this.exchange.getRequest().headers(); + assertThat(headers.get(HttpHeaders.AUTHORIZATION)).containsOnly("Bearer " + this.accessToken.getTokenValue()); + } +} From 986b1091999acd2a9ca23d0e2742368e2c8e38e1 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 25 May 2018 09:37:28 -0500 Subject: [PATCH 030/226] Samples use OAuth2AuthorizedClientExchangeFilterFunction Issue: gh-5386 --- ...uthorizationCodeGrantApplicationTests.java | 3 ++ .../java/sample/config/WebClientConfig.java | 35 +++++++++++++++++++ .../sample/web/GitHubReposController.java | 27 ++++++-------- .../src/main/java/sample/WebClientConfig.java | 35 +++++++++++++++++++ .../sample/web/OAuth2LoginController.java | 25 +++++-------- .../samples/OAuth2LoginApplicationTests.java | 3 ++ .../src/main/java/sample/WebClientConfig.java | 35 +++++++++++++++++++ .../sample/web/OAuth2LoginController.java | 33 +++++++---------- 8 files changed, 143 insertions(+), 53 deletions(-) create mode 100644 samples/boot/oauth2/authcodegrant/src/main/java/sample/config/WebClientConfig.java create mode 100644 samples/boot/oauth2login-webflux/src/main/java/sample/WebClientConfig.java create mode 100644 samples/boot/oauth2login/src/main/java/sample/WebClientConfig.java diff --git a/samples/boot/oauth2/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java b/samples/boot/oauth2/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java index 59df61ff536..36ee34e3659 100644 --- a/samples/boot/oauth2/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java +++ b/samples/boot/oauth2/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java @@ -23,6 +23,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Import; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpSession; @@ -47,6 +48,7 @@ import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; +import sample.config.WebClientConfig; import java.util.HashMap; import java.util.Map; @@ -160,6 +162,7 @@ private OAuth2AccessTokenResponseClient acc @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(basePackages = "sample.web") + @Import(WebClientConfig.class) public static class SpringBootApplicationTestConfig { } } diff --git a/samples/boot/oauth2/authcodegrant/src/main/java/sample/config/WebClientConfig.java b/samples/boot/oauth2/authcodegrant/src/main/java/sample/config/WebClientConfig.java new file mode 100644 index 00000000000..e462a620b8d --- /dev/null +++ b/samples/boot/oauth2/authcodegrant/src/main/java/sample/config/WebClientConfig.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.security.oauth2.client.web.reactive.function.client.OAuth2AuthorizedClientExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class WebClientConfig { + + @Bean + WebClient webClient() { + return WebClient.builder() + .filter(new OAuth2AuthorizedClientExchangeFilterFunction()) + .build(); + } +} diff --git a/samples/boot/oauth2/authcodegrant/src/main/java/sample/web/GitHubReposController.java b/samples/boot/oauth2/authcodegrant/src/main/java/sample/web/GitHubReposController.java index f32bdfe450a..fb1893fb8d3 100644 --- a/samples/boot/oauth2/authcodegrant/src/main/java/sample/web/GitHubReposController.java +++ b/samples/boot/oauth2/authcodegrant/src/main/java/sample/web/GitHubReposController.java @@ -15,24 +15,28 @@ */ package sample.web; -import org.springframework.http.HttpHeaders; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.annotation.OAuth2Client; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.reactive.function.client.ClientRequest; -import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; import java.util.List; +import static org.springframework.security.oauth2.client.web.reactive.function.client.OAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient; + /** * @author Joe Grandja + * @author Rob Winch */ @Controller public class GitHubReposController { + private final WebClient webClient; + + public GitHubReposController(WebClient webClient) { + this.webClient = webClient; + } @GetMapping("/") public String index() { @@ -42,11 +46,10 @@ public String index() { @GetMapping("/repos") public String gitHubRepos(Model model, @OAuth2Client("github") OAuth2AuthorizedClient authorizedClient) { String endpointUri = "https://api.github.com/user/repos"; - List repos = WebClient.builder() - .filter(oauth2Credentials(authorizedClient)) - .build() + List repos = this.webClient .get() .uri(endpointUri) + .attributes(oauth2AuthorizedClient(authorizedClient)) .retrieve() .bodyToMono(List.class) .block(); @@ -54,14 +57,4 @@ public String gitHubRepos(Model model, @OAuth2Client("github") OAuth2AuthorizedC return "github-repos"; } - - private ExchangeFilterFunction oauth2Credentials(OAuth2AuthorizedClient authorizedClient) { - return ExchangeFilterFunction.ofRequestProcessor( - clientRequest -> { - ClientRequest authorizedRequest = ClientRequest.from(clientRequest) - .header(HttpHeaders.AUTHORIZATION, "Bearer " + authorizedClient.getAccessToken().getTokenValue()) - .build(); - return Mono.just(authorizedRequest); - }); - } } diff --git a/samples/boot/oauth2login-webflux/src/main/java/sample/WebClientConfig.java b/samples/boot/oauth2login-webflux/src/main/java/sample/WebClientConfig.java new file mode 100644 index 00000000000..1d0ed4a2b4f --- /dev/null +++ b/samples/boot/oauth2login-webflux/src/main/java/sample/WebClientConfig.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample; + +import org.springframework.context.annotation.Bean; +import org.springframework.security.oauth2.client.web.reactive.function.client.OAuth2AuthorizedClientExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class WebClientConfig { + + @Bean + WebClient webClient() { + return WebClient.builder() + .filter(new OAuth2AuthorizedClientExchangeFilterFunction()) + .build(); + } +} diff --git a/samples/boot/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java b/samples/boot/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java index d3bcaf342e5..9c86d73bdce 100644 --- a/samples/boot/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java +++ b/samples/boot/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java @@ -16,17 +16,16 @@ package sample.web; +import static org.springframework.security.oauth2.client.web.reactive.function.client.OAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient; + import java.util.Map; -import org.springframework.http.HttpHeaders; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.annotation.OAuth2Client; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.reactive.function.client.ClientRequest; -import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; @@ -36,6 +35,11 @@ */ @Controller public class OAuth2LoginController { + private final WebClient webClient; + + public OAuth2LoginController(WebClient webClient) { + this.webClient = webClient; + } @GetMapping("/") public String index(Model model, @OAuth2Client OAuth2AuthorizedClient authorizedClient) { @@ -50,25 +54,14 @@ public String userinfo(Model model, @OAuth2Client OAuth2AuthorizedClient authori String userInfoEndpointUri = authorizedClient.getClientRegistration() .getProviderDetails().getUserInfoEndpoint().getUri(); if (!StringUtils.isEmpty(userInfoEndpointUri)) { // userInfoEndpointUri is optional for OIDC Clients - userAttributes = WebClient.builder() - .filter(oauth2Credentials(authorizedClient)) - .build() + userAttributes = this.webClient .get() .uri(userInfoEndpointUri) + .attributes(oauth2AuthorizedClient(authorizedClient)) .retrieve() .bodyToMono(Map.class); } model.addAttribute("userAttributes", userAttributes); return "userinfo"; } - - private ExchangeFilterFunction oauth2Credentials(OAuth2AuthorizedClient authorizedClient) { - return ExchangeFilterFunction.ofRequestProcessor( - clientRequest -> { - ClientRequest authorizedRequest = ClientRequest.from(clientRequest) - .header(HttpHeaders.AUTHORIZATION, "Bearer " + authorizedClient.getAccessToken().getTokenValue()) - .build(); - return Mono.just(authorizedRequest); - }); - } } diff --git a/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java b/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java index 1c722da290c..612e3583c92 100644 --- a/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java +++ b/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java @@ -32,6 +32,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -57,6 +58,7 @@ import org.springframework.test.context.junit4.SpringRunner; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; +import sample.WebClientConfig; import java.net.URI; import java.net.URL; @@ -401,6 +403,7 @@ private OAuth2UserService mockUserService() { @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(basePackages = "sample.web") + @Import(WebClientConfig.class) public static class SpringBootApplicationTestConfig { @Autowired diff --git a/samples/boot/oauth2login/src/main/java/sample/WebClientConfig.java b/samples/boot/oauth2login/src/main/java/sample/WebClientConfig.java new file mode 100644 index 00000000000..1d0ed4a2b4f --- /dev/null +++ b/samples/boot/oauth2login/src/main/java/sample/WebClientConfig.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample; + +import org.springframework.context.annotation.Bean; +import org.springframework.security.oauth2.client.web.reactive.function.client.OAuth2AuthorizedClientExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class WebClientConfig { + + @Bean + WebClient webClient() { + return WebClient.builder() + .filter(new OAuth2AuthorizedClientExchangeFilterFunction()) + .build(); + } +} diff --git a/samples/boot/oauth2login/src/main/java/sample/web/OAuth2LoginController.java b/samples/boot/oauth2login/src/main/java/sample/web/OAuth2LoginController.java index 4f8fa14ca82..5c29e84daea 100644 --- a/samples/boot/oauth2login/src/main/java/sample/web/OAuth2LoginController.java +++ b/samples/boot/oauth2login/src/main/java/sample/web/OAuth2LoginController.java @@ -15,26 +15,30 @@ */ package sample.web; -import org.springframework.http.HttpHeaders; +import static org.springframework.security.oauth2.client.web.reactive.function.client.OAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient; + +import java.util.Collections; +import java.util.Map; + import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.annotation.OAuth2Client; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.reactive.function.client.ClientRequest; -import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; - -import java.util.Collections; -import java.util.Map; /** * @author Joe Grandja + * @author Rob Winch */ @Controller public class OAuth2LoginController { + private final WebClient webClient; + + public OAuth2LoginController(WebClient webClient) { + this.webClient = webClient; + } @GetMapping("/") public String index(Model model, @OAuth2Client OAuth2AuthorizedClient authorizedClient) { @@ -49,11 +53,10 @@ public String userinfo(Model model, @OAuth2Client OAuth2AuthorizedClient authori String userInfoEndpointUri = authorizedClient.getClientRegistration() .getProviderDetails().getUserInfoEndpoint().getUri(); if (!StringUtils.isEmpty(userInfoEndpointUri)) { // userInfoEndpointUri is optional for OIDC Clients - userAttributes = WebClient.builder() - .filter(oauth2Credentials(authorizedClient)) - .build() + userAttributes = this.webClient .get() .uri(userInfoEndpointUri) + .attributes(oauth2AuthorizedClient(authorizedClient)) .retrieve() .bodyToMono(Map.class) .block(); @@ -61,14 +64,4 @@ public String userinfo(Model model, @OAuth2Client OAuth2AuthorizedClient authori model.addAttribute("userAttributes", userAttributes); return "userinfo"; } - - private ExchangeFilterFunction oauth2Credentials(OAuth2AuthorizedClient authorizedClient) { - return ExchangeFilterFunction.ofRequestProcessor( - clientRequest -> { - ClientRequest authorizedRequest = ClientRequest.from(clientRequest) - .header(HttpHeaders.AUTHORIZATION, "Bearer " + authorizedClient.getAccessToken().getTokenValue()) - .build(); - return Mono.just(authorizedRequest); - }); - } } From e0d6c332d421b1e8c5f1e3de0b4d6c81c703051a Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 25 May 2018 12:40:58 -0500 Subject: [PATCH 031/226] Update to Spring Boot 2.0.2.RELEASE Fixes: gh-5388 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f92573b2f0c..ca6c51d374c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ gaeVersion=1.9.63 -springBootVersion=2.0.1.RELEASE +springBootVersion=2.0.2.RELEASE version=5.1.0.BUILD-SNAPSHOT From ee4b8cd7b63e29d0a0808c324fadd44b3a27948d Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 25 May 2018 12:48:42 -0500 Subject: [PATCH 032/226] Add missing @Configuration for WebClientConfig Issue: gh-5388 --- .../src/main/java/sample/config/WebClientConfig.java | 2 ++ .../src/main/java/sample/WebClientConfig.java | 2 ++ .../boot/oauth2login/src/main/java/sample/WebClientConfig.java | 2 ++ 3 files changed, 6 insertions(+) diff --git a/samples/boot/oauth2/authcodegrant/src/main/java/sample/config/WebClientConfig.java b/samples/boot/oauth2/authcodegrant/src/main/java/sample/config/WebClientConfig.java index e462a620b8d..09dfea17ab1 100644 --- a/samples/boot/oauth2/authcodegrant/src/main/java/sample/config/WebClientConfig.java +++ b/samples/boot/oauth2/authcodegrant/src/main/java/sample/config/WebClientConfig.java @@ -17,6 +17,7 @@ package sample.config; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.client.web.reactive.function.client.OAuth2AuthorizedClientExchangeFilterFunction; import org.springframework.web.reactive.function.client.WebClient; @@ -24,6 +25,7 @@ * @author Rob Winch * @since 5.1 */ +@Configuration public class WebClientConfig { @Bean diff --git a/samples/boot/oauth2login-webflux/src/main/java/sample/WebClientConfig.java b/samples/boot/oauth2login-webflux/src/main/java/sample/WebClientConfig.java index 1d0ed4a2b4f..b5a96fe648a 100644 --- a/samples/boot/oauth2login-webflux/src/main/java/sample/WebClientConfig.java +++ b/samples/boot/oauth2login-webflux/src/main/java/sample/WebClientConfig.java @@ -17,6 +17,7 @@ package sample; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.client.web.reactive.function.client.OAuth2AuthorizedClientExchangeFilterFunction; import org.springframework.web.reactive.function.client.WebClient; @@ -24,6 +25,7 @@ * @author Rob Winch * @since 5.1 */ +@Configuration public class WebClientConfig { @Bean diff --git a/samples/boot/oauth2login/src/main/java/sample/WebClientConfig.java b/samples/boot/oauth2login/src/main/java/sample/WebClientConfig.java index 1d0ed4a2b4f..b5a96fe648a 100644 --- a/samples/boot/oauth2login/src/main/java/sample/WebClientConfig.java +++ b/samples/boot/oauth2login/src/main/java/sample/WebClientConfig.java @@ -17,6 +17,7 @@ package sample; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.client.web.reactive.function.client.OAuth2AuthorizedClientExchangeFilterFunction; import org.springframework.web.reactive.function.client.WebClient; @@ -24,6 +25,7 @@ * @author Rob Winch * @since 5.1 */ +@Configuration public class WebClientConfig { @Bean From 515159e2903c9ca2015e86593e50fee433e8e89f Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 25 May 2018 15:17:08 -0500 Subject: [PATCH 033/226] Add WebClient Bearer token support Fixes: gh-5389 --- .../DefaultReactiveOAuth2UserService.java | 5 ++- ...uthorizedClientExchangeFilterFunction.java | 16 +++---- .../security/web/http/SecurityHeaders.java | 43 +++++++++++++++++++ .../web/http/SecurityHeadersTests.java | 42 ++++++++++++++++++ 4 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 web/src/main/java/org/springframework/security/web/http/SecurityHeaders.java create mode 100644 web/src/test/groovy/org/springframework/security/web/http/SecurityHeadersTests.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java index 90a79a7bf80..1f9bc10c147 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java @@ -16,6 +16,8 @@ package org.springframework.security.oauth2.client.userinfo; +import static org.springframework.security.web.http.SecurityHeaders.bearerToken; + import java.net.UnknownHostException; import java.util.HashSet; import java.util.Map; @@ -99,8 +101,7 @@ public Mono loadUser(OAuth2UserRequest userRequest) Mono> userAttributes = this.webClient.get() .uri(userInfoUri) - .header(HttpHeaders.AUTHORIZATION, - "Bearer " + userRequest.getAccessToken().getTokenValue()) + .headers(bearerToken(userRequest.getAccessToken().getTokenValue())) .retrieve() .onStatus(s -> s != HttpStatus.OK, response -> { return parse(response).map(userInfoErrorResponse -> { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunction.java index 178868c39c1..8df207778aa 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunction.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunction.java @@ -16,17 +16,19 @@ package org.springframework.security.oauth2.client.web.reactive.function.client; -import org.springframework.http.HttpHeaders; +import static org.springframework.security.web.http.SecurityHeaders.bearerToken; + +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; + import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.ExchangeFunction; -import reactor.core.publisher.Mono; -import java.util.Map; -import java.util.Optional; -import java.util.function.Consumer; +import reactor.core.publisher.Mono; /** * Provides an easy mechanism for using an {@link OAuth2AuthorizedClient} to make OAuth2 requests by including the @@ -77,8 +79,4 @@ private ClientRequest bearer(ClientRequest request, OAuth2AuthorizedClient autho .headers(bearerToken(authorizedClient.getAccessToken().getTokenValue())) .build(); } - - private Consumer bearerToken(String token) { - return headers -> headers.set(HttpHeaders.AUTHORIZATION, "Bearer " + token); - } } diff --git a/web/src/main/java/org/springframework/security/web/http/SecurityHeaders.java b/web/src/main/java/org/springframework/security/web/http/SecurityHeaders.java new file mode 100644 index 00000000000..ef373724fcf --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/http/SecurityHeaders.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.http; + +import org.springframework.http.HttpHeaders; +import org.springframework.util.Assert; + +import java.util.function.Consumer; + +/** + * Utilities for interacting with {@link HttpHeaders} + * + * @author Rob Winch + * @since 5.1 + */ +public final class SecurityHeaders { + + /** + * Sets the provided value as a Bearer token in a header with the name of {@link HttpHeaders#AUTHORIZATION} + * @param bearerTokenValue the bear token value + * @return a {@link Consumer} that sets the header. + */ + public static Consumer bearerToken(String bearerTokenValue) { + Assert.hasText(bearerTokenValue, "bearerTokenValue cannot be null"); + return headers -> headers.set(HttpHeaders.AUTHORIZATION, "Bearer " + bearerTokenValue); + } + + private SecurityHeaders() {} +} diff --git a/web/src/test/groovy/org/springframework/security/web/http/SecurityHeadersTests.java b/web/src/test/groovy/org/springframework/security/web/http/SecurityHeadersTests.java new file mode 100644 index 00000000000..164a4ba39f6 --- /dev/null +++ b/web/src/test/groovy/org/springframework/security/web/http/SecurityHeadersTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.http; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.*; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class SecurityHeadersTests { + + @Test + public void bearerTokenWhenNullThenIllegalArgumentException() { + String bearerTokenValue = null; + assertThatThrownBy(() -> SecurityHeaders.bearerToken(bearerTokenValue)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void bearerTokenWhenEmptyStringThenIllegalArgumentException() { + assertThatThrownBy(() -> SecurityHeaders.bearerToken("")) + .isInstanceOf(IllegalArgumentException.class); + } + +} From 945f6e4a124c84f1d4a03003cf7f516aa5271a9c Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 29 May 2018 09:50:14 -0400 Subject: [PATCH 034/226] DefaultLoginPageGeneratingFilter escapes OAuth2 ClientRegistrations Fixes gh-5394 --- .../ui/DefaultLoginPageGeneratingFilter.java | 3 +- ...DefaultLoginPageGeneratingFilterTests.java | 36 ++++++++++++++----- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java index a0d683e5300..74c0ae67965 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java @@ -22,6 +22,7 @@ import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices; import org.springframework.util.Assert; import org.springframework.web.filter.GenericFilterBean; +import org.springframework.web.util.HtmlUtils; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -286,7 +287,7 @@ private String generateLoginPageHtml(HttpServletRequest request, boolean loginEr for (Map.Entry clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) { sb.append(" "); sb.append(""); - sb.append(clientAuthenticationUrlToClientName.getValue()); + sb.append(HtmlUtils.htmlEscape(clientAuthenticationUrlToClientName.getValue(), "UTF-8")); sb.append(""); sb.append("\n"); } diff --git a/web/src/test/java/org/springframework/security/web/authentication/DefaultLoginPageGeneratingFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/DefaultLoginPageGeneratingFilterTests.java index 4f7b3ca9a41..ec7abb46640 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/DefaultLoginPageGeneratingFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/DefaultLoginPageGeneratingFilterTests.java @@ -15,6 +15,16 @@ */ package org.springframework.security.web.authentication; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import java.util.Collections; +import java.util.Locale; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import org.junit.Test; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.mock.web.MockHttpServletRequest; @@ -26,15 +36,6 @@ import org.springframework.security.web.WebAttributes; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; -import javax.servlet.FilterChain; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.util.Collections; -import java.util.Locale; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - /** * * @author Luke Taylor @@ -187,4 +188,21 @@ public void handlesNonIso8859CharsInErrorMessage() throws Exception { filter.doFilter(request, new MockHttpServletResponse(), chain); } + + // gh-5394 + @Test + public void generatesForOAuth2LoginAndEscapesClientName() throws Exception { + DefaultLoginPageGeneratingFilter filter = new DefaultLoginPageGeneratingFilter(); + filter.setLoginPageUrl(DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL); + filter.setOauth2LoginEnabled(true); + + String clientName = "Google < > \" \' &"; + filter.setOauth2AuthenticationUrlToClientName( + Collections.singletonMap("/oauth2/authorization/google", clientName)); + + MockHttpServletResponse response = new MockHttpServletResponse(); + filter.doFilter(new MockHttpServletRequest("GET", "/login"), response, chain); + + assertThat(response.getContentAsString()).contains("Google < > " ' &"); + } } From 52f95fafb59e263943772e45d42b54b1993eb023 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 29 May 2018 21:10:34 -0400 Subject: [PATCH 035/226] Add OAuth2AuthenticationException constructor that takes only OAuth2Error Fixes gh-5374 --- .../oauth2/core/OAuth2AuthenticationException.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthenticationException.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthenticationException.java index ab0b8c050c4..3229b04b002 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthenticationException.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthenticationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,15 @@ public class OAuth2AuthenticationException extends AuthenticationException { private OAuth2Error error; + /** + * Constructs an {@code OAuth2AuthenticationException} using the provided parameters. + * + * @param error the {@link OAuth2Error OAuth 2.0 Error} + */ + public OAuth2AuthenticationException(OAuth2Error error) { + this(error, error.getDescription()); + } + /** * Constructs an {@code OAuth2AuthenticationException} using the provided parameters. * From 3d691506759884408aab6e2ebfa1ad4fce7504a8 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 29 May 2018 21:29:33 -0400 Subject: [PATCH 036/226] OAuth2ClientArgumentResolver uses AnnotatedElementUtils Fixes gh-5335 --- .../web/method/annotation/OAuth2ClientArgumentResolver.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2ClientArgumentResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2ClientArgumentResolver.java index 1c0b99d95a9..695af81b540 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2ClientArgumentResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2ClientArgumentResolver.java @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.client.web.method.annotation; import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.security.core.Authentication; @@ -90,7 +91,7 @@ public boolean supportsParameter(MethodParameter parameter) { return ((OAuth2AccessToken.class.isAssignableFrom(parameterType) || OAuth2AuthorizedClient.class.isAssignableFrom(parameterType) || ClientRegistration.class.isAssignableFrom(parameterType)) && - (parameter.hasParameterAnnotation(OAuth2Client.class))); + (AnnotatedElementUtils.findMergedAnnotation(parameter.getParameter(), OAuth2Client.class) != null)); } @NonNull @@ -100,7 +101,8 @@ public Object resolveArgument(MethodParameter parameter, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { - OAuth2Client oauth2ClientAnnotation = parameter.getParameterAnnotation(OAuth2Client.class); + OAuth2Client oauth2ClientAnnotation = AnnotatedElementUtils.findMergedAnnotation( + parameter.getParameter(), OAuth2Client.class); Authentication principal = SecurityContextHolder.getContext().getAuthentication(); String clientRegistrationId = null; From 2a5ab9b67421fd2a2af3057107af49ac6c699b42 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Wed, 30 May 2018 11:29:28 -0400 Subject: [PATCH 037/226] Simplify oauth2login sample Fixes gh-5384 --- ...g-security-samples-boot-oauth2login.gradle | 2 - .../samples/OAuth2LoginApplicationTests.java | 3 -- .../java/sample/OAuth2LoginApplication.java | 6 +-- .../src/main/java/sample/WebClientConfig.java | 37 ------------------ .../sample/web/OAuth2LoginController.java | 39 ++++--------------- .../src/main/resources/templates/index.html | 7 +++- .../main/resources/templates/userinfo.html | 19 --------- 7 files changed, 14 insertions(+), 99 deletions(-) delete mode 100644 samples/boot/oauth2login/src/main/java/sample/WebClientConfig.java delete mode 100644 samples/boot/oauth2login/src/main/resources/templates/userinfo.html diff --git a/samples/boot/oauth2login/spring-security-samples-boot-oauth2login.gradle b/samples/boot/oauth2login/spring-security-samples-boot-oauth2login.gradle index beee079bd41..01bce8a118f 100644 --- a/samples/boot/oauth2login/spring-security-samples-boot-oauth2login.gradle +++ b/samples/boot/oauth2login/spring-security-samples-boot-oauth2login.gradle @@ -6,11 +6,9 @@ dependencies { compile project(':spring-security-config') compile project(':spring-security-oauth2-client') compile project(':spring-security-oauth2-jose') - compile 'org.springframework:spring-webflux' compile 'org.springframework.boot:spring-boot-starter-thymeleaf' compile 'org.springframework.boot:spring-boot-starter-web' compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4' - compile 'io.projectreactor.ipc:reactor-netty' testCompile project(':spring-security-test') testCompile 'net.sourceforge.htmlunit:htmlunit' diff --git a/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java b/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java index 612e3583c92..1c722da290c 100644 --- a/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java +++ b/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java @@ -32,7 +32,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -58,7 +57,6 @@ import org.springframework.test.context.junit4.SpringRunner; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; -import sample.WebClientConfig; import java.net.URI; import java.net.URL; @@ -403,7 +401,6 @@ private OAuth2UserService mockUserService() { @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(basePackages = "sample.web") - @Import(WebClientConfig.class) public static class SpringBootApplicationTestConfig { @Autowired diff --git a/samples/boot/oauth2login/src/main/java/sample/OAuth2LoginApplication.java b/samples/boot/oauth2login/src/main/java/sample/OAuth2LoginApplication.java index 7ec72679feb..14b247827f3 100644 --- a/samples/boot/oauth2login/src/main/java/sample/OAuth2LoginApplication.java +++ b/samples/boot/oauth2login/src/main/java/sample/OAuth2LoginApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,11 +24,7 @@ @SpringBootApplication public class OAuth2LoginApplication { - public OAuth2LoginApplication() { - } - public static void main(String[] args) { SpringApplication.run(OAuth2LoginApplication.class, args); } - } diff --git a/samples/boot/oauth2login/src/main/java/sample/WebClientConfig.java b/samples/boot/oauth2login/src/main/java/sample/WebClientConfig.java deleted file mode 100644 index b5a96fe648a..00000000000 --- a/samples/boot/oauth2login/src/main/java/sample/WebClientConfig.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2002-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package sample; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.client.web.reactive.function.client.OAuth2AuthorizedClientExchangeFilterFunction; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * @author Rob Winch - * @since 5.1 - */ -@Configuration -public class WebClientConfig { - - @Bean - WebClient webClient() { - return WebClient.builder() - .filter(new OAuth2AuthorizedClientExchangeFilterFunction()) - .build(); - } -} diff --git a/samples/boot/oauth2login/src/main/java/sample/web/OAuth2LoginController.java b/samples/boot/oauth2login/src/main/java/sample/web/OAuth2LoginController.java index 5c29e84daea..867cd3703b9 100644 --- a/samples/boot/oauth2login/src/main/java/sample/web/OAuth2LoginController.java +++ b/samples/boot/oauth2login/src/main/java/sample/web/OAuth2LoginController.java @@ -15,18 +15,13 @@ */ package sample.web; -import static org.springframework.security.oauth2.client.web.reactive.function.client.OAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient; - -import java.util.Collections; -import java.util.Map; - +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.annotation.OAuth2Client; +import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.reactive.function.client.WebClient; /** * @author Joe Grandja @@ -34,34 +29,14 @@ */ @Controller public class OAuth2LoginController { - private final WebClient webClient; - - public OAuth2LoginController(WebClient webClient) { - this.webClient = webClient; - } @GetMapping("/") - public String index(Model model, @OAuth2Client OAuth2AuthorizedClient authorizedClient) { - model.addAttribute("userName", authorizedClient.getPrincipalName()); + public String index(Model model, + @OAuth2Client OAuth2AuthorizedClient authorizedClient, + @AuthenticationPrincipal OAuth2User oauth2User) { + model.addAttribute("userName", oauth2User.getName()); model.addAttribute("clientName", authorizedClient.getClientRegistration().getClientName()); + model.addAttribute("userAttributes", oauth2User.getAttributes()); return "index"; } - - @GetMapping("/userinfo") - public String userinfo(Model model, @OAuth2Client OAuth2AuthorizedClient authorizedClient) { - Map userAttributes = Collections.emptyMap(); - String userInfoEndpointUri = authorizedClient.getClientRegistration() - .getProviderDetails().getUserInfoEndpoint().getUri(); - if (!StringUtils.isEmpty(userInfoEndpointUri)) { // userInfoEndpointUri is optional for OIDC Clients - userAttributes = this.webClient - .get() - .uri(userInfoEndpointUri) - .attributes(oauth2AuthorizedClient(authorizedClient)) - .retrieve() - .bodyToMono(Map.class) - .block(); - } - model.addAttribute("userAttributes", userAttributes); - return "userinfo"; - } } diff --git a/samples/boot/oauth2login/src/main/resources/templates/index.html b/samples/boot/oauth2login/src/main/resources/templates/index.html index c30b73de69c..c5a54504d8c 100644 --- a/samples/boot/oauth2login/src/main/resources/templates/index.html +++ b/samples/boot/oauth2login/src/main/resources/templates/index.html @@ -23,7 +23,12 @@

    OAuth 2.0 Login with Spring Security

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

    OAuth 2.0 User Info

    -
    - User Attributes: -
      -
    • - : -
    • -
    -
    - - From 333a2215ea65a1d07a5d48c4794e5acd3be22741 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Wed, 30 May 2018 11:53:25 -0400 Subject: [PATCH 038/226] Simplify oauth2login-webflux sample Fixes gh-5396 --- ...ty-samples-boot-oauth2login-webflux.gradle | 1 - .../src/main/java/sample/WebClientConfig.java | 37 ------------------ .../sample/web/OAuth2LoginController.java | 39 ++++--------------- .../src/main/resources/templates/index.html | 7 +++- .../main/resources/templates/userinfo.html | 38 ------------------ 5 files changed, 13 insertions(+), 109 deletions(-) delete mode 100644 samples/boot/oauth2login-webflux/src/main/java/sample/WebClientConfig.java delete mode 100644 samples/boot/oauth2login-webflux/src/main/resources/templates/userinfo.html diff --git a/samples/boot/oauth2login-webflux/spring-security-samples-boot-oauth2login-webflux.gradle b/samples/boot/oauth2login-webflux/spring-security-samples-boot-oauth2login-webflux.gradle index 933d9079e9e..88e6d285d8d 100644 --- a/samples/boot/oauth2login-webflux/spring-security-samples-boot-oauth2login-webflux.gradle +++ b/samples/boot/oauth2login-webflux/spring-security-samples-boot-oauth2login-webflux.gradle @@ -9,7 +9,6 @@ dependencies { compile 'org.springframework.boot:spring-boot-starter-thymeleaf' compile 'org.springframework.boot:spring-boot-starter-webflux' compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4' - compile 'io.projectreactor.ipc:reactor-netty' testCompile project(':spring-security-test') testCompile 'net.sourceforge.htmlunit:htmlunit' diff --git a/samples/boot/oauth2login-webflux/src/main/java/sample/WebClientConfig.java b/samples/boot/oauth2login-webflux/src/main/java/sample/WebClientConfig.java deleted file mode 100644 index b5a96fe648a..00000000000 --- a/samples/boot/oauth2login-webflux/src/main/java/sample/WebClientConfig.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2002-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package sample; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.client.web.reactive.function.client.OAuth2AuthorizedClientExchangeFilterFunction; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * @author Rob Winch - * @since 5.1 - */ -@Configuration -public class WebClientConfig { - - @Bean - WebClient webClient() { - return WebClient.builder() - .filter(new OAuth2AuthorizedClientExchangeFilterFunction()) - .build(); - } -} diff --git a/samples/boot/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java b/samples/boot/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java index 9c86d73bdce..d781489baa6 100644 --- a/samples/boot/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java +++ b/samples/boot/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java @@ -16,52 +16,27 @@ package sample.web; -import static org.springframework.security.oauth2.client.web.reactive.function.client.OAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient; - -import java.util.Map; - +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.annotation.OAuth2Client; +import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.reactive.function.client.WebClient; - -import reactor.core.publisher.Mono; /** * @author Rob Winch */ @Controller public class OAuth2LoginController { - private final WebClient webClient; - - public OAuth2LoginController(WebClient webClient) { - this.webClient = webClient; - } @GetMapping("/") - public String index(Model model, @OAuth2Client OAuth2AuthorizedClient authorizedClient) { - model.addAttribute("userName", authorizedClient.getPrincipalName()); + public String index(Model model, + @OAuth2Client OAuth2AuthorizedClient authorizedClient, + @AuthenticationPrincipal OAuth2User oauth2User) { + model.addAttribute("userName", oauth2User.getName()); model.addAttribute("clientName", authorizedClient.getClientRegistration().getClientName()); + model.addAttribute("userAttributes", oauth2User.getAttributes()); return "index"; } - - @GetMapping("/userinfo") - public String userinfo(Model model, @OAuth2Client OAuth2AuthorizedClient authorizedClient) { - Mono userAttributes = Mono.empty(); - String userInfoEndpointUri = authorizedClient.getClientRegistration() - .getProviderDetails().getUserInfoEndpoint().getUri(); - if (!StringUtils.isEmpty(userInfoEndpointUri)) { // userInfoEndpointUri is optional for OIDC Clients - userAttributes = this.webClient - .get() - .uri(userInfoEndpointUri) - .attributes(oauth2AuthorizedClient(authorizedClient)) - .retrieve() - .bodyToMono(Map.class); - } - model.addAttribute("userAttributes", userAttributes); - return "userinfo"; - } } diff --git a/samples/boot/oauth2login-webflux/src/main/resources/templates/index.html b/samples/boot/oauth2login-webflux/src/main/resources/templates/index.html index 19165ae1c3f..bbb2fab3f17 100644 --- a/samples/boot/oauth2login-webflux/src/main/resources/templates/index.html +++ b/samples/boot/oauth2login-webflux/src/main/resources/templates/index.html @@ -40,7 +40,12 @@

    OAuth 2.0 Login with Spring Security

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

    OAuth 2.0 User Info

    -
    - User Attributes: -
      -
    • - : -
    • -
    -
    - - From 17302aa6eb32cd9b2673824f94febf29f0b10225 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Wed, 30 May 2018 12:07:31 -0400 Subject: [PATCH 039/226] Move oauth2login samples under oauth2 directory Fixes gh-5397 --- docs/manual/src/docs/asciidoc/_includes/preface/guides.adoc | 4 ++-- .../docs/asciidoc/_includes/preface/java-configuration.adoc | 2 +- samples/boot/{ => oauth2}/oauth2login-webflux/README.adoc | 0 ...g-security-samples-boot-oauth2-oauth2login-webflux.gradle} | 0 .../oauth2/client/ReactiveOAuth2ClientAutoConfiguration.java | 0 ...activeOAuth2ClientRegistrationRepositoryConfiguration.java | 0 .../oauth2/client/ReactiveOAuth2WebSecurityConfiguration.java | 0 .../autoconfigure/security/oauth2/client/package-info.java | 0 .../src/main/java/sample/ReactiveOAuth2LoginApplication.java | 0 .../src/main/java/sample/web/OAuth2LoginController.java | 0 .../src/main/resources/META-INF/spring.factories | 0 .../oauth2login-webflux/src/main/resources/application.yml | 0 .../src/main/resources/templates/index.html | 0 samples/boot/{ => oauth2}/oauth2login/README.adoc | 0 .../spring-security-samples-boot-oauth2-oauth2login.gradle} | 0 .../security/samples/OAuth2LoginApplicationTests.java | 0 .../src/main/java/sample/OAuth2LoginApplication.java | 0 .../src/main/java/sample/web/OAuth2LoginController.java | 0 .../oauth2login/src/main/resources/application.yml | 0 .../oauth2login/src/main/resources/templates/index.html | 0 20 files changed, 3 insertions(+), 3 deletions(-) rename samples/boot/{ => oauth2}/oauth2login-webflux/README.adoc (100%) rename samples/boot/{oauth2login-webflux/spring-security-samples-boot-oauth2login-webflux.gradle => oauth2/oauth2login-webflux/spring-security-samples-boot-oauth2-oauth2login-webflux.gradle} (100%) rename samples/boot/{ => oauth2}/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientAutoConfiguration.java (100%) rename samples/boot/{ => oauth2}/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientRegistrationRepositoryConfiguration.java (100%) rename samples/boot/{ => oauth2}/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2WebSecurityConfiguration.java (100%) rename samples/boot/{ => oauth2}/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/package-info.java (100%) rename samples/boot/{ => oauth2}/oauth2login-webflux/src/main/java/sample/ReactiveOAuth2LoginApplication.java (100%) rename samples/boot/{ => oauth2}/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java (100%) rename samples/boot/{ => oauth2}/oauth2login-webflux/src/main/resources/META-INF/spring.factories (100%) rename samples/boot/{ => oauth2}/oauth2login-webflux/src/main/resources/application.yml (100%) rename samples/boot/{ => oauth2}/oauth2login-webflux/src/main/resources/templates/index.html (100%) rename samples/boot/{ => oauth2}/oauth2login/README.adoc (100%) rename samples/boot/{oauth2login/spring-security-samples-boot-oauth2login.gradle => oauth2/oauth2login/spring-security-samples-boot-oauth2-oauth2login.gradle} (100%) rename samples/boot/{ => oauth2}/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java (100%) rename samples/boot/{ => oauth2}/oauth2login/src/main/java/sample/OAuth2LoginApplication.java (100%) rename samples/boot/{ => oauth2}/oauth2login/src/main/java/sample/web/OAuth2LoginController.java (100%) rename samples/boot/{ => oauth2}/oauth2login/src/main/resources/application.yml (100%) rename samples/boot/{ => oauth2}/oauth2login/src/main/resources/templates/index.html (100%) diff --git a/docs/manual/src/docs/asciidoc/_includes/preface/guides.adoc b/docs/manual/src/docs/asciidoc/_includes/preface/guides.adoc index 87490678866..d6cf49dda5a 100644 --- a/docs/manual/src/docs/asciidoc/_includes/preface/guides.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/preface/guides.adoc @@ -27,8 +27,8 @@ If you are looking to get started with Spring Security, the best place to start | Demonstrates how to create a custom login form. | link:../../guides/html5/form-javaconfig.html[Custom Login Form Guide] -| {gh-samples-url}/boot/oauth2login[OAuth 2.0 Login] +| {gh-samples-url}/boot/oauth2/oauth2login[OAuth 2.0 Login] | Demonstrates how to integrate OAuth 2.0 Login with an OAuth 2.0 or OpenID Connect 1.0 Provider. -| link:{gh-samples-url}/boot/oauth2login/README.adoc[OAuth 2.0 Login Guide] +| link:{gh-samples-url}/boot/oauth2/oauth2login/README.adoc[OAuth 2.0 Login Guide] |=== diff --git a/docs/manual/src/docs/asciidoc/_includes/preface/java-configuration.adoc b/docs/manual/src/docs/asciidoc/_includes/preface/java-configuration.adoc index 4f2d9ebbeff..7b5f78e9e71 100644 --- a/docs/manual/src/docs/asciidoc/_includes/preface/java-configuration.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/preface/java-configuration.adoc @@ -466,7 +466,7 @@ NOTE: OAuth 2.0 Login is implemented by using the *Authorization Code Grant*, as Spring Boot 2.0 brings full auto-configuration capabilities for OAuth 2.0 Login. -This section shows how to configure the {gh-samples-url}/boot/oauth2login[*OAuth 2.0 Login sample*] using _Google_ as the _Authentication Provider_ and covers the following topics: +This section shows how to configure the {gh-samples-url}/boot/oauth2/oauth2login[*OAuth 2.0 Login sample*] using _Google_ as the _Authentication Provider_ and covers the following topics: * <> * <> diff --git a/samples/boot/oauth2login-webflux/README.adoc b/samples/boot/oauth2/oauth2login-webflux/README.adoc similarity index 100% rename from samples/boot/oauth2login-webflux/README.adoc rename to samples/boot/oauth2/oauth2login-webflux/README.adoc diff --git a/samples/boot/oauth2login-webflux/spring-security-samples-boot-oauth2login-webflux.gradle b/samples/boot/oauth2/oauth2login-webflux/spring-security-samples-boot-oauth2-oauth2login-webflux.gradle similarity index 100% rename from samples/boot/oauth2login-webflux/spring-security-samples-boot-oauth2login-webflux.gradle rename to samples/boot/oauth2/oauth2login-webflux/spring-security-samples-boot-oauth2-oauth2login-webflux.gradle diff --git a/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientAutoConfiguration.java b/samples/boot/oauth2/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientAutoConfiguration.java similarity index 100% rename from samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientAutoConfiguration.java rename to samples/boot/oauth2/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientAutoConfiguration.java diff --git a/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientRegistrationRepositoryConfiguration.java b/samples/boot/oauth2/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientRegistrationRepositoryConfiguration.java similarity index 100% rename from samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientRegistrationRepositoryConfiguration.java rename to samples/boot/oauth2/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientRegistrationRepositoryConfiguration.java diff --git a/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2WebSecurityConfiguration.java b/samples/boot/oauth2/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2WebSecurityConfiguration.java similarity index 100% rename from samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2WebSecurityConfiguration.java rename to samples/boot/oauth2/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2WebSecurityConfiguration.java diff --git a/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/package-info.java b/samples/boot/oauth2/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/package-info.java similarity index 100% rename from samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/package-info.java rename to samples/boot/oauth2/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/package-info.java diff --git a/samples/boot/oauth2login-webflux/src/main/java/sample/ReactiveOAuth2LoginApplication.java b/samples/boot/oauth2/oauth2login-webflux/src/main/java/sample/ReactiveOAuth2LoginApplication.java similarity index 100% rename from samples/boot/oauth2login-webflux/src/main/java/sample/ReactiveOAuth2LoginApplication.java rename to samples/boot/oauth2/oauth2login-webflux/src/main/java/sample/ReactiveOAuth2LoginApplication.java diff --git a/samples/boot/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java b/samples/boot/oauth2/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java similarity index 100% rename from samples/boot/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java rename to samples/boot/oauth2/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java diff --git a/samples/boot/oauth2login-webflux/src/main/resources/META-INF/spring.factories b/samples/boot/oauth2/oauth2login-webflux/src/main/resources/META-INF/spring.factories similarity index 100% rename from samples/boot/oauth2login-webflux/src/main/resources/META-INF/spring.factories rename to samples/boot/oauth2/oauth2login-webflux/src/main/resources/META-INF/spring.factories diff --git a/samples/boot/oauth2login-webflux/src/main/resources/application.yml b/samples/boot/oauth2/oauth2login-webflux/src/main/resources/application.yml similarity index 100% rename from samples/boot/oauth2login-webflux/src/main/resources/application.yml rename to samples/boot/oauth2/oauth2login-webflux/src/main/resources/application.yml diff --git a/samples/boot/oauth2login-webflux/src/main/resources/templates/index.html b/samples/boot/oauth2/oauth2login-webflux/src/main/resources/templates/index.html similarity index 100% rename from samples/boot/oauth2login-webflux/src/main/resources/templates/index.html rename to samples/boot/oauth2/oauth2login-webflux/src/main/resources/templates/index.html diff --git a/samples/boot/oauth2login/README.adoc b/samples/boot/oauth2/oauth2login/README.adoc similarity index 100% rename from samples/boot/oauth2login/README.adoc rename to samples/boot/oauth2/oauth2login/README.adoc diff --git a/samples/boot/oauth2login/spring-security-samples-boot-oauth2login.gradle b/samples/boot/oauth2/oauth2login/spring-security-samples-boot-oauth2-oauth2login.gradle similarity index 100% rename from samples/boot/oauth2login/spring-security-samples-boot-oauth2login.gradle rename to samples/boot/oauth2/oauth2login/spring-security-samples-boot-oauth2-oauth2login.gradle diff --git a/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java b/samples/boot/oauth2/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java similarity index 100% rename from samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java rename to samples/boot/oauth2/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java diff --git a/samples/boot/oauth2login/src/main/java/sample/OAuth2LoginApplication.java b/samples/boot/oauth2/oauth2login/src/main/java/sample/OAuth2LoginApplication.java similarity index 100% rename from samples/boot/oauth2login/src/main/java/sample/OAuth2LoginApplication.java rename to samples/boot/oauth2/oauth2login/src/main/java/sample/OAuth2LoginApplication.java diff --git a/samples/boot/oauth2login/src/main/java/sample/web/OAuth2LoginController.java b/samples/boot/oauth2/oauth2login/src/main/java/sample/web/OAuth2LoginController.java similarity index 100% rename from samples/boot/oauth2login/src/main/java/sample/web/OAuth2LoginController.java rename to samples/boot/oauth2/oauth2login/src/main/java/sample/web/OAuth2LoginController.java diff --git a/samples/boot/oauth2login/src/main/resources/application.yml b/samples/boot/oauth2/oauth2login/src/main/resources/application.yml similarity index 100% rename from samples/boot/oauth2login/src/main/resources/application.yml rename to samples/boot/oauth2/oauth2login/src/main/resources/application.yml diff --git a/samples/boot/oauth2login/src/main/resources/templates/index.html b/samples/boot/oauth2/oauth2login/src/main/resources/templates/index.html similarity index 100% rename from samples/boot/oauth2login/src/main/resources/templates/index.html rename to samples/boot/oauth2/oauth2login/src/main/resources/templates/index.html From 33badf6416d2397fad4d40de6ae6b8d677613040 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Wed, 30 May 2018 17:37:45 -0600 Subject: [PATCH 040/226] SecurityContextHolderAwareRequestConfig groovy->java Issue: gh-4939 --- ...ontextHolderAwareRequestConfigTests.groovy | 171 ---------- ...yContextHolderAwareRequestConfigTests.java | 302 ++++++++++++++++++ ...olderAwareRequestConfigTests-FormLogin.xml | 35 ++ ...olderAwareRequestConfigTests-HttpBasic.xml | 39 +++ ...xtHolderAwareRequestConfigTests-Logout.xml | 36 +++ ...olderAwareRequestConfigTests-MultiHttp.xml | 56 ++++ ...xtHolderAwareRequestConfigTests-Simple.xml | 34 ++ 7 files changed, 502 insertions(+), 171 deletions(-) delete mode 100644 config/src/test/groovy/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.groovy create mode 100644 config/src/test/java/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.java create mode 100644 config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-FormLogin.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-HttpBasic.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-Logout.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-MultiHttp.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-Simple.xml diff --git a/config/src/test/groovy/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.groovy deleted file mode 100644 index de68710f253..00000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.groovy +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright 2002-2012 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.config.http - -import static org.springframework.security.config.ConfigTestUtils.AUTH_PROVIDER_XML - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse - -import org.springframework.mock.web.MockFilterChain -import org.springframework.mock.web.MockHttpServletRequest -import org.springframework.mock.web.MockHttpServletResponse -import org.springframework.security.authentication.TestingAuthenticationToken -import org.springframework.security.core.context.SecurityContext -import org.springframework.security.core.context.SecurityContextHolder -import org.springframework.security.web.access.ExceptionTranslationFilter -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter -import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler -import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler -import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint -import org.springframework.security.web.authentication.www.BasicAuthenticationFilter -import org.springframework.security.web.context.HttpSessionSecurityContextRepository -import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter - -/** - * - * @author Rob Winch - */ -class SecurityContextHolderAwareRequestConfigTests extends AbstractHttpConfigTests { - - def withAutoConfig() { - httpAutoConfig () { - csrf(disabled:true) - } - createAppContext(AUTH_PROVIDER_XML) - - def securityContextAwareFilter = getFilter(SecurityContextHolderAwareRequestFilter) - - expect: - securityContextAwareFilter.authenticationEntryPoint.loginFormUrl == getFilter(ExceptionTranslationFilter).authenticationEntryPoint.loginFormUrl - securityContextAwareFilter.authenticationManager == getFilter(UsernamePasswordAuthenticationFilter).authenticationManager - securityContextAwareFilter.logoutHandlers.size() == 1 - securityContextAwareFilter.logoutHandlers[0].class == SecurityContextLogoutHandler - } - - def explicitEntryPoint() { - xml.http() { - 'http-basic'('entry-point-ref': 'ep') - } - bean('ep', BasicAuthenticationEntryPoint.class.name, ['realmName':'whocares'],[:]) - createAppContext(AUTH_PROVIDER_XML) - - def securityContextAwareFilter = getFilter(SecurityContextHolderAwareRequestFilter) - - expect: - securityContextAwareFilter.authenticationEntryPoint == getFilter(ExceptionTranslationFilter).authenticationEntryPoint - securityContextAwareFilter.authenticationManager == getFilter(BasicAuthenticationFilter).authenticationManager - securityContextAwareFilter.logoutHandlers == null - } - - def formLogin() { - xml.http() { - 'form-login'() - } - createAppContext(AUTH_PROVIDER_XML) - - def securityContextAwareFilter = getFilter(SecurityContextHolderAwareRequestFilter) - - expect: - securityContextAwareFilter.authenticationEntryPoint.loginFormUrl == getFilter(ExceptionTranslationFilter).authenticationEntryPoint.loginFormUrl - securityContextAwareFilter.authenticationManager == getFilter(UsernamePasswordAuthenticationFilter).authenticationManager - securityContextAwareFilter.logoutHandlers == null - } - - def multiHttp() { - xml.http('authentication-manager-ref' : 'authManager', 'pattern' : '/first/**') { - 'form-login'('login-page' : '/login') - 'logout'('invalidate-session' : 'true') - csrf(disabled:true) - } - xml.http('authentication-manager-ref' : 'authManager2') { - 'form-login'('login-page' : '/login2') - 'logout'('invalidate-session' : 'false') - csrf(disabled:true) - } - - String secondAuthManager = AUTH_PROVIDER_XML.replace("alias='authManager'", "id='authManager2'") - createAppContext(AUTH_PROVIDER_XML + secondAuthManager) - - def securityContextAwareFilter = getFilters('/first/filters').find { it instanceof SecurityContextHolderAwareRequestFilter } - def secondSecurityContextAwareFilter = getFilter(SecurityContextHolderAwareRequestFilter) - - expect: - securityContextAwareFilter.authenticationEntryPoint.loginFormUrl == '/login' - securityContextAwareFilter.authenticationManager == getFilters('/first/filters').find { it instanceof UsernamePasswordAuthenticationFilter}.authenticationManager - securityContextAwareFilter.authenticationManager.parent == appContext.getBean('authManager') - securityContextAwareFilter.logoutHandlers.size() == 1 - securityContextAwareFilter.logoutHandlers[0].class == SecurityContextLogoutHandler - securityContextAwareFilter.logoutHandlers[0].invalidateHttpSession == true - - secondSecurityContextAwareFilter.authenticationEntryPoint.loginFormUrl == '/login2' - secondSecurityContextAwareFilter.authenticationManager == getFilter(UsernamePasswordAuthenticationFilter).authenticationManager - secondSecurityContextAwareFilter.authenticationManager.parent == appContext.getBean('authManager2') - securityContextAwareFilter.logoutHandlers.size() == 1 - secondSecurityContextAwareFilter.logoutHandlers[0].class == SecurityContextLogoutHandler - secondSecurityContextAwareFilter.logoutHandlers[0].invalidateHttpSession == false - } - - def logoutCustom() { - xml.http() { - 'form-login'('login-page' : '/login') - 'logout'('invalidate-session' : 'false', 'logout-success-url' : '/login?logout', 'delete-cookies' : 'JSESSIONID') - csrf(disabled:true) - } - createAppContext(AUTH_PROVIDER_XML) - - def securityContextAwareFilter = getFilter(SecurityContextHolderAwareRequestFilter) - - expect: - securityContextAwareFilter.authenticationEntryPoint.loginFormUrl == getFilter(ExceptionTranslationFilter).authenticationEntryPoint.loginFormUrl - securityContextAwareFilter.authenticationManager == getFilter(UsernamePasswordAuthenticationFilter).authenticationManager - securityContextAwareFilter.logoutHandlers.size() == 2 - securityContextAwareFilter.logoutHandlers[0].class == SecurityContextLogoutHandler - securityContextAwareFilter.logoutHandlers[0].invalidateHttpSession == false - securityContextAwareFilter.logoutHandlers[1].class == CookieClearingLogoutHandler - securityContextAwareFilter.logoutHandlers[1].cookiesToClear == ['JSESSIONID'] - } - - def 'SEC-2926: Role Prefix is set'() { - setup: - httpAutoConfig () { - - } - createAppContext(AUTH_PROVIDER_XML) - - MockFilterChain chain = new MockFilterChain() { - public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { - assert request.isUserInRole("USER") - - super.doFilter(request,response) - } - } - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - SecurityContext context = SecurityContextHolder.createEmptyContext() - context.setAuthentication(new TestingAuthenticationToken("user", "pass", "ROLE_USER")) - request.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, context) - - when: - springSecurityFilterChain.doFilter(request, new MockHttpServletResponse(), chain) - then: - chain.request != null - } -} diff --git a/config/src/test/java/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.java b/config/src/test/java/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.java new file mode 100644 index 00000000000..af22e07fc4d --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.java @@ -0,0 +1,302 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.http; + +import org.apache.http.HttpHeaders; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.core.StringContains.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * + * @author Rob Winch + * @author Josh Cummings + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SecurityTestExecutionListeners +public class SecurityContextHolderAwareRequestConfigTests { + + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests"; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + private MockMvc mvc; + + @Test + public void servletLoginWhenUsingDefaultConfigurationThenUsesSpringSecurity() + throws Exception { + + this.spring.configLocations(this.xml("Simple")).autowire(); + + this.mvc.perform(get("/good-login")) + .andExpect(status().isOk()) + .andExpect(content().string("user")); + } + + @Test + public void servletAuthenticateWhenUsingDefaultConfigurationThenUsesSpringSecurity() + throws Exception { + + this.spring.configLocations(this.xml("Simple")).autowire(); + + this.mvc.perform(get("/authenticate")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login")); + } + + @Test + public void servletLogoutWhenUsingDefaultConfigurationThenUsesSpringSecurity() + throws Exception { + + this.spring.configLocations(this.xml("Simple")).autowire(); + + MvcResult result = this.mvc.perform(get("/good-login")).andReturn(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); + + assertThat(session).isNotNull(); + + result = this.mvc.perform(get("/do-logout").session(session)) + .andExpect(status().isOk()) + .andExpect(content().string("")) + .andReturn(); + + session = (MockHttpSession) result.getRequest().getSession(false); + + assertThat(session).isNull(); + } + + @Test + public void servletAuthenticateWhenUsingHttpBasicThenUsesSpringSecurity() + throws Exception { + + this.spring.configLocations(this.xml("HttpBasic")).autowire(); + + this.mvc.perform(get("/authenticate")) + .andExpect(status().isUnauthorized()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, containsString("discworld"))); + } + + @Test + public void servletAuthenticateWhenUsingFormLoginThenUsesSpringSecurity() + throws Exception { + + this.spring.configLocations(this.xml("FormLogin")).autowire(); + + this.mvc.perform(get("/authenticate")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login")); + } + + @Test + public void servletLoginWhenUsingMultipleHttpConfigsThenUsesSpringSecurity() + throws Exception { + + this.spring.configLocations(this.xml("MultiHttp")).autowire(); + + this.mvc.perform(get("/good-login")) + .andExpect(status().isOk()) + .andExpect(content().string("user")); + + this.mvc.perform(get("/v2/good-login")) + .andExpect(status().isOk()) + .andExpect(content().string("user2")); + } + + @Test + public void servletAuthenticateWhenUsingMultipleHttpConfigsThenUsesSpringSecurity() + throws Exception { + + this.spring.configLocations(this.xml("MultiHttp")).autowire(); + + this.mvc.perform(get("/authenticate")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login")); + + this.mvc.perform(get("/v2/authenticate")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login2")); + + } + + @Test + public void servletLogoutWhenUsingMultipleHttpConfigsThenUsesSpringSecurity() + throws Exception { + + this.spring.configLocations(this.xml("MultiHttp")).autowire(); + + MvcResult result = this.mvc.perform(get("/good-login")).andReturn(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); + + assertThat(session).isNotNull(); + + result = this.mvc.perform(get("/do-logout").session(session)) + .andExpect(status().isOk()) + .andExpect(content().string("")) + .andReturn(); + + session = (MockHttpSession) result.getRequest().getSession(false); + + assertThat(session).isNotNull(); + + result = this.mvc.perform(get("/v2/good-login")).andReturn(); + + session = (MockHttpSession) result.getRequest().getSession(false); + + assertThat(session).isNotNull(); + + result = this.mvc.perform(get("/v2/do-logout").session(session)) + .andExpect(status().isOk()) + .andExpect(content().string("")) + .andReturn(); + + session = (MockHttpSession) result.getRequest().getSession(false); + + assertThat(session).isNull(); + } + + @Test + public void servletLogoutWhenUsingCustomLogoutThenUsesSpringSecurity() + throws Exception { + + this.spring.configLocations(this.xml("Logout")).autowire(); + + this.mvc.perform(get("/authenticate")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/signin")); + + MvcResult result = this.mvc.perform(get("/good-login")).andReturn(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); + + assertThat(session).isNotNull(); + + result = this.mvc.perform(get("/do-logout").session(session)) + .andExpect(status().isOk()) + .andExpect(content().string("")) + .andExpect(cookie().maxAge("JSESSIONID", 0)) + .andReturn(); + + session = (MockHttpSession) result.getRequest().getSession(false); + + assertThat(session).isNotNull(); + } + + /** + * SEC-2926: Role Prefix is set + */ + @Test + @WithMockUser + public void servletIsUserInRoleWhenUsingDefaultConfigThenRoleIsSet() + throws Exception { + + this.spring.configLocations(this.xml("Simple")).autowire(); + + this.mvc.perform(get("/role")).andExpect(content().string("true")); + } + + @RestController + public static class ServletAuthenticatedController { + @GetMapping("/v2/good-login") + public String v2Login(HttpServletRequest request) throws ServletException { + + request.login("user2", "password2"); + + return this.principal(); + } + + @GetMapping("/good-login") + public String login(HttpServletRequest request) throws ServletException { + + request.login("user", "password"); + + return this.principal(); + } + + @GetMapping("/v2/authenticate") + public String v2Authenticate(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + + return this.authenticate(request, response); + } + + @GetMapping("/authenticate") + public String authenticate(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + + request.authenticate(response); + + return this.principal(); + } + + @GetMapping("/v2/do-logout") + public String v2Logout(HttpServletRequest request) throws ServletException { + return this.logout(request); + } + + @GetMapping("/do-logout") + public String logout(HttpServletRequest request) throws ServletException { + request.logout(); + + return this.principal(); + } + + @GetMapping("/role") + public String role(HttpServletRequest request) { + return String.valueOf(request.isUserInRole("USER")); + } + + private String principal() { + if ( SecurityContextHolder.getContext().getAuthentication() != null ) { + return SecurityContextHolder.getContext().getAuthentication().getName(); + } + return null; + } + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-FormLogin.xml b/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-FormLogin.xml new file mode 100644 index 00000000000..0caf67fac90 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-FormLogin.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-HttpBasic.xml b/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-HttpBasic.xml new file mode 100644 index 00000000000..b89d96a67ae --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-HttpBasic.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-Logout.xml b/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-Logout.xml new file mode 100644 index 00000000000..d2b0532fe16 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-Logout.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-MultiHttp.xml b/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-MultiHttp.xml new file mode 100644 index 00000000000..38d74133a7e --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-MultiHttp.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-Simple.xml b/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-Simple.xml new file mode 100644 index 00000000000..8d8e2473b1f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-Simple.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + From 895821f666335c3b02ba1dd76b0b311dda1a68b4 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Thu, 31 May 2018 16:13:16 -0400 Subject: [PATCH 041/226] OidcUserService leverages DefaultOAuth2UserService Fixes gh-5390 --- .../NimbusUserInfoResponseClient.java | 168 ------------------ .../client/oidc/userinfo/OidcUserService.java | 15 +- .../oidc/userinfo/OidcUserServiceTests.java | 2 + 3 files changed, 9 insertions(+), 176 deletions(-) delete mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/NimbusUserInfoResponseClient.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/NimbusUserInfoResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/NimbusUserInfoResponseClient.java deleted file mode 100644 index c63d905ec21..00000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/NimbusUserInfoResponseClient.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2002-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.oauth2.client.oidc.userinfo; - -import com.nimbusds.oauth2.sdk.ErrorObject; -import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.http.HTTPRequest; -import com.nimbusds.oauth2.sdk.http.HTTPResponse; -import com.nimbusds.oauth2.sdk.token.BearerAccessToken; -import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse; -import com.nimbusds.openid.connect.sdk.UserInfoRequest; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.client.AbstractClientHttpResponse; -import org.springframework.http.client.ClientHttpResponse; -import org.springframework.http.converter.GenericHttpMessageConverter; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.security.authentication.AuthenticationServiceException; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.core.OAuth2AccessToken; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.OAuth2Error; -import org.springframework.util.Assert; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.nio.charset.Charset; - -/** - * NOTE: This is a straight copy of org.springframework.security.oauth2.client.userinfo.NimbusUserInfoResponseClient - * - * @author Joe Grandja - * @since 5.0 - */ -final class NimbusUserInfoResponseClient { - private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response"; - private final GenericHttpMessageConverter genericHttpMessageConverter = new MappingJackson2HttpMessageConverter(); - - T getUserInfoResponse(OAuth2UserRequest userInfoRequest, Class returnType) throws OAuth2AuthenticationException { - ClientHttpResponse userInfoResponse = this.getUserInfoResponse( - userInfoRequest.getClientRegistration(), userInfoRequest.getAccessToken()); - try { - return (T) this.genericHttpMessageConverter.read(returnType, userInfoResponse); - } catch (IOException | HttpMessageNotReadableException ex) { - OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, - "An error occurred reading the UserInfo Success response: " + ex.getMessage(), null); - throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex); - } - } - - T getUserInfoResponse(OAuth2UserRequest userInfoRequest, ParameterizedTypeReference typeReference) throws OAuth2AuthenticationException { - ClientHttpResponse userInfoResponse = this.getUserInfoResponse( - userInfoRequest.getClientRegistration(), userInfoRequest.getAccessToken()); - try { - return (T) this.genericHttpMessageConverter.read(typeReference.getType(), null, userInfoResponse); - } catch (IOException | HttpMessageNotReadableException ex) { - OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, - "An error occurred reading the UserInfo Success response: " + ex.getMessage(), null); - throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex); - } - } - - private ClientHttpResponse getUserInfoResponse(ClientRegistration clientRegistration, - OAuth2AccessToken oauth2AccessToken) throws OAuth2AuthenticationException { - URI userInfoUri = URI.create(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri()); - BearerAccessToken accessToken = new BearerAccessToken(oauth2AccessToken.getTokenValue()); - - UserInfoRequest userInfoRequest = new UserInfoRequest(userInfoUri, accessToken); - HTTPRequest httpRequest = userInfoRequest.toHTTPRequest(); - httpRequest.setAccept(MediaType.APPLICATION_JSON_VALUE); - httpRequest.setConnectTimeout(30000); - httpRequest.setReadTimeout(30000); - HTTPResponse httpResponse; - - try { - httpResponse = httpRequest.send(); - } catch (IOException ex) { - throw new AuthenticationServiceException("An error occurred while sending the UserInfo Request: " + - ex.getMessage(), ex); - } - - if (httpResponse.getStatusCode() == HTTPResponse.SC_OK) { - return new NimbusClientHttpResponse(httpResponse); - } - - UserInfoErrorResponse userInfoErrorResponse; - try { - userInfoErrorResponse = UserInfoErrorResponse.parse(httpResponse); - } catch (ParseException ex) { - OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, - "An error occurred parsing the UserInfo Error response: " + ex.getMessage(), null); - throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex); - } - ErrorObject errorObject = userInfoErrorResponse.getErrorObject(); - - StringBuilder errorDescription = new StringBuilder(); - errorDescription.append("An error occurred while attempting to access the UserInfo Endpoint -> "); - errorDescription.append("Error details: ["); - errorDescription.append("UserInfo Uri: ").append(userInfoUri.toString()); - errorDescription.append(", Http Status: ").append(errorObject.getHTTPStatusCode()); - if (errorObject.getCode() != null) { - errorDescription.append(", Error Code: ").append(errorObject.getCode()); - } - if (errorObject.getDescription() != null) { - errorDescription.append(", Error Description: ").append(errorObject.getDescription()); - } - errorDescription.append("]"); - - OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, errorDescription.toString(), null); - throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); - } - - private static class NimbusClientHttpResponse extends AbstractClientHttpResponse { - private final HTTPResponse httpResponse; - private final HttpHeaders headers; - - private NimbusClientHttpResponse(HTTPResponse httpResponse) { - Assert.notNull(httpResponse, "httpResponse cannot be null"); - this.httpResponse = httpResponse; - this.headers = new HttpHeaders(); - this.headers.setAll(httpResponse.getHeaders()); - } - - @Override - public int getRawStatusCode() throws IOException { - return this.httpResponse.getStatusCode(); - } - - @Override - public String getStatusText() throws IOException { - return String.valueOf(this.getRawStatusCode()); - } - - @Override - public void close() { - } - - @Override - public InputStream getBody() throws IOException { - InputStream inputStream = new ByteArrayInputStream( - this.httpResponse.getContent().getBytes(Charset.forName("UTF-8"))); - return inputStream; - } - - @Override - public HttpHeaders getHeaders() { - return this.headers; - } - } -} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java index e354608c3db..28eb750fd9e 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,9 @@ */ package org.springframework.security.oauth2.client.oidc.userinfo; -import org.springframework.core.ParameterizedTypeReference; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; @@ -26,12 +27,12 @@ import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import java.util.Arrays; import java.util.HashSet; -import java.util.Map; import java.util.Set; /** @@ -49,17 +50,15 @@ public class OidcUserService implements OAuth2UserService userInfoScopes = new HashSet<>( Arrays.asList(OidcScopes.PROFILE, OidcScopes.EMAIL, OidcScopes.ADDRESS, OidcScopes.PHONE)); - private NimbusUserInfoResponseClient userInfoResponseClient = new NimbusUserInfoResponseClient(); + private final OAuth2UserService defaultUserService = new DefaultOAuth2UserService(); @Override public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { Assert.notNull(userRequest, "userRequest cannot be null"); OidcUserInfo userInfo = null; if (this.shouldRetrieveUserInfo(userRequest)) { - ParameterizedTypeReference> typeReference = - new ParameterizedTypeReference>() {}; - Map userAttributes = this.userInfoResponseClient.getUserInfoResponse(userRequest, typeReference); - userInfo = new OidcUserInfo(userAttributes); + OAuth2User oauth2User = this.defaultUserService.loadUser(userRequest); + userInfo = new OidcUserInfo(oauth2User.getAttributes()); // http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse // Due to the possibility of token substitution attacks (see Section 16.11), diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java index ba08c8163b0..7ac59eb954f 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java @@ -79,6 +79,8 @@ public void setUp() throws Exception { when(this.providerDetails.getUserInfoEndpoint()).thenReturn(this.userInfoEndpoint); when(this.clientRegistration.getAuthorizationGrantType()).thenReturn(AuthorizationGrantType.AUTHORIZATION_CODE); + when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn(StandardClaimNames.SUB); + this.accessToken = mock(OAuth2AccessToken.class); Set authorizedScopes = new LinkedHashSet<>(Arrays.asList(OidcScopes.OPENID, OidcScopes.PROFILE)); when(this.accessToken.getScopes()).thenReturn(authorizedScopes); From 578b863dc91ff1b3010506b3ccf5e315302782dd Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Fri, 1 Jun 2018 11:01:37 -0400 Subject: [PATCH 042/226] Fix snapshot build Fixes gh-5402 --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 8703b270ffc..8cbd437f754 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -59,7 +59,7 @@ try { node { checkout scm try { - sh "./gradlew clean test -PspringVersion='5.+' -PreactorVersion=Bismuth-BUILD-SNAPSHOT -PspringDataVersion=Kay-BUILD-SNAPSHOT --refresh-dependencies --no-daemon --stacktrace" + sh "./gradlew clean test -PspringVersion='5.0.+' -PreactorVersion=Bismuth-BUILD-SNAPSHOT -PspringDataVersion=Kay-BUILD-SNAPSHOT --refresh-dependencies --no-daemon --stacktrace" } catch(Exception e) { currentBuild.result = 'FAILED: snapshots' throw e From 8fa048b63d064bcd105df6de927e981edc7e90b6 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 5 Jun 2018 10:27:08 -0500 Subject: [PATCH 043/226] Update WebFlux samples to use Spring Boot Fixes: gh-5411 --- ...ty-samples-boot-hellowebflux-method.gradle | 12 +++ .../HelloWebfluxMethodApplicationITests.java | 73 ++++++++---------- .../sample/HelloWebfluxMethodApplication.java | 32 ++++++++ .../java/sample/HelloWorldMessageService.java | 0 .../main/java/sample/MessageController.java | 0 .../src/main/java/sample/SecurityConfig.java | 0 .../HelloWebfluxMethodApplicationTests.java | 35 ++++----- .../sample/HelloWorldMessageServiceTests.java | 6 +- ...-security-samples-boot-hellowebflux.gradle | 11 +++ .../sample/HelloWebfluxApplicationITests.java | 31 +++----- .../main/java/sample/HelloUserController.java | 0 .../java/sample/HelloWebfluxApplication.java | 33 ++++++++ .../sample/HelloWebfluxSecurityConfig.java | 0 .../sample/HelloWebfluxApplicationTests.java | 37 ++++----- ...ecurity-samples-boot-hellowebfluxfn.gradle | 11 +++ .../HelloWebfluxFnApplicationITests.java | 30 +++----- .../main/java/sample/HelloUserController.java | 0 .../sample/HelloWebfluxFnApplication.java | 44 +++++++++++ .../sample/HelloWebfluxFnSecurityConfig.java | 0 .../HelloWebfluxFnApplicationTests.java | 41 +++++----- ...security-samples-boot-webflux-form.gradle} | 13 ++-- .../sample/WebfluxFormApplicationTests.java | 14 ++-- .../java/sample/webdriver/IndexPage.java | 0 .../java/sample/webdriver/LoginPage.java | 0 .../java/sample/CsrfControllerAdvice.java | 0 .../src/main/java/sample/IndexController.java | 0 .../java/sample/WebfluxFormApplication.java | 32 ++++++++ .../sample/WebfluxFormSecurityConfig.java | 0 .../src/main/resources/logback.xml | 0 .../src/main/resources/templates/index.html | 0 .../src/main/resources/templates/login.html | 0 ...ples-javaconfig-hellowebflux-method.gradle | 16 ---- .../sample/HelloWebfluxMethodApplication.java | 56 -------------- ...ity-samples-javaconfig-hellowebflux.gradle | 17 ----- .../java/sample/HelloWebfluxApplication.java | 55 -------------- ...y-samples-javaconfig-hellowebfluxfn.gradle | 16 ---- .../sample/HelloWebfluxFnApplication.java | 75 ------------------ .../src/main/java/sample/ThymeleafConfig.java | 76 ------------------- .../java/sample/WebfluxFormApplication.java | 56 -------------- 39 files changed, 286 insertions(+), 536 deletions(-) create mode 100644 samples/boot/hellowebflux-method/spring-security-samples-boot-hellowebflux-method.gradle rename samples/{javaconfig => boot}/hellowebflux-method/src/integration-test/java/sample/HelloWebfluxMethodApplicationITests.java (56%) create mode 100644 samples/boot/hellowebflux-method/src/main/java/sample/HelloWebfluxMethodApplication.java rename samples/{javaconfig => boot}/hellowebflux-method/src/main/java/sample/HelloWorldMessageService.java (100%) rename samples/{javaconfig => boot}/hellowebflux-method/src/main/java/sample/MessageController.java (100%) rename samples/{javaconfig => boot}/hellowebflux-method/src/main/java/sample/SecurityConfig.java (100%) rename samples/{javaconfig => boot}/hellowebflux-method/src/test/java/sample/HelloWebfluxMethodApplicationTests.java (89%) rename samples/{javaconfig => boot}/hellowebflux-method/src/test/java/sample/HelloWorldMessageServiceTests.java (88%) create mode 100644 samples/boot/hellowebflux/spring-security-samples-boot-hellowebflux.gradle rename samples/{javaconfig => boot}/hellowebflux/src/integration-test/java/sample/HelloWebfluxApplicationITests.java (73%) rename samples/{javaconfig => boot}/hellowebflux/src/main/java/sample/HelloUserController.java (100%) create mode 100644 samples/boot/hellowebflux/src/main/java/sample/HelloWebfluxApplication.java rename samples/{javaconfig => boot}/hellowebflux/src/main/java/sample/HelloWebfluxSecurityConfig.java (100%) rename samples/{javaconfig => boot}/hellowebflux/src/test/java/sample/HelloWebfluxApplicationTests.java (87%) create mode 100644 samples/boot/hellowebfluxfn/spring-security-samples-boot-hellowebfluxfn.gradle rename samples/{javaconfig => boot}/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationITests.java (73%) rename samples/{javaconfig => boot}/hellowebfluxfn/src/main/java/sample/HelloUserController.java (100%) create mode 100644 samples/boot/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java rename samples/{javaconfig => boot}/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnSecurityConfig.java (100%) rename samples/{javaconfig => boot}/hellowebfluxfn/src/test/java/sample/HelloWebfluxFnApplicationTests.java (81%) rename samples/{javaconfig/webflux-form/spring-security-samples-javaconfig-webflux-form.gradle => boot/webflux-form/spring-security-samples-boot-webflux-form.gradle} (74%) rename samples/{javaconfig => boot}/webflux-form/src/integration-test/java/sample/WebfluxFormApplicationTests.java (84%) rename samples/{javaconfig => boot}/webflux-form/src/integration-test/java/sample/webdriver/IndexPage.java (100%) rename samples/{javaconfig => boot}/webflux-form/src/integration-test/java/sample/webdriver/LoginPage.java (100%) rename samples/{javaconfig => boot}/webflux-form/src/main/java/sample/CsrfControllerAdvice.java (100%) rename samples/{javaconfig => boot}/webflux-form/src/main/java/sample/IndexController.java (100%) create mode 100644 samples/boot/webflux-form/src/main/java/sample/WebfluxFormApplication.java rename samples/{javaconfig => boot}/webflux-form/src/main/java/sample/WebfluxFormSecurityConfig.java (100%) rename samples/{javaconfig => boot}/webflux-form/src/main/resources/logback.xml (100%) rename samples/{javaconfig => boot}/webflux-form/src/main/resources/templates/index.html (100%) rename samples/{javaconfig => boot}/webflux-form/src/main/resources/templates/login.html (100%) delete mode 100644 samples/javaconfig/hellowebflux-method/spring-security-samples-javaconfig-hellowebflux-method.gradle delete mode 100644 samples/javaconfig/hellowebflux-method/src/main/java/sample/HelloWebfluxMethodApplication.java delete mode 100644 samples/javaconfig/hellowebflux/spring-security-samples-javaconfig-hellowebflux.gradle delete mode 100644 samples/javaconfig/hellowebflux/src/main/java/sample/HelloWebfluxApplication.java delete mode 100644 samples/javaconfig/hellowebfluxfn/spring-security-samples-javaconfig-hellowebfluxfn.gradle delete mode 100644 samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java delete mode 100644 samples/javaconfig/webflux-form/src/main/java/sample/ThymeleafConfig.java delete mode 100644 samples/javaconfig/webflux-form/src/main/java/sample/WebfluxFormApplication.java diff --git a/samples/boot/hellowebflux-method/spring-security-samples-boot-hellowebflux-method.gradle b/samples/boot/hellowebflux-method/spring-security-samples-boot-hellowebflux-method.gradle new file mode 100644 index 00000000000..c21781806f8 --- /dev/null +++ b/samples/boot/hellowebflux-method/spring-security-samples-boot-hellowebflux-method.gradle @@ -0,0 +1,12 @@ +apply plugin: 'io.spring.convention.spring-sample-boot' + +dependencies { + compile project(':spring-security-core') + compile project(':spring-security-config') + compile project(':spring-security-web') + compile 'org.springframework.boot:spring-boot-starter-webflux' + + testCompile project(':spring-security-test') + testCompile 'io.projectreactor:reactor-test' + testCompile 'org.springframework.boot:spring-boot-starter-test' +} diff --git a/samples/javaconfig/hellowebflux-method/src/integration-test/java/sample/HelloWebfluxMethodApplicationITests.java b/samples/boot/hellowebflux-method/src/integration-test/java/sample/HelloWebfluxMethodApplicationITests.java similarity index 56% rename from samples/javaconfig/hellowebflux-method/src/integration-test/java/sample/HelloWebfluxMethodApplicationITests.java rename to samples/boot/hellowebflux-method/src/integration-test/java/sample/HelloWebfluxMethodApplicationITests.java index 813614f4bf5..ff56fb8c1d8 100644 --- a/samples/javaconfig/hellowebflux-method/src/integration-test/java/sample/HelloWebfluxMethodApplicationITests.java +++ b/samples/boot/hellowebflux-method/src/integration-test/java/sample/HelloWebfluxMethodApplicationITests.java @@ -15,75 +15,65 @@ */ package sample; -import org.junit.Before; +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; + +import java.util.Map; +import java.util.function.Consumer; + import org.junit.Test; import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpStatus; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; - -import java.nio.charset.Charset; -import java.time.Duration; -import java.util.Base64; -import java.util.Map; -import java.util.function.Consumer; - -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; +import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; /** * @author Rob Winch * @since 5.0 */ @RunWith(SpringRunner.class) -@ContextConfiguration(classes = HelloWebfluxMethodApplication.class) -@TestPropertySource(properties = "server.port=0") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class HelloWebfluxMethodApplicationITests { - @Value("#{@nettyContext.address().getPort()}") - int port; WebTestClient rest; - @Before - public void setup() { - this.rest = WebTestClient.bindToServer() - .filter(basicAuthentication()) - .responseTimeout(Duration.ofDays(1)) - .baseUrl("http://localhost:" + this.port) - .build(); + @Autowired + public void setRest(WebTestClient rest) { + this.rest = rest + .mutateWith((b, h, c) -> b.filter(ExchangeFilterFunctions.basicAuthentication())); } + @Test public void messageWhenNotAuthenticated() throws Exception { this.rest - .get() - .uri("/message") - .exchange() - .expectStatus().isUnauthorized(); + .get() + .uri("/message") + .exchange() + .expectStatus().isUnauthorized(); } @Test public void messageWhenUserThenForbidden() throws Exception { this.rest - .get() - .uri("/message") - .attributes(robsCredentials()) - .exchange() - .expectStatus().isEqualTo(HttpStatus.FORBIDDEN); + .get() + .uri("/message") + .attributes(robsCredentials()) + .exchange() + .expectStatus().isEqualTo(HttpStatus.FORBIDDEN); } @Test public void messageWhenAdminThenOk() throws Exception { this.rest - .get() - .uri("/message") - .attributes(adminCredentials()) - .exchange() - .expectStatus().isOk() - .expectBody(String.class).isEqualTo("Hello World!"); + .get() + .uri("/message") + .attributes(adminCredentials()) + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("Hello World!"); } private Consumer> robsCredentials() { @@ -93,8 +83,5 @@ private Consumer> robsCredentials() { private Consumer> adminCredentials() { return basicAuthenticationCredentials("admin", "admin"); } - - private String base64Encode(String value) { - return Base64.getEncoder().encodeToString(value.getBytes(Charset.defaultCharset())); - } } + diff --git a/samples/boot/hellowebflux-method/src/main/java/sample/HelloWebfluxMethodApplication.java b/samples/boot/hellowebflux-method/src/main/java/sample/HelloWebfluxMethodApplication.java new file mode 100644 index 00000000000..85aa92bb61b --- /dev/null +++ b/samples/boot/hellowebflux-method/src/main/java/sample/HelloWebfluxMethodApplication.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Rob Winch + * @since 5.0 + */ +@SpringBootApplication +public class HelloWebfluxMethodApplication { + + public static void main(String[] args) { + SpringApplication.run(HelloWebfluxMethodApplication.class, args); + } +} diff --git a/samples/javaconfig/hellowebflux-method/src/main/java/sample/HelloWorldMessageService.java b/samples/boot/hellowebflux-method/src/main/java/sample/HelloWorldMessageService.java similarity index 100% rename from samples/javaconfig/hellowebflux-method/src/main/java/sample/HelloWorldMessageService.java rename to samples/boot/hellowebflux-method/src/main/java/sample/HelloWorldMessageService.java diff --git a/samples/javaconfig/hellowebflux-method/src/main/java/sample/MessageController.java b/samples/boot/hellowebflux-method/src/main/java/sample/MessageController.java similarity index 100% rename from samples/javaconfig/hellowebflux-method/src/main/java/sample/MessageController.java rename to samples/boot/hellowebflux-method/src/main/java/sample/MessageController.java diff --git a/samples/javaconfig/hellowebflux-method/src/main/java/sample/SecurityConfig.java b/samples/boot/hellowebflux-method/src/main/java/sample/SecurityConfig.java similarity index 100% rename from samples/javaconfig/hellowebflux-method/src/main/java/sample/SecurityConfig.java rename to samples/boot/hellowebflux-method/src/main/java/sample/SecurityConfig.java diff --git a/samples/javaconfig/hellowebflux-method/src/test/java/sample/HelloWebfluxMethodApplicationTests.java b/samples/boot/hellowebflux-method/src/test/java/sample/HelloWebfluxMethodApplicationTests.java similarity index 89% rename from samples/javaconfig/hellowebflux-method/src/test/java/sample/HelloWebfluxMethodApplicationTests.java rename to samples/boot/hellowebflux-method/src/test/java/sample/HelloWebfluxMethodApplicationTests.java index 88e2c629c3c..a167da26ff4 100644 --- a/samples/javaconfig/hellowebflux-method/src/test/java/sample/HelloWebfluxMethodApplicationTests.java +++ b/samples/boot/hellowebflux-method/src/test/java/sample/HelloWebfluxMethodApplicationTests.java @@ -15,48 +15,41 @@ */ package sample; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; + import java.util.Map; import java.util.function.Consumer; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; - import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser; -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; - /** * @author Rob Winch * @since 5.0 */ @RunWith(SpringRunner.class) -@ContextConfiguration(classes = HelloWebfluxMethodApplication.class) -@ActiveProfiles("test") +@SpringBootTest public class HelloWebfluxMethodApplicationTests { - @Autowired - ApplicationContext context; - WebTestClient rest; - @Before - public void setup() { + @Autowired + public void setup(ApplicationContext context) { this.rest = WebTestClient - .bindToApplicationContext(this.context) - .apply(springSecurity()) - .configureClient() - .filter(basicAuthentication()) - .build(); + .bindToApplicationContext(context) + .apply(springSecurity()) + .configureClient() + .filter(basicAuthentication()) + .build(); } @Test diff --git a/samples/javaconfig/hellowebflux-method/src/test/java/sample/HelloWorldMessageServiceTests.java b/samples/boot/hellowebflux-method/src/test/java/sample/HelloWorldMessageServiceTests.java similarity index 88% rename from samples/javaconfig/hellowebflux-method/src/test/java/sample/HelloWorldMessageServiceTests.java rename to samples/boot/hellowebflux-method/src/test/java/sample/HelloWorldMessageServiceTests.java index c33a0accafa..9989b9d030c 100644 --- a/samples/javaconfig/hellowebflux-method/src/test/java/sample/HelloWorldMessageServiceTests.java +++ b/samples/boot/hellowebflux-method/src/test/java/sample/HelloWorldMessageServiceTests.java @@ -19,10 +19,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import reactor.test.StepVerifier; @@ -31,8 +30,7 @@ * @since 5.0 */ @RunWith(SpringRunner.class) -@ContextConfiguration(classes = HelloWebfluxMethodApplication.class) -@ActiveProfiles("test") +@SpringBootTest public class HelloWorldMessageServiceTests { @Autowired HelloWorldMessageService messages; diff --git a/samples/boot/hellowebflux/spring-security-samples-boot-hellowebflux.gradle b/samples/boot/hellowebflux/spring-security-samples-boot-hellowebflux.gradle new file mode 100644 index 00000000000..d65d168ad11 --- /dev/null +++ b/samples/boot/hellowebflux/spring-security-samples-boot-hellowebflux.gradle @@ -0,0 +1,11 @@ +apply plugin: 'io.spring.convention.spring-sample-boot' + +dependencies { + compile project(':spring-security-core') + compile project(':spring-security-config') + compile project(':spring-security-web') + compile 'org.springframework.boot:spring-boot-starter-webflux' + + testCompile project(':spring-security-test') + testCompile 'org.springframework.boot:spring-boot-starter-test' +} diff --git a/samples/javaconfig/hellowebflux/src/integration-test/java/sample/HelloWebfluxApplicationITests.java b/samples/boot/hellowebflux/src/integration-test/java/sample/HelloWebfluxApplicationITests.java similarity index 73% rename from samples/javaconfig/hellowebflux/src/integration-test/java/sample/HelloWebfluxApplicationITests.java rename to samples/boot/hellowebflux/src/integration-test/java/sample/HelloWebfluxApplicationITests.java index bc1abf00ecc..0d6be6b71b7 100644 --- a/samples/javaconfig/hellowebflux/src/integration-test/java/sample/HelloWebfluxApplicationITests.java +++ b/samples/boot/hellowebflux/src/integration-test/java/sample/HelloWebfluxApplicationITests.java @@ -15,46 +15,35 @@ */ package sample; -import java.time.Duration; +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; + import java.util.Map; import java.util.function.Consumer; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; - -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; +import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; /** * @author Rob Winch * @since 5.0 */ @RunWith(SpringRunner.class) -@ContextConfiguration(classes = HelloWebfluxApplication.class) -@TestPropertySource(properties = "server.port=0") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class HelloWebfluxApplicationITests { - @Value("#{@nettyContext.address().getPort()}") - int port; WebTestClient rest; - @Before - public void setup() { - this.rest = WebTestClient.bindToServer() - .responseTimeout(Duration.ofDays(1)) - .baseUrl("http://localhost:" + this.port) - .filter(basicAuthentication()) - .build(); + @Autowired + public void setRest(WebTestClient rest) { + this.rest = rest + .mutateWith((b, h, c) -> b.filter(ExchangeFilterFunctions.basicAuthentication())); } - @Test public void basicWhenNoCredentialsThenUnauthorized() throws Exception { this.rest diff --git a/samples/javaconfig/hellowebflux/src/main/java/sample/HelloUserController.java b/samples/boot/hellowebflux/src/main/java/sample/HelloUserController.java similarity index 100% rename from samples/javaconfig/hellowebflux/src/main/java/sample/HelloUserController.java rename to samples/boot/hellowebflux/src/main/java/sample/HelloUserController.java diff --git a/samples/boot/hellowebflux/src/main/java/sample/HelloWebfluxApplication.java b/samples/boot/hellowebflux/src/main/java/sample/HelloWebfluxApplication.java new file mode 100644 index 00000000000..f47de63567f --- /dev/null +++ b/samples/boot/hellowebflux/src/main/java/sample/HelloWebfluxApplication.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Rob Winch + * @since 5.0 + */ +@SpringBootApplication +public class HelloWebfluxApplication { + + public static void main(String[] args) { + SpringApplication.run(HelloWebfluxApplication.class, args); + } + +} diff --git a/samples/javaconfig/hellowebflux/src/main/java/sample/HelloWebfluxSecurityConfig.java b/samples/boot/hellowebflux/src/main/java/sample/HelloWebfluxSecurityConfig.java similarity index 100% rename from samples/javaconfig/hellowebflux/src/main/java/sample/HelloWebfluxSecurityConfig.java rename to samples/boot/hellowebflux/src/main/java/sample/HelloWebfluxSecurityConfig.java diff --git a/samples/javaconfig/hellowebflux/src/test/java/sample/HelloWebfluxApplicationTests.java b/samples/boot/hellowebflux/src/test/java/sample/HelloWebfluxApplicationTests.java similarity index 87% rename from samples/javaconfig/hellowebflux/src/test/java/sample/HelloWebfluxApplicationTests.java rename to samples/boot/hellowebflux/src/test/java/sample/HelloWebfluxApplicationTests.java index e3a840b05e7..0c755510f7e 100644 --- a/samples/javaconfig/hellowebflux/src/test/java/sample/HelloWebfluxApplicationTests.java +++ b/samples/boot/hellowebflux/src/test/java/sample/HelloWebfluxApplicationTests.java @@ -15,47 +15,42 @@ */ package sample; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; + import java.util.Map; import java.util.function.Consumer; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; - import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ApplicationContext; import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser; -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; - /** * @author Rob Winch * @since 5.0 */ @RunWith(SpringRunner.class) -@ContextConfiguration(classes = HelloWebfluxApplication.class) -@ActiveProfiles("test") +@SpringBootTest +@AutoConfigureWebTestClient public class HelloWebfluxApplicationTests { - @Autowired - ApplicationContext context; - WebTestClient rest; - @Before - public void setup() { + @Autowired + public void setup(ApplicationContext context) { this.rest = WebTestClient - .bindToApplicationContext(this.context) - .apply(springSecurity()) - .configureClient() - .filter(basicAuthentication()) - .build(); + .bindToApplicationContext(context) + .apply(springSecurity()) + .configureClient() + .filter(basicAuthentication()) + .build(); } @Test diff --git a/samples/boot/hellowebfluxfn/spring-security-samples-boot-hellowebfluxfn.gradle b/samples/boot/hellowebfluxfn/spring-security-samples-boot-hellowebfluxfn.gradle new file mode 100644 index 00000000000..d65d168ad11 --- /dev/null +++ b/samples/boot/hellowebfluxfn/spring-security-samples-boot-hellowebfluxfn.gradle @@ -0,0 +1,11 @@ +apply plugin: 'io.spring.convention.spring-sample-boot' + +dependencies { + compile project(':spring-security-core') + compile project(':spring-security-config') + compile project(':spring-security-web') + compile 'org.springframework.boot:spring-boot-starter-webflux' + + testCompile project(':spring-security-test') + testCompile 'org.springframework.boot:spring-boot-starter-test' +} diff --git a/samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationITests.java b/samples/boot/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationITests.java similarity index 73% rename from samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationITests.java rename to samples/boot/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationITests.java index 280f8d7cc7a..c0e176f2b18 100644 --- a/samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationITests.java +++ b/samples/boot/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationITests.java @@ -15,43 +15,33 @@ */ package sample; -import java.time.Duration; +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; + import java.util.Map; import java.util.function.Consumer; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; - -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; +import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; /** * @author Rob Winch * @since 5.0 */ @RunWith(SpringRunner.class) -@ContextConfiguration(classes = HelloWebfluxFnApplication.class) -@TestPropertySource(properties = "server.port=0") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class HelloWebfluxFnApplicationITests { - @Value("#{@nettyContext.address().getPort()}") - int port; WebTestClient rest; - @Before - public void setup() { - this.rest = WebTestClient.bindToServer() - .responseTimeout(Duration.ofDays(1)) - .baseUrl("http://localhost:" + this.port) - .filter(basicAuthentication()) - .build(); + @Autowired + public void setRest(WebTestClient rest) { + this.rest = rest + .mutateWith((b, h, c) -> b.filter(ExchangeFilterFunctions.basicAuthentication())); } @Test diff --git a/samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloUserController.java b/samples/boot/hellowebfluxfn/src/main/java/sample/HelloUserController.java similarity index 100% rename from samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloUserController.java rename to samples/boot/hellowebfluxfn/src/main/java/sample/HelloUserController.java diff --git a/samples/boot/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java b/samples/boot/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java new file mode 100644 index 00000000000..bec73a10a5a --- /dev/null +++ b/samples/boot/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample; + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RouterFunctions.route; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; + +/** + * @author Rob Winch + * @since 5.0 + */ +@SpringBootApplication +public class HelloWebfluxFnApplication { + + public static void main(String[] args) { + SpringApplication.run(HelloWebfluxFnApplication.class, args); + } + + @Bean + public RouterFunction routes(HelloUserController userController) { + return route( + GET("/"), userController::hello); + } +} diff --git a/samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnSecurityConfig.java b/samples/boot/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnSecurityConfig.java similarity index 100% rename from samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnSecurityConfig.java rename to samples/boot/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnSecurityConfig.java diff --git a/samples/javaconfig/hellowebfluxfn/src/test/java/sample/HelloWebfluxFnApplicationTests.java b/samples/boot/hellowebfluxfn/src/test/java/sample/HelloWebfluxFnApplicationTests.java similarity index 81% rename from samples/javaconfig/hellowebfluxfn/src/test/java/sample/HelloWebfluxFnApplicationTests.java rename to samples/boot/hellowebfluxfn/src/test/java/sample/HelloWebfluxFnApplicationTests.java index ac549cf79da..0de4c718e12 100644 --- a/samples/javaconfig/hellowebfluxfn/src/test/java/sample/HelloWebfluxFnApplicationTests.java +++ b/samples/boot/hellowebfluxfn/src/test/java/sample/HelloWebfluxFnApplicationTests.java @@ -15,50 +15,43 @@ */ package sample; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; + import java.util.Map; import java.util.function.Consumer; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; - import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.security.web.server.WebFilterChainProxy; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.reactive.function.server.RouterFunction; - -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser; -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; /** * @author Rob Winch * @since 5.0 */ @RunWith(SpringRunner.class) -@ContextConfiguration(classes = HelloWebfluxFnApplication.class) -@ActiveProfiles("test") +@SpringBootTest +@AutoConfigureWebTestClient public class HelloWebfluxFnApplicationTests { - @Autowired - RouterFunction routerFunction; - @Autowired WebFilterChainProxy springSecurityFilterChain; WebTestClient rest; - @Before - public void setup() { + @Autowired + public void setup(ApplicationContext context) { this.rest = WebTestClient - .bindToRouterFunction(this.routerFunction) - .webFilter(this.springSecurityFilterChain) - .apply(springSecurity()) - .configureClient() - .filter(basicAuthentication()) - .build(); + .bindToApplicationContext(context) + .apply(springSecurity()) + .configureClient() + .filter(basicAuthentication()) + .build(); } @Test diff --git a/samples/javaconfig/webflux-form/spring-security-samples-javaconfig-webflux-form.gradle b/samples/boot/webflux-form/spring-security-samples-boot-webflux-form.gradle similarity index 74% rename from samples/javaconfig/webflux-form/spring-security-samples-javaconfig-webflux-form.gradle rename to samples/boot/webflux-form/spring-security-samples-boot-webflux-form.gradle index cf489639314..f42a169c657 100644 --- a/samples/javaconfig/webflux-form/spring-security-samples-javaconfig-webflux-form.gradle +++ b/samples/boot/webflux-form/spring-security-samples-boot-webflux-form.gradle @@ -14,24 +14,21 @@ * limitations under the License. */ -apply plugin: 'io.spring.convention.spring-sample' +apply plugin: 'io.spring.convention.spring-sample-boot' dependencies { compile project(':spring-security-core') compile project(':spring-security-config') compile project(':spring-security-web') - compile 'com.fasterxml.jackson.core:jackson-databind' - compile 'io.netty:netty-buffer' - compile 'io.projectreactor.ipc:reactor-netty' - compile 'org.springframework:spring-context' - compile 'org.springframework:spring-webflux' - compile 'org.thymeleaf:thymeleaf-spring5' - compile slf4jDependencies + compile 'org.springframework.boot:spring-boot-starter-thymeleaf' + compile 'org.springframework.boot:spring-boot-starter-webflux' testCompile project(':spring-security-test') + testCompile 'org.springframework.boot:spring-boot-starter-test' testCompile 'io.projectreactor:reactor-test' testCompile 'org.skyscreamer:jsonassert' testCompile 'org.springframework:spring-test' integrationTestCompile seleniumDependencies } + diff --git a/samples/javaconfig/webflux-form/src/integration-test/java/sample/WebfluxFormApplicationTests.java b/samples/boot/webflux-form/src/integration-test/java/sample/WebfluxFormApplicationTests.java similarity index 84% rename from samples/javaconfig/webflux-form/src/integration-test/java/sample/WebfluxFormApplicationTests.java rename to samples/boot/webflux-form/src/integration-test/java/sample/WebfluxFormApplicationTests.java index 7e6e1373452..b8bf628bc37 100644 --- a/samples/javaconfig/webflux-form/src/integration-test/java/sample/WebfluxFormApplicationTests.java +++ b/samples/boot/webflux-form/src/integration-test/java/sample/WebfluxFormApplicationTests.java @@ -15,16 +15,17 @@ */ package sample; -import com.gargoylesoftware.htmlunit.BrowserVersion; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.openqa.selenium.WebDriver; import org.openqa.selenium.htmlunit.HtmlUnitDriver; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; import org.springframework.test.context.junit4.SpringRunner; + +import com.gargoylesoftware.htmlunit.BrowserVersion; + import sample.webdriver.IndexPage; import sample.webdriver.LoginPage; @@ -33,12 +34,11 @@ * @since 5.0 */ @RunWith(SpringRunner.class) -@ContextConfiguration(classes = WebfluxFormApplication.class) -@TestPropertySource(properties = "server.port=0") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class WebfluxFormApplicationTests { WebDriver driver; - @Value("#{@nettyContext.address().getPort()}") + @LocalServerPort int port; @Before diff --git a/samples/javaconfig/webflux-form/src/integration-test/java/sample/webdriver/IndexPage.java b/samples/boot/webflux-form/src/integration-test/java/sample/webdriver/IndexPage.java similarity index 100% rename from samples/javaconfig/webflux-form/src/integration-test/java/sample/webdriver/IndexPage.java rename to samples/boot/webflux-form/src/integration-test/java/sample/webdriver/IndexPage.java diff --git a/samples/javaconfig/webflux-form/src/integration-test/java/sample/webdriver/LoginPage.java b/samples/boot/webflux-form/src/integration-test/java/sample/webdriver/LoginPage.java similarity index 100% rename from samples/javaconfig/webflux-form/src/integration-test/java/sample/webdriver/LoginPage.java rename to samples/boot/webflux-form/src/integration-test/java/sample/webdriver/LoginPage.java diff --git a/samples/javaconfig/webflux-form/src/main/java/sample/CsrfControllerAdvice.java b/samples/boot/webflux-form/src/main/java/sample/CsrfControllerAdvice.java similarity index 100% rename from samples/javaconfig/webflux-form/src/main/java/sample/CsrfControllerAdvice.java rename to samples/boot/webflux-form/src/main/java/sample/CsrfControllerAdvice.java diff --git a/samples/javaconfig/webflux-form/src/main/java/sample/IndexController.java b/samples/boot/webflux-form/src/main/java/sample/IndexController.java similarity index 100% rename from samples/javaconfig/webflux-form/src/main/java/sample/IndexController.java rename to samples/boot/webflux-form/src/main/java/sample/IndexController.java diff --git a/samples/boot/webflux-form/src/main/java/sample/WebfluxFormApplication.java b/samples/boot/webflux-form/src/main/java/sample/WebfluxFormApplication.java new file mode 100644 index 00000000000..9b93f1e9d48 --- /dev/null +++ b/samples/boot/webflux-form/src/main/java/sample/WebfluxFormApplication.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Rob Winch + * @since 5.0 + */ +@SpringBootApplication +public class WebfluxFormApplication { + + public static void main(String[] args) { + SpringApplication.run(WebfluxFormApplication.class, args); + } +} diff --git a/samples/javaconfig/webflux-form/src/main/java/sample/WebfluxFormSecurityConfig.java b/samples/boot/webflux-form/src/main/java/sample/WebfluxFormSecurityConfig.java similarity index 100% rename from samples/javaconfig/webflux-form/src/main/java/sample/WebfluxFormSecurityConfig.java rename to samples/boot/webflux-form/src/main/java/sample/WebfluxFormSecurityConfig.java diff --git a/samples/javaconfig/webflux-form/src/main/resources/logback.xml b/samples/boot/webflux-form/src/main/resources/logback.xml similarity index 100% rename from samples/javaconfig/webflux-form/src/main/resources/logback.xml rename to samples/boot/webflux-form/src/main/resources/logback.xml diff --git a/samples/javaconfig/webflux-form/src/main/resources/templates/index.html b/samples/boot/webflux-form/src/main/resources/templates/index.html similarity index 100% rename from samples/javaconfig/webflux-form/src/main/resources/templates/index.html rename to samples/boot/webflux-form/src/main/resources/templates/index.html diff --git a/samples/javaconfig/webflux-form/src/main/resources/templates/login.html b/samples/boot/webflux-form/src/main/resources/templates/login.html similarity index 100% rename from samples/javaconfig/webflux-form/src/main/resources/templates/login.html rename to samples/boot/webflux-form/src/main/resources/templates/login.html diff --git a/samples/javaconfig/hellowebflux-method/spring-security-samples-javaconfig-hellowebflux-method.gradle b/samples/javaconfig/hellowebflux-method/spring-security-samples-javaconfig-hellowebflux-method.gradle deleted file mode 100644 index 33375a6fb71..00000000000 --- a/samples/javaconfig/hellowebflux-method/spring-security-samples-javaconfig-hellowebflux-method.gradle +++ /dev/null @@ -1,16 +0,0 @@ -apply plugin: 'io.spring.convention.spring-sample' - -dependencies { - compile project(':spring-security-core') - compile project(':spring-security-config') - compile project(':spring-security-web') - compile 'com.fasterxml.jackson.core:jackson-databind' - compile 'io.projectreactor.ipc:reactor-netty' - compile 'org.springframework:spring-context' - compile 'org.springframework:spring-webflux' - - testCompile project(':spring-security-test') - testCompile 'io.projectreactor:reactor-test' - testCompile 'org.skyscreamer:jsonassert' - testCompile 'org.springframework:spring-test' -} diff --git a/samples/javaconfig/hellowebflux-method/src/main/java/sample/HelloWebfluxMethodApplication.java b/samples/javaconfig/hellowebflux-method/src/main/java/sample/HelloWebfluxMethodApplication.java deleted file mode 100644 index 0d068142d84..00000000000 --- a/samples/javaconfig/hellowebflux-method/src/main/java/sample/HelloWebfluxMethodApplication.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2002-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package sample; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.*; -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; -import org.springframework.web.reactive.config.EnableWebFlux; -import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import reactor.ipc.netty.NettyContext; -import reactor.ipc.netty.http.server.HttpServer; - -/** - * @author Rob Winch - * @since 5.0 - */ -@Configuration -@EnableWebFlux -@ComponentScan -public class HelloWebfluxMethodApplication { - @Value("${server.port:8080}") - private int port = 8080; - - public static void main(String[] args) throws Exception { - try(AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( - HelloWebfluxMethodApplication.class)) { - context.getBean(NettyContext.class).onClose().block(); - } - } - - @Profile("default") - @Bean - public NettyContext nettyContext(ApplicationContext context) { - HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context) - .build(); - ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler); - HttpServer httpServer = HttpServer.create("localhost", port); - return httpServer.newHandler(adapter).block(); - } -} diff --git a/samples/javaconfig/hellowebflux/spring-security-samples-javaconfig-hellowebflux.gradle b/samples/javaconfig/hellowebflux/spring-security-samples-javaconfig-hellowebflux.gradle deleted file mode 100644 index 5ac19deea82..00000000000 --- a/samples/javaconfig/hellowebflux/spring-security-samples-javaconfig-hellowebflux.gradle +++ /dev/null @@ -1,17 +0,0 @@ -apply plugin: 'io.spring.convention.spring-sample' - -dependencies { - compile project(':spring-security-core') - compile project(':spring-security-config') - compile project(':spring-security-web') - compile 'com.fasterxml.jackson.core:jackson-databind' - compile 'io.projectreactor.ipc:reactor-netty' - compile 'org.springframework:spring-context' - compile 'org.springframework:spring-webflux' - compile slf4jDependencies - - testCompile project(':spring-security-test') - testCompile 'io.projectreactor:reactor-test' - testCompile 'org.skyscreamer:jsonassert' - testCompile 'org.springframework:spring-test' -} diff --git a/samples/javaconfig/hellowebflux/src/main/java/sample/HelloWebfluxApplication.java b/samples/javaconfig/hellowebflux/src/main/java/sample/HelloWebfluxApplication.java deleted file mode 100644 index fd2db5edb84..00000000000 --- a/samples/javaconfig/hellowebflux/src/main/java/sample/HelloWebfluxApplication.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2002-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package sample; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.*; -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; -import org.springframework.web.reactive.config.EnableWebFlux; -import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import reactor.ipc.netty.NettyContext; -import reactor.ipc.netty.http.server.HttpServer; - -/** - * @author Rob Winch - * @since 5.0 - */ -@Configuration -@EnableWebFlux -@ComponentScan -public class HelloWebfluxApplication { - @Value("${server.port:8080}") - private int port = 8080; - - public static void main(String[] args) throws Exception { - try(AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(HelloWebfluxApplication.class)) { - context.getBean(NettyContext.class).onClose().block(); - } - } - - @Profile("default") - @Bean - public NettyContext nettyContext(ApplicationContext context) { - HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context) - .build(); - ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler); - HttpServer httpServer = HttpServer.create("localhost", port); - return httpServer.newHandler(adapter).block(); - } -} diff --git a/samples/javaconfig/hellowebfluxfn/spring-security-samples-javaconfig-hellowebfluxfn.gradle b/samples/javaconfig/hellowebfluxfn/spring-security-samples-javaconfig-hellowebfluxfn.gradle deleted file mode 100644 index 33375a6fb71..00000000000 --- a/samples/javaconfig/hellowebfluxfn/spring-security-samples-javaconfig-hellowebfluxfn.gradle +++ /dev/null @@ -1,16 +0,0 @@ -apply plugin: 'io.spring.convention.spring-sample' - -dependencies { - compile project(':spring-security-core') - compile project(':spring-security-config') - compile project(':spring-security-web') - compile 'com.fasterxml.jackson.core:jackson-databind' - compile 'io.projectreactor.ipc:reactor-netty' - compile 'org.springframework:spring-context' - compile 'org.springframework:spring-webflux' - - testCompile project(':spring-security-test') - testCompile 'io.projectreactor:reactor-test' - testCompile 'org.skyscreamer:jsonassert' - testCompile 'org.springframework:spring-test' -} diff --git a/samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java b/samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java deleted file mode 100644 index 4885ae60580..00000000000 --- a/samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2002-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package sample; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.*; -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; -import org.springframework.web.reactive.config.EnableWebFlux; -import org.springframework.web.reactive.function.server.HandlerStrategies; -import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.RouterFunctions; -import org.springframework.web.reactive.function.server.ServerResponse; -import org.springframework.web.server.WebFilter; -import reactor.ipc.netty.NettyContext; -import reactor.ipc.netty.http.server.HttpServer; - -import static org.springframework.web.reactive.function.server.RequestPredicates.GET; -import static org.springframework.web.reactive.function.server.RouterFunctions.route; - -/** - * @author Rob Winch - * @since 5.0 - */ -@Configuration -@EnableWebFlux -@ComponentScan -public class HelloWebfluxFnApplication { - @Value("${server.port:8080}") - private int port = 8080; - - public static void main(String[] args) throws Exception { - try(AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(HelloWebfluxFnApplication.class)) { - context.getBean(NettyContext.class).onClose().block(); - } - } - - @Profile("default") - @Bean - public NettyContext nettyContext(HttpHandler handler) { - ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler); - HttpServer httpServer = HttpServer.create("localhost", this.port); - return httpServer.newHandler(adapter).block(); - } - - @Bean - public RouterFunction routes(HelloUserController userController) { - return route( - GET("/"), userController::hello); - } - - @Bean - public HttpHandler httpHandler(RouterFunction routes, WebFilter springSecurityFilterChain) { - HandlerStrategies handlerStrategies = HandlerStrategies.builder() - .webFilter(springSecurityFilterChain) - .build(); - - return RouterFunctions.toHttpHandler(routes, handlerStrategies); - } - -} diff --git a/samples/javaconfig/webflux-form/src/main/java/sample/ThymeleafConfig.java b/samples/javaconfig/webflux-form/src/main/java/sample/ThymeleafConfig.java deleted file mode 100644 index 8ea1e8e3b15..00000000000 --- a/samples/javaconfig/webflux-form/src/main/java/sample/ThymeleafConfig.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2002-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package sample; - -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.reactive.config.ViewResolverRegistry; -import org.springframework.web.reactive.config.WebFluxConfigurer; -import org.thymeleaf.spring5.ISpringWebFluxTemplateEngine; -import org.thymeleaf.spring5.SpringWebFluxTemplateEngine; -import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver; -import org.thymeleaf.spring5.view.reactive.ThymeleafReactiveViewResolver; -import org.thymeleaf.templatemode.TemplateMode; - -/** - * @author Rob Winch - * @since 5.0 - */ -@Configuration -public class ThymeleafConfig implements WebFluxConfigurer { - private ApplicationContext applicationContext; - - public ThymeleafConfig(final ApplicationContext applicationContext) { - this.applicationContext = applicationContext; - } - - @Bean - public SpringResourceTemplateResolver thymeleafTemplateResolver() { - - SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver(); - resolver.setApplicationContext(this.applicationContext); - resolver.setPrefix("classpath:/templates/"); - resolver.setSuffix(".html"); - resolver.setTemplateMode(TemplateMode.HTML); - resolver.setCacheable(false); - resolver.setCheckExistence(true); - return resolver; - - } - - @Bean - public ISpringWebFluxTemplateEngine thymeleafTemplateEngine() { - SpringWebFluxTemplateEngine templateEngine = new SpringWebFluxTemplateEngine(); - templateEngine.setTemplateResolver(thymeleafTemplateResolver()); - return templateEngine; - } - - @Bean - public ThymeleafReactiveViewResolver thymeleafChunkedAndDataDrivenViewResolver() { - ThymeleafReactiveViewResolver viewResolver = new ThymeleafReactiveViewResolver(); - viewResolver.setTemplateEngine(thymeleafTemplateEngine()); - viewResolver.setOrder(1); - viewResolver.setResponseMaxChunkSizeBytes(8192); // OUTPUT BUFFER size limit - return viewResolver; - } - - @Override - public void configureViewResolvers(ViewResolverRegistry registry) { - registry.viewResolver(thymeleafChunkedAndDataDrivenViewResolver()); - } -} diff --git a/samples/javaconfig/webflux-form/src/main/java/sample/WebfluxFormApplication.java b/samples/javaconfig/webflux-form/src/main/java/sample/WebfluxFormApplication.java deleted file mode 100644 index f942a1cf151..00000000000 --- a/samples/javaconfig/webflux-form/src/main/java/sample/WebfluxFormApplication.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2002-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package sample; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.*; -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; -import org.springframework.web.reactive.config.EnableWebFlux; -import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import reactor.ipc.netty.NettyContext; -import reactor.ipc.netty.http.server.HttpServer; - -/** - * @author Rob Winch - * @since 5.0 - */ -@Configuration -@EnableWebFlux -@ComponentScan -public class WebfluxFormApplication { - @Value("${server.port:8080}") - private int port = 8080; - - public static void main(String[] args) throws Exception { - try(AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( - WebfluxFormApplication.class)) { - context.getBean(NettyContext.class).onClose().block(); - } - } - - @Profile("default") - @Bean - public NettyContext nettyContext(ApplicationContext context) { - HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context) - .build(); - ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler); - HttpServer httpServer = HttpServer.create("localhost", port); - return httpServer.newHandler(adapter).block(); - } -} From c9b719e3c19174a20885db752f2fb51c7651e1ed Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 4 Jun 2018 13:52:42 -0500 Subject: [PATCH 044/226] Use Spring Framework 5.1.0 SNAPSHOT Fixes: gh-5408 --- Jenkinsfile | 2 +- config/spring-security-config.gradle | 2 +- gradle/dependency-management.gradle | 6 +++--- oauth2/oauth2-client/spring-security-oauth2-client.gradle | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 8cbd437f754..4b209611b37 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -59,7 +59,7 @@ try { node { checkout scm try { - sh "./gradlew clean test -PspringVersion='5.0.+' -PreactorVersion=Bismuth-BUILD-SNAPSHOT -PspringDataVersion=Kay-BUILD-SNAPSHOT --refresh-dependencies --no-daemon --stacktrace" + sh "./gradlew clean test -PspringVersion='5.+' -PreactorVersion=Californium-BUILD-SNAPSHOT -PspringDataVersion=Lovelace-BUILD-SNAPSHOT --refresh-dependencies --no-daemon --stacktrace" } catch(Exception e) { currentBuild.result = 'FAILED: snapshots' throw e diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index e59f23fb099..ecce84d7a9d 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -36,7 +36,7 @@ dependencies { testCompile spockDependencies testCompile 'com.squareup.okhttp3:mockwebserver' testCompile 'ch.qos.logback:logback-classic' - testCompile 'io.projectreactor.ipc:reactor-netty' + testCompile 'io.projectreactor.netty:reactor-netty' testCompile 'javax.annotation:jsr250-api:1.0' testCompile 'javax.xml.bind:jaxb-api' testCompile 'ldapsdk:ldapsdk:4.1' diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index dcad76a6fa7..ee0de7b7016 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -1,13 +1,13 @@ if (!project.hasProperty('reactorVersion')) { - ext.reactorVersion = 'Bismuth-SR9' + ext.reactorVersion = 'Californium-BUILD-SNAPSHOT' } if (!project.hasProperty('springVersion')) { - ext.springVersion = '5.0.6.RELEASE' + ext.springVersion = '5.1.0.BUILD-SNAPSHOT' } if (!project.hasProperty('springDataVersion')) { - ext.springDataVersion = 'Kay-SR7' + ext.springDataVersion = 'Lovelace-BUILD-SNAPSHOT' } dependencyManagement { diff --git a/oauth2/oauth2-client/spring-security-oauth2-client.gradle b/oauth2/oauth2-client/spring-security-oauth2-client.gradle index 94f29ae6074..188582aa640 100644 --- a/oauth2/oauth2-client/spring-security-oauth2-client.gradle +++ b/oauth2/oauth2-client/spring-security-oauth2-client.gradle @@ -14,7 +14,7 @@ dependencies { testCompile powerMock2Dependencies testCompile 'com.squareup.okhttp3:mockwebserver' testCompile 'com.fasterxml.jackson.core:jackson-databind' - testCompile 'io.projectreactor.ipc:reactor-netty' + testCompile 'io.projectreactor.netty:reactor-netty' testCompile 'io.projectreactor:reactor-test' provided 'javax.servlet:javax.servlet-api' From 35b0e289d67bc52bf0b5d6a7e86b91eed7abcf52 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 5 Jun 2018 12:30:37 -0500 Subject: [PATCH 045/226] Update to Gradle 4.8 Fixes: gh-5412 --- gradle/wrapper/gradle-wrapper.jar | Bin 54329 -> 54413 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f6b961fd5a86aa5fbfe90f707c3138408be7c718..91ca28c8b802289c3a438766657a5e98f20eff03 100644 GIT binary patch delta 7397 zcmY+JWmFVEyv3IV=@jV(>F!z@2@w$KmS%wkX_s20k!ES6J0t|8k&cC>kxuDOX?#B4 zJ1^$b@7_Ce=FH5Ong2AGqQ;b=#*3n*Sr_*uNE)JFxShG70OBcY>n*sk%p!xW6fgjQ zBRDM&7tC8npX5nn+ckXXnY>Y{cCI=kWN6%){zd#LVaB+K7p4${BAbJYEe{+=^g7o2 z__WmJ>zD&~hr|ATWSj$-S%Gb#e5Sm?p<;&4WZ3+X?4hqH!1wr#Ksqkbh>`eCw*T+> zdr4oIU5+5{&z^U6jHJ3~r{01DW22Zt>hf#9IBE|C7t*QCvC|Q0*{0hOPzIC%= zQ~86R@Nx1*(OD6DAAhX2>zDp?^3l(m3<+(5VctWD-UDZ}ZTc@y9Q*FiP_%Axap|1< z!cW)r{Ltu)8r7|NIH$V8}$P&4CK|U7pkjTPh7{DVY2-*0E<=ux<`7XB~s7k>OnJOT0zW_N)}E z+g1`<4W42#8BC;7L%Z2UF=_s>L?z*|w|n&6dGuDALeY@xl%#dE8tsB$VoFb#-B1$; z?l2EYZZ3fYzSNvjt^GTfLZ5Ck_rS zb=+}lXfza2Kg4%j^mWuVtR-m0-PGJ-<{|A1_w>f(M2+>%gZeS4Q=RPJtsOa~wu^M{;luOUT~@`NyIiV`8gW1%!)1FBg69GkONB9Yy5ArTOk<9HJqOLnS; z_ha%bf9_T|HUSg07;>0*`)&S?nJd$ zX35$0rU2z3=pctjJUdOtFihi$tv5`)U{}Il=z(>9A?GY3_6KH&9M;c_J`G%m=(d@~ zQT|Z%r%J9eROrRbV?3?ln9u_v;qBZ?{YznP790&%Huf27@X93tC6JT;OaECsKUiea zzsl(|B)p_fa=uWSn^5<*HOAGAd>a^5NZyd3eI-Dg=$i<;SI>EGT@Vi9(jt~1bqU#R)(|KcV?Vnrdv z>od_H7eYVBMcSVUI!l{l9kdDQIF4F^RZM2ct(;m%MiI7=QXq_GefpEOfWhE~h@&Qi zRz{RPm*_pzQ9)$~R-*KwZ_rHOcl@xjB5G}jfbd-AAJN0GJ{*^1n+xVki0+BFneb56 z_OQA3_N?{?Hz7?YG`|5^|5vRjWbL!d@@)$E4_73(1!$ zexzL2VHAi_;ULOV67s}an3{HylUZb&MWy2J)T{QZ?vY-?fi9f|8RN-eyeop^y0-dz#>hdNrDDENnCbbIPam0LCGW!8rk8}OVsk1$ zeMXB>B6zg+;t#aLLHu1m6=DSY@hk^^P(jT7X>k(@nj-`AW#tb6^2l9Q32_x>)@d%N ze{vs?DtgQbR>Vh4e+Ut4j@2UYV`daonL6yRRonz7?d$hAB>k9>C(#fyKz=7vb#s?& zPG?|(#U;du3%CZEfRtIJHF(Tk5}UsyGiOkK8cqJw9cUEg?;v%o$G%i?=bA&=FK0Ab z%73UIXQ?6;in>pMTS3N`Z`RLn#euKic?WCsA~;jZYU@HfIlFrBRi8CJn`NeUig`p%kI zm6yJA34!)1L5d2pz+PDrXItk)oVs2~Z+f|kpFsvkmdi%^V4p4n`6EW96&6If%`F<;#pM$`8!{M*kv2ftii=> z)VSk{*&GKY?4ac_K_Ccj$4-qst|QIVS$F$}BArRSw%hKRJ!u^NsR<9(TkcfEm;e}2 zR7T#HY*O^0A!mmjWUj}BIc{Rc_ABb4&7ea~aj6I<;O!O2av>lyJLnBXsa|sjwk3|? z<$7m#H=r0{o23~ut7i#a$>+(jRsO!X75JvKt`y39dJ!7nocO5$MItV<$dGL9$}gdt z>V5ShA%*FMWseR0UV;WF4Eq13E>ak{0|v#w<*yH^ADvR0B--JYm8-5H1K zVCtf~$hfY>fx)s;iX+Lc6=IOnik?}#FSTyZ0Y046r_yQ{q5cUUYA`aXv#IbZ82a)o=5&_lnAI}NuapU)+> zvraAS9lK`~t!C;2wqJmXeNw#MXd-l4gqXa}R^QZnbTOJK;;?@I@rm%&)XT!SNUM{(Lnx-WhRxPu7{fnlcuStU#%j%GVhvSQyi=Oa>s@|Bti%&f;g2~wYZL4OB}Mxk z+b?F2QRYl8SEH$^Gq|gKB#haIz z!t;iN69p}snp1u63`$`ywSPlVpl8E$$3d3G`1YD}`;<3C3`sekPxy0hxMmFyo{Al` zjUy|SyiSr2y5f=5&mgP6XF%K}*ytW8IoHpLli1}$X7!wnQbBvW;f3szBEF}rR? zY`y}KF6S!8;D}f}))rrPH$rXQNJnS4R74kLLc$Xlfi3HZv7(c;kW~xC!l%tIi8<%%fuw4h?eLli`gI6Kc1dj;G?dZU%Rmk>0 zlI*fs%~LpWeDKVqjhU}l1BezIGOxA@6Fy1dK%$X2@wi`9-Dk2rw!iAbE@yVE;^ikoLwA7OmE8`D`Eh`+;V~q8G+M=nR8w0D3rUpl=QIqW6cR#W zY;H=fZDK3v(JfVx;PyNoNGh$v?A6D?Ny}xo_u*btdiuJqOFV-}gTehtE+HTLD#vCi z(P;=v&BQ)l*JOM5u$++%C0KV4T1u0cFKqo1W9t@;MmdYl7p_>lNlO)kT)JCW9VtPHV=LvN7Cmu!15P~= zF$c_e{H7EUclHaLPVW}W^}+i_m3k%^T=!F`!E7jtgD#J*Vrk%!gD!bR`;?-cQw6^s zMs?OLY~y(CPr6ho2v{&%;&-<-BtQw`Ip1zs{2 zlJHij73LF3dT9AFz7*)QEA_3p!}WYQ0?UXmjQacL??6~-aXabU#l!MXHFclTo1S(a{$9+Ou=$cZDW!05dm|L@K!HX_WTu+?pi z$kqV%mHXt^bQ5$ho@BSoFJ?IYgt;hefLyzkEix$*J}QA5*Dk9&DFAaCBd*-o7VNTW zlvG=i*DqY|*|1z;)URPCs(YsgPe~S0Zam|}Y9`eqd2n!l{gFqusPv4n+&W+Fk)xbm zdzXM7H=IUdvNTq4Kthztjw5VUA;BmZ#T_a} zXfCQ{AXqeAM&}`GrRdZ)&TYsdzqA9lM`6MdV_3Bc&Q<-{go7O~E^ zB+1H5?E^JZhaahb98T-Q=;L^R_V?-q9=(nTHYUTIvVQs?(oce8pj#_agXmxlPa_bK zau%s)QijfkTYKXL^IMmNlo8?0wTk{5qF^^Za9AiYC4CJlE5;ez-EDZhq=WtV+9|=J zdrjxAPf6+VIY|9?-N@&u?um#2njz=;MmR!S(Qmtp^<%eV{n8&8ahAq!gPbab^ic`s zyJ0~wcc99P7n5uLIQeEmYkzWh5Vrh@;jC-W2ZwsPj>On| z(^XgybF&i7-OamAiBIwSlZlSR#Vdp}R)ZsekYoaci@43^_HDzOFN#Ygfn8V?j~7T|6QKR zmkHh^#w|>z0HL?8tL4 z#j>Jk@asSr=>&I8HHPo-uHK!5P$s$*!T6DZaAxro&F<|k)!b3vsAMfhs+KaHbFTqF z@E*H_x@c_yC1XKvMRO`cO0DhJz=Ysa#sj0v%Cb_Lfw z__Tj&^5blO$_^Vs9$%Defz%#eJ>{M>`+`?KO3+@yNN&l@ph=^p?3kpN6HX^mLL8;h znX}4v-_c1ZGNxK)$1vi4km%f%ejMkj0D~6P=zkXprF=)BTwIrrvDK197V+Vd)W1H7wH&?}1G!1${&? zji^*OwX5nDnXEZsh`aiRBDWb5%DiG%R*%%4a7zk2KHuRt{-opP?bG6RQ@_Jrjwlcc z1c?I{V!mStTm$P+uMqeQJ+G&A-Bogkd9n^ zow6ZV5GhtRsc+n5Vpm!gL~@IgZ%M6SEirNsUCuhF?$hN(;FV7cQV-0WmV+mE&j1UM z*5gsoF|(+ckJqjHl>N;;_7d$6)awK+so|DP85m4m6cAC}YjgzQewH$pvS#KLNV=gt zWpcapyxLARfGB_63|p6Ui?{UbnZJMi1E7pVZdJRUG0y0E8+C|I2lvjlSp5_*-9ppR z9Q?Y|hjxxf;g3l!AMqNq?7hENyaue$&EQ)6FbDWW#-+fS((exFbIWL=*R@bj_n6DF zGCb@vMge`!wA!G4&-nEB;*{lqQ0^)M{2B->b;pmtJ_V>dbMl(ZYwlESZKFt3Bd@t; zzC*F~yF5cZKTtpDuTE;>X#^O@JfP2J!20&Rf0;m2)H9@oGAu5WhS2UQ705k;s>y`Sd{YI7DBC zBW86YCiE2dyyhHO54}mazQYIr&G>9TXvbTK9y3uYjZq3E0q|CoB)hdt{WX#^W6{9b z&nmv5v0eF#uJwL#X3rXVGUQea5`B~e}uKLq6{Vekx6e!b8jq)3LJg-M=&0@ZdMnC*XKnKQY#v0E4a#+rkFQG4PF7MDnu$0{d@2)I(fA!1#X({jHj zKz%G%jhilKF2nq6|EC<~QQ!(T#~2+bAc{|YD0lVv+2a^p7)&X3?Y#g)mTJl)9^m8s z*)qZWHG$;QO3&fF5Pr+Eyk+V;JgzHaiSU4Mr<=IkOW*N3%Xx*Cn8({WVH}|VjK0f+x=;||J9}N#t+mw ze-&XLYz)9#H4ID`K0E~8)~)&PQ>%`k2OI7G{A4i>C{zExYTMuX+yA!D!tkzMlE1|_ zJb1|f2XI2=pR7{Bg%1rA!qEmPf!1pOEH!mJxP}@z+-Q&k=&b(F${dseX6XFGlR;&m zkKsR5A5sMBnfy0mz^jMEfL1neP8()8K7s?K8!nCj0ncpU%{GGY_$fko3xX2G4?csi z2Qon5av{`k<6${qqw_y(>mc~oXMUIxNcQ2Mb?<@;ry1b@(t7=sOmL?Wejv8@KfLmy zgfIO~a(wy2|{?vC?!fAymk}} ztd0Gb$0wk}UyaEEixU1@lEOR3IDoB5|C3fG5&at~GERxo1lJmW56nvcpD;H4Kl>BH zffJNKuP^^>xd}-iGW#DkWz+s0$^pd5|0@~cc$0jXlaLj2=bn>46Jlg*Nq_8n4 delta 7284 zcmZ9Rbxa&UyY*p_;_gLisRLH+^J!mIhcBl0_*qd%+} z)z^`2t@btAW%fOwIG?H%1_Gy}x}Ohmufbm)bnYro1zK{}9Me&D!8f@=>j4?J0qgJA zg}{&N4ZX;wm8$Z$}t~&p!qz zM5;2nN)8r$+-$is#4E@!h2DJts4|?vM8xO@h=*PjKYn;=j4y+ja!IgSxH&u(A7W1;N33(Q2q!A~a znNEU3e@F;(X)!lnU|J{3)s6K=skjw?AmpuVeqrer_x^Qs?U&(N%rWdSjeR~{n!e@(0V97}K*+nu2Gq5=j6o^hv|lm0T9Bb8WMS zgKYy1bsTHu6|8vQ?<5DpPa|m~{O6AS^c_47VPgZ=b4afD;E_7_Xb%RrlN{zQ?;(Li zk{4cli|tG6EF)i*N7s||M~56IUUx4Wa+^=RB_F3rZ&~^KuljFRS0H}eL+JeqEg{}H zg-#~J!arLXb*9*e!eH!7QguKjMMk{*#Re%DD+RoOV+Vv3c? z%Hl`dk8~MmbT;yavPj;fG|3rJJ+TeqjV+kDyskGI#1j|bu@EVctW@VIiwMv498cGt z3~9Jq|71$c-rJ`=M1*Kk?gj!{PPI+l6LXxK(Zko3FVB}enFMsJ1scd_yis6sE&zQV z59eLQhBr$;2q zENT4}bGlE%Z?&PZn(k*cvMTfMGPUc5GPg3dMG`Tb0Zo2bZAMsZT_(XqJ>j;N*gs~4 zR>e9`XjM2!Z6K2Yxgoa9`a?fa&&8sA7C>E^p#7RnEM_e?n>+Wsc2$`Mvp~cu+x_GE z7~xu>RZ*PooqG`+#i9C*yjbD{(Tgkg2!SM$=r+WTC`k>s69#Eb^wiktBAd-Qh;air zLF@d4xu)Ou>3pZ0NWd;T%63rQ$E7_U_E0B8J)=a)1Mz8322(^MxmkEDO&G4fPmVRh zTB7+LNZ}xKL#M2AOk3b?IY$o$QAd@J7Vm9S$zmCqrLC6^tv9`W{fhGvVfLBo&B8ll z1#Vwm%#Fh8-?(1zQ45zOS{$2=eyEAawONDQ4`4WKE|t{uoO8~C%{xXt-q0{hEU@Ml zNMAHY4sxe$ra?)cEw!y9c|5|DMP;_#=9;OLz&*_DcUp05!Dk$KI!v;`#GtM)+;x*& zH1Z)5*(AA9Aib*i^aAfH;d|jk*M=bO{uk_#uzSQ`Z@Y5_-(;elL9~XZdG8)M)D#gA z3E<#RP~hO;B;oSVJ0$4w;oxq7aB$Rr9T#^SXbm3?gmCAxIOFZ^)Z}C%(uek1I=oRT zZ(b&{6zV1#YBMhCO?sb{_*I6fJVm@C?YG+c$HiW4gg%vJS|B`L=2ovCKlSTc7Z+_D z0*`9~8(a?j8(e^=5Nl^==M>4W+Zis_y*st99=(@=;A_T4f zT=>NdWLvRhY_A`22k9OX&n+qq*rz!@D zNbOF8e6^EoqI|I&&EqiihS_L#%$FUp zHH5}E+Yx9SDWq{yyagHZuXEHK&^>qDTTfERdpcCH46v7BktjXLcO zEgF3+W0D$Zxp6jfQKLRnF8Ma!+sTcIO@m87O}y-OXLWH53tCQ zH{cQxfsdM`o0Hu#8|VbiX&()wUuZWF-u>!ihJpn!7h_h%IcYdO z1{mQ3T*WVT&dT|1tT)3LYfQ8`eYtZ1?&vn65$iDAV9AR9=oCMKS;Jn<2$f9%8m>2X zdTY)PQ(k{!BNyPU{|ReyGrCN;{!9+}!AZz#RD3#DQ{va%(mB*R;k$=c?&cNaJGZ!X zV=bB;%C5tOR2(yQjVGV1(#t=B*R4##m~9a`dJierXH{O(Jeb?t<{e|;m(H&RD4cP#>8G zk4lf(3s8vck@u|0b~#DsCYvrq5P;^pH8~NE^8T9B)6KI38RZi z&4Vu<@bl`AJ!m~vI+IHtkf`XwycZm=JgN(~{GkQpIn?OSmxYd~B;i^hPW`Y7b&)|v zoQfS5)YY$YCK2~n_@<2LfX6jMx)-#-V9q6)z@*tkYgRYu`(4SIYilW^@N+|{CkufR zeU6u27BN?G^RebJPyAnYB)1jI@_M;SL|&}a&;3EfNQ zgMG>KWF9*(qMrOSVb@5 z6={LFVXask#>{ck;vqwfIXWJG#MmBs?+mqbVO=dHR?!O=&_!*cHo=2cneOa-9fo<3 zU%s*00RrT~KkdOzqO4$^1LT*TuYN%G@II}+oTKszMYMX3suww4wYoo5rtn;*Q>ARp zv#!5Ot2g~i<%L(b+y=+!NQPE#zKhyDO8`Rao&^h^OA*dv^_a<4gH(hgChjDggFTCS zDN{hnn83LJ&i7^fW^+R6eWVB%?R%nLt(}!S?=;Bi@At|2-44hRryBP;Pi$$YyFJY7 z1Gfqu1!K2y$k>%n)IDKFroQKqpjCbg*>?s!MgjFDN$EyRwwH0x%g^%t^7iN9bwy7w zFP{2?b1|is#^rbPIa+p@2YEUz`1)^}+pVw6p$Rv3=scGX)oK>*{tKYb6)(FZE;VVOs6uz?KPbMtFx9tVzX4 zEK01{Tn!42gf>7)zK6MA!D87dH;jKE+M$)|C>_(_i0in%db3?K$$)F}WX_2f0e-qeNH7EQ7=tIh-L}XuaF?; zDZHQy9hTD)`<4iNY9CXL-okbhZ#~?A={e~66>O`dz1gQs!@h(vi!-={osm8(B|#|c zdA1Hv2<}8at=;P)O`$Ade*e{RY}AApi?e=Frl=JZd5e-}qb1fq4SWhrgHW5Yl94;u z#p=hIA(%;h&RrBWoUo%rt&xT9KNj^**E|7E-^Tvr(xRApqI!hAFTaR;N|C>A=52^? zR4X6Tyw~~_W-j?e({%N72A2FidkoSr4qGlTZsbb)&Y&?Hjej`0#S~rV7q~D34dqc* z>BED%3e!&gaphbp8F7jkCfG%JnZs?V!i|ymNMEyAc?^2N{Ze$6sP%&SrRqZUo-O{m z-KucR<#HMn6tymcd8qeTdG-FKqyLNAy{O~}$Ne*nVq(=S=!Gy~;tSEA2@1$@ycb<; zo82-{$Y1{HETkRDVCL-seuOyaULfGp*q5D^WP`*W#=;xQ!{j8n>$d%E)lsQ@uhonl zo`o@ul5}4FF^?BiW0XSsL0k{|=NN{v5*NhsPuZzn!}%JU;OfPM3ex!mXb1eg(hpRm zl0k13EYj;pmRX!#D`z5a$S$PWru4gtuF$C;YhlGS_vidK_-~e%&{s6S-MMvjV)cIv zPiWtA70_W4+|p#{b~TNBNC-Oj-v2VQWoqn~o0Bw3b%1_=k{E63zEV9WYnytQu-}?0 zzVB*A7F{ZHmXSJ!Ia->WHQK>2(T493$)Sj*NG5tt^wq6w{=|J#!Z!wzx8o^PQtJHY zY2sO7sLAU(n+k?6K@ z!e%G8h>!0=7%r3V8&wBIE{!Jt6#dvWYqFBAB(2#>bI$7y{Uc=sC;reVV_ER}Ln?ct z<&R~eb7qM_Z?SuW<<=i+IR3lCm60(R-K4Tdv9fZVlFSEj*HqA38mQq$$S(+rYEBo=nYPVmu zdE~*fYRvN|YL?{?&eSSg$^x4JC`X_Bb>kbE?#6a-ximH&Bibi3s$~xmFjFOU{4X!c z5tItqJ!jT~&h^V!Cc_k`(Bxa5h3;7L94Aug8 zI)503EUDZw^Naj}408$aeP8A$aYOfF?N&N3W*~IsrIDRHmb4bwCpaw8pdBmYG*Z#Q znNPbLE2^oKm|FDZND6Dc9K>A)Z+6G;Sx&4;nS6M(3NXS%3liB&!&Ea~rjF1@zhIml zM<}H#cC*@vk_XlZN{2L$N%6iE4s7_r=7GW9?ArFc5h@W7j7wX7!mVT8PIx*icI5}O zz_f2*{H!G~ewBN!K=OFhuZGzlvvhM5&AA4JT zF3W*zCn_FfXO8p=vy&9`0T%8Y5*Vm!Alo)aIpc_Y)eSxAukGN4_Qtfq1)|qH>w*Oq ze|H}q4t&~2H{1RhY8M6U_D(8qOUIsxw_fLdEseSIY<$-?Q_zxOtsUv{Xt&D^DSACA zfz+eyJ)jkB*9OXoSMOIW^Ub)c9KqS~#k%MMEu{fWlGBpg!KR3WWk;X@_{$H zgR+mlL|qJw4I({z9;P?a8eO*j!Mf-zaS2ZgVy5CBx6cm00YtAU;M=kkHes@NC+*I7 zXR8wTslDI#v=0-a^<*c7g$VDPAJRiR_cb~ZXL#UXnHK$6Ouk&&>*#blvtU^2Ny#IS zNdaOr;m0NlM@M7ccqr+IqM=l11WUXbs6|RGb+L$e%UgWK8TiK92(|vr8PQh{hQN&8 zHv(4VcesAMn3)w^v}z9QVMZP~ECr?WBx2e8@|Ona3QyB&b~O#fJDl)qJ93=*A)saf zQ9~iWrCWNf9W^qEURF3gTWHcUvaJuim?#AFh8zQ-N2tWE1p&kR7gj&5kZwmLRmq6t zDe6^q0%7SMj$gCaVMPc`W%?_i|8voWOf^dlN#P+Gq^vNgFAqi%tlM5@n!Fip{A=z| zZe%lkadj+xQDTXsdSP2kRuNHE@j1$F*>z&dE7u)~#8~D&dIT8#jkEaN69s}b*Z_HR zi%16ErWZe6f^ILobOzyc!Z%|cH6koHD&9M?cax3{CvB43ij($u6Pn_-!6K!pgv^XjtvU~E$ACJ zx(81gk57hqsyO^6t#-O5tPMRvJNzwp*U)PfOt$*eN_LM~|Nd4*m=INkv=qX}scq2* ze!dcNF8Z1K&00xVs|9lhGF%cjk`MNlaUOOPBcV(rp`; z*pXNr_ou(W7C01FewCk)?7LebGPBcL>o=NKhB;GU2lV&(y7d#NHo(DwlMa< zuf)w~H{Ayua|;|-5dd{B_U~t`M!a=zgj~_+Vd4V@-Di78agKSG-|bP>vo*oyIIFCF zpNyk29Bdh%qkjcwyd7SBJFhz9^9}4jA96E7It@`0_Yx7$`qh7Gln@DVnOnhvd2DmD z^td?>Wqb|t>q{8$Jhv#l)ilrqOZtn;xKkAxzQV7EDD4vpiX^~^PtSr1oygSa$dtT* zrQjOg{9j)E%yQ1-^8r-hsiBiQJ1Qce4YS(ox?HUg<=?5?Z;T0Bh~P~SwlgrdRmQF< zmyV&jeG3r-AKK4f@h~7)+`2uXroM#l{4s%lyy%~FY;g&Qi_(LQxU75RzJxDr;Zmi{ z1nPQ_O}w(mZSRnJuvM76-yKV?dAX|pfvkduD1fOE^~WcVy}6TKG$$orx!bUxb6AD= zWcGcfJa1J}B8lfgg`nEQA~gft`N<-#1%l@RoE{Tgf6#$KBmx4&5`9(k(KZ+T38X93 ze7Zl`@0sC=oY&@h_D#|jMoFHzL+`Z$j!eQXWADV`oAoMb{pQ2lwlvc@XCL|>=i}7ps@ZPpD8dj;6XJ(M&K*eW#d|q{Fe}?HjA8R zHtjbOZEs9pNLnKxb=*WfF)uVd5vz#VfUo4|J5%WJs}MM1$h+}Y^QXk$JJ!obvy7&| z{B1d}SwrKP>SC{~S9UTJ^@9Zh(Z_@r)W&@okqX%ikJr$f^`aXSwS^~Qw5~57_FtzY z0Mb0PL{fdv3DHAp=X5q1Ia`0$#Nf)KUzSH}NpTsG52gztC$USxLdA_5+6i@Rf`+Mx z6rns6k=gGfUic=}iXe(B(jn2!b=KabIIrl>a>BaR=c#%fDaG9%Yfca|c!^#aJt?~c zUCrZO(GWV~9Oe$Fe-qIDs#$ZzwuGUz>xWFe&b<1~p1&3RDEul1QOp60*3x}sMBypn zzM;G--A?C_?ZYUeX6ktK!}0dMy{4w5>}XKM=vVo3Fh$|QUvM(j)x3&Y>x%W*9pCp0 zmS~1CW~R;-<#BMSf;=VJ?*Vb%s;b1hdxdYAmFM$ALx59Le1mczmU;=r8BqP#f@-lC zKtudrZ71&$Ig0-q#r;j={yWI|pc})4(324XK%o3T6#2#vz)}2%1 Date: Tue, 5 Jun 2018 12:51:03 -0500 Subject: [PATCH 046/226] Remove Spring IO Tests Spring IO Cairo is the last build and it does not use Spring 5.1 dependencies. --- Jenkinsfile | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 4b209611b37..9ff67dd8f0a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -39,21 +39,6 @@ try { } } }, - springio: { - stage('Spring IO') { - node { - checkout scm - try { - sh "./gradlew clean springIoCheck -PplatformVersion=Cairo-BUILD-SNAPSHOT -PexcludeProjects='**/samples/**' --refresh-dependencies --no-daemon --stacktrace" - } catch(Exception e) { - currentBuild.result = 'FAILED: springio' - throw e - } finally { - junit '**/build/spring-io*-results/*.xml' - } - } - } - }, snapshots: { stage('Snapshot Tests') { node { From 5c76a12e8729aa57dd815f3cccab2f928339b065 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Wed, 6 Jun 2018 09:37:22 -0400 Subject: [PATCH 047/226] Move oauth2 samples under boot directory Issue gh-5397 --- docs/manual/src/docs/asciidoc/_includes/preface/guides.adoc | 4 ++-- .../docs/asciidoc/_includes/preface/java-configuration.adoc | 2 +- samples/boot/{oauth2 => }/authcodegrant/README.adoc | 0 .../spring-security-samples-boot-authcodegrant.gradle} | 0 .../samples/OAuth2AuthorizationCodeGrantApplicationTests.java | 0 .../java/sample/OAuth2AuthorizationCodeGrantApplication.java | 0 .../src/main/java/sample/config/SecurityConfig.java | 0 .../src/main/java/sample/config/WebClientConfig.java | 0 .../src/main/java/sample/web/GitHubReposController.java | 0 .../authcodegrant/src/main/resources/application.yml | 0 .../src/main/resources/templates/github-repos.html | 0 samples/boot/{oauth2 => }/oauth2login-webflux/README.adoc | 0 .../spring-security-samples-boot-oauth2login-webflux.gradle} | 0 .../oauth2/client/ReactiveOAuth2ClientAutoConfiguration.java | 0 ...activeOAuth2ClientRegistrationRepositoryConfiguration.java | 0 .../oauth2/client/ReactiveOAuth2WebSecurityConfiguration.java | 0 .../autoconfigure/security/oauth2/client/package-info.java | 0 .../src/main/java/sample/ReactiveOAuth2LoginApplication.java | 0 .../src/main/java/sample/web/OAuth2LoginController.java | 0 .../src/main/resources/META-INF/spring.factories | 0 .../oauth2login-webflux/src/main/resources/application.yml | 0 .../src/main/resources/templates/index.html | 0 samples/boot/{oauth2 => }/oauth2login/README.adoc | 0 .../spring-security-samples-boot-oauth2login.gradle} | 0 .../security/samples/OAuth2LoginApplicationTests.java | 0 .../src/main/java/sample/OAuth2LoginApplication.java | 0 .../src/main/java/sample/web/OAuth2LoginController.java | 0 .../oauth2login/src/main/resources/application.yml | 0 .../oauth2login/src/main/resources/templates/index.html | 0 29 files changed, 3 insertions(+), 3 deletions(-) rename samples/boot/{oauth2 => }/authcodegrant/README.adoc (100%) rename samples/boot/{oauth2/authcodegrant/spring-security-samples-boot-oauth2-authcodegrant.gradle => authcodegrant/spring-security-samples-boot-authcodegrant.gradle} (100%) rename samples/boot/{oauth2 => }/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java (100%) rename samples/boot/{oauth2 => }/authcodegrant/src/main/java/sample/OAuth2AuthorizationCodeGrantApplication.java (100%) rename samples/boot/{oauth2 => }/authcodegrant/src/main/java/sample/config/SecurityConfig.java (100%) rename samples/boot/{oauth2 => }/authcodegrant/src/main/java/sample/config/WebClientConfig.java (100%) rename samples/boot/{oauth2 => }/authcodegrant/src/main/java/sample/web/GitHubReposController.java (100%) rename samples/boot/{oauth2 => }/authcodegrant/src/main/resources/application.yml (100%) rename samples/boot/{oauth2 => }/authcodegrant/src/main/resources/templates/github-repos.html (100%) rename samples/boot/{oauth2 => }/oauth2login-webflux/README.adoc (100%) rename samples/boot/{oauth2/oauth2login-webflux/spring-security-samples-boot-oauth2-oauth2login-webflux.gradle => oauth2login-webflux/spring-security-samples-boot-oauth2login-webflux.gradle} (100%) rename samples/boot/{oauth2 => }/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientAutoConfiguration.java (100%) rename samples/boot/{oauth2 => }/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientRegistrationRepositoryConfiguration.java (100%) rename samples/boot/{oauth2 => }/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2WebSecurityConfiguration.java (100%) rename samples/boot/{oauth2 => }/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/package-info.java (100%) rename samples/boot/{oauth2 => }/oauth2login-webflux/src/main/java/sample/ReactiveOAuth2LoginApplication.java (100%) rename samples/boot/{oauth2 => }/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java (100%) rename samples/boot/{oauth2 => }/oauth2login-webflux/src/main/resources/META-INF/spring.factories (100%) rename samples/boot/{oauth2 => }/oauth2login-webflux/src/main/resources/application.yml (100%) rename samples/boot/{oauth2 => }/oauth2login-webflux/src/main/resources/templates/index.html (100%) rename samples/boot/{oauth2 => }/oauth2login/README.adoc (100%) rename samples/boot/{oauth2/oauth2login/spring-security-samples-boot-oauth2-oauth2login.gradle => oauth2login/spring-security-samples-boot-oauth2login.gradle} (100%) rename samples/boot/{oauth2 => }/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java (100%) rename samples/boot/{oauth2 => }/oauth2login/src/main/java/sample/OAuth2LoginApplication.java (100%) rename samples/boot/{oauth2 => }/oauth2login/src/main/java/sample/web/OAuth2LoginController.java (100%) rename samples/boot/{oauth2 => }/oauth2login/src/main/resources/application.yml (100%) rename samples/boot/{oauth2 => }/oauth2login/src/main/resources/templates/index.html (100%) diff --git a/docs/manual/src/docs/asciidoc/_includes/preface/guides.adoc b/docs/manual/src/docs/asciidoc/_includes/preface/guides.adoc index d6cf49dda5a..87490678866 100644 --- a/docs/manual/src/docs/asciidoc/_includes/preface/guides.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/preface/guides.adoc @@ -27,8 +27,8 @@ If you are looking to get started with Spring Security, the best place to start | Demonstrates how to create a custom login form. | link:../../guides/html5/form-javaconfig.html[Custom Login Form Guide] -| {gh-samples-url}/boot/oauth2/oauth2login[OAuth 2.0 Login] +| {gh-samples-url}/boot/oauth2login[OAuth 2.0 Login] | Demonstrates how to integrate OAuth 2.0 Login with an OAuth 2.0 or OpenID Connect 1.0 Provider. -| link:{gh-samples-url}/boot/oauth2/oauth2login/README.adoc[OAuth 2.0 Login Guide] +| link:{gh-samples-url}/boot/oauth2login/README.adoc[OAuth 2.0 Login Guide] |=== diff --git a/docs/manual/src/docs/asciidoc/_includes/preface/java-configuration.adoc b/docs/manual/src/docs/asciidoc/_includes/preface/java-configuration.adoc index 7b5f78e9e71..4f2d9ebbeff 100644 --- a/docs/manual/src/docs/asciidoc/_includes/preface/java-configuration.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/preface/java-configuration.adoc @@ -466,7 +466,7 @@ NOTE: OAuth 2.0 Login is implemented by using the *Authorization Code Grant*, as Spring Boot 2.0 brings full auto-configuration capabilities for OAuth 2.0 Login. -This section shows how to configure the {gh-samples-url}/boot/oauth2/oauth2login[*OAuth 2.0 Login sample*] using _Google_ as the _Authentication Provider_ and covers the following topics: +This section shows how to configure the {gh-samples-url}/boot/oauth2login[*OAuth 2.0 Login sample*] using _Google_ as the _Authentication Provider_ and covers the following topics: * <> * <> diff --git a/samples/boot/oauth2/authcodegrant/README.adoc b/samples/boot/authcodegrant/README.adoc similarity index 100% rename from samples/boot/oauth2/authcodegrant/README.adoc rename to samples/boot/authcodegrant/README.adoc diff --git a/samples/boot/oauth2/authcodegrant/spring-security-samples-boot-oauth2-authcodegrant.gradle b/samples/boot/authcodegrant/spring-security-samples-boot-authcodegrant.gradle similarity index 100% rename from samples/boot/oauth2/authcodegrant/spring-security-samples-boot-oauth2-authcodegrant.gradle rename to samples/boot/authcodegrant/spring-security-samples-boot-authcodegrant.gradle diff --git a/samples/boot/oauth2/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java b/samples/boot/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java similarity index 100% rename from samples/boot/oauth2/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java rename to samples/boot/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java diff --git a/samples/boot/oauth2/authcodegrant/src/main/java/sample/OAuth2AuthorizationCodeGrantApplication.java b/samples/boot/authcodegrant/src/main/java/sample/OAuth2AuthorizationCodeGrantApplication.java similarity index 100% rename from samples/boot/oauth2/authcodegrant/src/main/java/sample/OAuth2AuthorizationCodeGrantApplication.java rename to samples/boot/authcodegrant/src/main/java/sample/OAuth2AuthorizationCodeGrantApplication.java diff --git a/samples/boot/oauth2/authcodegrant/src/main/java/sample/config/SecurityConfig.java b/samples/boot/authcodegrant/src/main/java/sample/config/SecurityConfig.java similarity index 100% rename from samples/boot/oauth2/authcodegrant/src/main/java/sample/config/SecurityConfig.java rename to samples/boot/authcodegrant/src/main/java/sample/config/SecurityConfig.java diff --git a/samples/boot/oauth2/authcodegrant/src/main/java/sample/config/WebClientConfig.java b/samples/boot/authcodegrant/src/main/java/sample/config/WebClientConfig.java similarity index 100% rename from samples/boot/oauth2/authcodegrant/src/main/java/sample/config/WebClientConfig.java rename to samples/boot/authcodegrant/src/main/java/sample/config/WebClientConfig.java diff --git a/samples/boot/oauth2/authcodegrant/src/main/java/sample/web/GitHubReposController.java b/samples/boot/authcodegrant/src/main/java/sample/web/GitHubReposController.java similarity index 100% rename from samples/boot/oauth2/authcodegrant/src/main/java/sample/web/GitHubReposController.java rename to samples/boot/authcodegrant/src/main/java/sample/web/GitHubReposController.java diff --git a/samples/boot/oauth2/authcodegrant/src/main/resources/application.yml b/samples/boot/authcodegrant/src/main/resources/application.yml similarity index 100% rename from samples/boot/oauth2/authcodegrant/src/main/resources/application.yml rename to samples/boot/authcodegrant/src/main/resources/application.yml diff --git a/samples/boot/oauth2/authcodegrant/src/main/resources/templates/github-repos.html b/samples/boot/authcodegrant/src/main/resources/templates/github-repos.html similarity index 100% rename from samples/boot/oauth2/authcodegrant/src/main/resources/templates/github-repos.html rename to samples/boot/authcodegrant/src/main/resources/templates/github-repos.html diff --git a/samples/boot/oauth2/oauth2login-webflux/README.adoc b/samples/boot/oauth2login-webflux/README.adoc similarity index 100% rename from samples/boot/oauth2/oauth2login-webflux/README.adoc rename to samples/boot/oauth2login-webflux/README.adoc diff --git a/samples/boot/oauth2/oauth2login-webflux/spring-security-samples-boot-oauth2-oauth2login-webflux.gradle b/samples/boot/oauth2login-webflux/spring-security-samples-boot-oauth2login-webflux.gradle similarity index 100% rename from samples/boot/oauth2/oauth2login-webflux/spring-security-samples-boot-oauth2-oauth2login-webflux.gradle rename to samples/boot/oauth2login-webflux/spring-security-samples-boot-oauth2login-webflux.gradle diff --git a/samples/boot/oauth2/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientAutoConfiguration.java b/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientAutoConfiguration.java similarity index 100% rename from samples/boot/oauth2/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientAutoConfiguration.java rename to samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientAutoConfiguration.java diff --git a/samples/boot/oauth2/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientRegistrationRepositoryConfiguration.java b/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientRegistrationRepositoryConfiguration.java similarity index 100% rename from samples/boot/oauth2/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientRegistrationRepositoryConfiguration.java rename to samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientRegistrationRepositoryConfiguration.java diff --git a/samples/boot/oauth2/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2WebSecurityConfiguration.java b/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2WebSecurityConfiguration.java similarity index 100% rename from samples/boot/oauth2/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2WebSecurityConfiguration.java rename to samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2WebSecurityConfiguration.java diff --git a/samples/boot/oauth2/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/package-info.java b/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/package-info.java similarity index 100% rename from samples/boot/oauth2/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/package-info.java rename to samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/package-info.java diff --git a/samples/boot/oauth2/oauth2login-webflux/src/main/java/sample/ReactiveOAuth2LoginApplication.java b/samples/boot/oauth2login-webflux/src/main/java/sample/ReactiveOAuth2LoginApplication.java similarity index 100% rename from samples/boot/oauth2/oauth2login-webflux/src/main/java/sample/ReactiveOAuth2LoginApplication.java rename to samples/boot/oauth2login-webflux/src/main/java/sample/ReactiveOAuth2LoginApplication.java diff --git a/samples/boot/oauth2/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java b/samples/boot/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java similarity index 100% rename from samples/boot/oauth2/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java rename to samples/boot/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java diff --git a/samples/boot/oauth2/oauth2login-webflux/src/main/resources/META-INF/spring.factories b/samples/boot/oauth2login-webflux/src/main/resources/META-INF/spring.factories similarity index 100% rename from samples/boot/oauth2/oauth2login-webflux/src/main/resources/META-INF/spring.factories rename to samples/boot/oauth2login-webflux/src/main/resources/META-INF/spring.factories diff --git a/samples/boot/oauth2/oauth2login-webflux/src/main/resources/application.yml b/samples/boot/oauth2login-webflux/src/main/resources/application.yml similarity index 100% rename from samples/boot/oauth2/oauth2login-webflux/src/main/resources/application.yml rename to samples/boot/oauth2login-webflux/src/main/resources/application.yml diff --git a/samples/boot/oauth2/oauth2login-webflux/src/main/resources/templates/index.html b/samples/boot/oauth2login-webflux/src/main/resources/templates/index.html similarity index 100% rename from samples/boot/oauth2/oauth2login-webflux/src/main/resources/templates/index.html rename to samples/boot/oauth2login-webflux/src/main/resources/templates/index.html diff --git a/samples/boot/oauth2/oauth2login/README.adoc b/samples/boot/oauth2login/README.adoc similarity index 100% rename from samples/boot/oauth2/oauth2login/README.adoc rename to samples/boot/oauth2login/README.adoc diff --git a/samples/boot/oauth2/oauth2login/spring-security-samples-boot-oauth2-oauth2login.gradle b/samples/boot/oauth2login/spring-security-samples-boot-oauth2login.gradle similarity index 100% rename from samples/boot/oauth2/oauth2login/spring-security-samples-boot-oauth2-oauth2login.gradle rename to samples/boot/oauth2login/spring-security-samples-boot-oauth2login.gradle diff --git a/samples/boot/oauth2/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java b/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java similarity index 100% rename from samples/boot/oauth2/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java rename to samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java diff --git a/samples/boot/oauth2/oauth2login/src/main/java/sample/OAuth2LoginApplication.java b/samples/boot/oauth2login/src/main/java/sample/OAuth2LoginApplication.java similarity index 100% rename from samples/boot/oauth2/oauth2login/src/main/java/sample/OAuth2LoginApplication.java rename to samples/boot/oauth2login/src/main/java/sample/OAuth2LoginApplication.java diff --git a/samples/boot/oauth2/oauth2login/src/main/java/sample/web/OAuth2LoginController.java b/samples/boot/oauth2login/src/main/java/sample/web/OAuth2LoginController.java similarity index 100% rename from samples/boot/oauth2/oauth2login/src/main/java/sample/web/OAuth2LoginController.java rename to samples/boot/oauth2login/src/main/java/sample/web/OAuth2LoginController.java diff --git a/samples/boot/oauth2/oauth2login/src/main/resources/application.yml b/samples/boot/oauth2login/src/main/resources/application.yml similarity index 100% rename from samples/boot/oauth2/oauth2login/src/main/resources/application.yml rename to samples/boot/oauth2login/src/main/resources/application.yml diff --git a/samples/boot/oauth2/oauth2login/src/main/resources/templates/index.html b/samples/boot/oauth2login/src/main/resources/templates/index.html similarity index 100% rename from samples/boot/oauth2/oauth2login/src/main/resources/templates/index.html rename to samples/boot/oauth2login/src/main/resources/templates/index.html From d29c9796032b59a788a49b59710e0ed4fd0ae21a Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Fri, 8 Jun 2018 10:50:15 -0400 Subject: [PATCH 048/226] Fix package tangle in OAuth2Configurer Fixes gh-5342 --- .../annotation/web/builders/HttpSecurity.java | 4 +- .../configurers/oauth2/OAuth2Configurer.java | 48 +++++++++++++++---- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index da2416d4b78..ac22562cd43 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -999,8 +999,8 @@ public OAuth2LoginConfigurer oauth2Login() throws Exception { * @return the {@link OAuth2Configurer} for further customizations * @throws Exception */ - public OAuth2Configurer oauth2() throws Exception { - return getOrApply(new OAuth2Configurer()); + public OAuth2Configurer oauth2() throws Exception { + return getOrApply(new OAuth2Configurer<>()); } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java index 60b5e498558..a1cba10d999 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java @@ -15,6 +15,8 @@ */ package org.springframework.security.config.annotation.web.configurers.oauth2; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2ClientConfigurer; @@ -29,7 +31,11 @@ * @see OAuth2ClientConfigurer * @see AbstractHttpConfigurer */ -public final class OAuth2Configurer extends AbstractHttpConfigurer { +public final class OAuth2Configurer> + extends AbstractHttpConfigurer, B> { + + private final OAuth2ClientConfigurer clientConfigurer = new OAuth2ClientConfigurer<>(); + private boolean clientEnabled; /** * Returns the {@link OAuth2ClientConfigurer} for configuring OAuth 2.0 Client support. @@ -37,16 +43,40 @@ public final class OAuth2Configurer extends AbstractHttpConfigurer client() throws Exception { - return this.getOrApply(new OAuth2ClientConfigurer<>()); + public OAuth2ClientConfigurer client() throws Exception { + this.clientEnabled = true; + return this.clientConfigurer; } - @SuppressWarnings("unchecked") - private > C getOrApply(C configurer) throws Exception { - C existingConfigurer = (C) this.getBuilder().getConfigurer(configurer.getClass()); - if (existingConfigurer != null) { - return existingConfigurer; + @Override + public void init(B builder) throws Exception { + if (this.clientEnabled) { + this.clientConfigurer.init(builder); } - return this.getBuilder().apply(configurer); + } + + @Override + public void configure(B builder) throws Exception { + if (this.clientEnabled) { + this.clientConfigurer.configure(builder); + } + } + + @Override + public void setBuilder(B builder) { + this.clientConfigurer.setBuilder(builder); + super.setBuilder(builder); + } + + @Override + public void addObjectPostProcessor(ObjectPostProcessor objectPostProcessor) { + this.clientConfigurer.addObjectPostProcessor(objectPostProcessor); + super.addObjectPostProcessor(objectPostProcessor); + } + + @Override + public OAuth2Configurer withObjectPostProcessor(ObjectPostProcessor objectPostProcessor) { + this.clientConfigurer.withObjectPostProcessor(objectPostProcessor); + return super.withObjectPostProcessor(objectPostProcessor); } } From 64ac24e8b7cb9917a900b3189a712f86002762d0 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Fri, 8 Jun 2018 17:33:21 -0400 Subject: [PATCH 049/226] Rename @OAuth2Client to @RegisteredOAuth2AuthorizedClient Fixes gh-5360 --- .../OAuth2ClientConfiguration.java | 14 +- .../ReactiveOAuth2ClientImportSelector.java | 20 +-- .../OAuth2ClientConfigurationTests.java | 53 +------ .../client/OAuth2ClientConfigurerTests.java | 4 +- ... => RegisteredOAuth2AuthorizedClient.java} | 29 +--- ...uth2AuthorizedClientArgumentResolver.java} | 66 +++----- ...uth2AuthorizedClientArgumentResolver.java} | 55 ++----- ...uthorizedClientArgumentResolverTests.java} | 128 ++-------------- ...uthorizedClientArgumentResolverTests.java} | 145 +++--------------- .../sample/web/GitHubReposController.java | 4 +- .../sample/web/OAuth2LoginController.java | 4 +- .../sample/web/OAuth2LoginController.java | 4 +- 12 files changed, 94 insertions(+), 432 deletions(-) rename oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/annotation/{OAuth2Client.java => RegisteredOAuth2AuthorizedClient.java} (62%) rename oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/{OAuth2ClientArgumentResolver.java => OAuth2AuthorizedClientArgumentResolver.java} (56%) rename oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/{OAuth2ClientArgumentResolver.java => OAuth2AuthorizedClientArgumentResolver.java} (61%) rename oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/{OAuth2ClientArgumentResolverTests.java => OAuth2AuthorizedClientArgumentResolverTests.java} (51%) rename oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/{OAuth2ClientArgumentResolverTests.java => OAuth2AuthorizedClientArgumentResolverTests.java} (52%) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java index e0ff29b5992..86cfdc65f2f 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java @@ -21,8 +21,7 @@ import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.web.method.annotation.OAuth2ClientArgumentResolver; +import org.springframework.security.oauth2.client.web.method.annotation.OAuth2AuthorizedClientArgumentResolver; import org.springframework.util.ClassUtils; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -58,18 +57,15 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { @Configuration static class OAuth2ClientWebMvcSecurityConfiguration implements WebMvcConfigurer { - @Autowired(required = false) - private ClientRegistrationRepository clientRegistrationRepository; - @Autowired(required = false) private OAuth2AuthorizedClientService authorizedClientService; @Override public void addArgumentResolvers(List argumentResolvers) { - if (this.clientRegistrationRepository != null && this.authorizedClientService != null) { - OAuth2ClientArgumentResolver oauth2ClientArgumentResolver = new OAuth2ClientArgumentResolver( - this.clientRegistrationRepository, this.authorizedClientService); - argumentResolvers.add(oauth2ClientArgumentResolver); + if (this.authorizedClientService != null) { + OAuth2AuthorizedClientArgumentResolver authorizedClientArgumentResolver = + new OAuth2AuthorizedClientArgumentResolver(this.authorizedClientService); + argumentResolvers.add(authorizedClientArgumentResolver); } } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java index c5d2ddb6dac..6c0603049dc 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java @@ -16,19 +16,18 @@ package org.springframework.security.config.annotation.web.reactive; -import java.util.List; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; -import org.springframework.security.oauth2.client.web.reactive.result.method.annotation.OAuth2ClientArgumentResolver; +import org.springframework.security.oauth2.client.web.reactive.result.method.annotation.OAuth2AuthorizedClientArgumentResolver; import org.springframework.util.ClassUtils; import org.springframework.web.reactive.config.WebFluxConfigurer; import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; +import java.util.List; + /** * {@link Configuration} for OAuth 2.0 Client support. * @@ -52,21 +51,12 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { @Configuration static class OAuth2ClientWebFluxSecurityConfiguration implements WebFluxConfigurer { - private ReactiveClientRegistrationRepository clientRegistrationRepository; - private ReactiveOAuth2AuthorizedClientService authorizedClientService; @Override public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { - if (this.clientRegistrationRepository != null && this.authorizedClientService != null) { - configurer.addCustomResolver(new OAuth2ClientArgumentResolver(this.clientRegistrationRepository, this.authorizedClientService)); - } - } - - @Autowired(required = false) - public void setClientRegistrationRepository(List clientRegistrationRepository) { - if (clientRegistrationRepository.size() == 1) { - this.clientRegistrationRepository = clientRegistrationRepository.get(0); + if (this.authorizedClientService != null) { + configurer.addCustomResolver(new OAuth2AuthorizedClientArgumentResolver(this.authorizedClientService)); } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java index c4eb67cd157..e2f84b2d86b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java @@ -23,11 +23,7 @@ import org.springframework.security.config.test.SpringTestRule; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.annotation.OAuth2Client; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.test.web.servlet.MockMvc; import org.springframework.web.bind.annotation.GetMapping; @@ -54,24 +50,10 @@ public class OAuth2ClientConfigurationTests { private MockMvc mockMvc; @Test - public void requestWhenAuthorizedClientFoundThenOAuth2ClientArgumentsResolved() throws Exception { + public void requestWhenAuthorizedClientFoundThenMethodArgumentResolved() throws Exception { String clientRegistrationId = "client1"; String principalName = "user1"; - ClientRegistrationRepository clientRegistrationRepository = mock(ClientRegistrationRepository.class); - ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(clientRegistrationId) - .clientId("client-id") - .clientSecret("secret") - .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUriTemplate("{baseUrl}/client1") - .scope("scope1", "scope2") - .authorizationUri("https://provider.com/oauth2/auth") - .tokenUri("https://provider.com/oauth2/token") - .clientName("Client 1") - .build(); - when(clientRegistrationRepository.findByRegistrationId(clientRegistrationId)).thenReturn(clientRegistration); - OAuth2AuthorizedClientService authorizedClientService = mock(OAuth2AuthorizedClientService.class); OAuth2AuthorizedClient authorizedClient = mock(OAuth2AuthorizedClient.class); when(authorizedClientService.loadAuthorizedClient(clientRegistrationId, principalName)).thenReturn(authorizedClient); @@ -79,25 +61,17 @@ public void requestWhenAuthorizedClientFoundThenOAuth2ClientArgumentsResolved() OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class); when(authorizedClient.getAccessToken()).thenReturn(accessToken); - OAuth2ClientArgumentResolverConfig.CLIENT_REGISTRATION_REPOSITORY = clientRegistrationRepository; - OAuth2ClientArgumentResolverConfig.AUTHORIZED_CLIENT_SERVICE = authorizedClientService; - this.spring.register(OAuth2ClientArgumentResolverConfig.class).autowire(); + OAuth2AuthorizedClientArgumentResolverConfig.AUTHORIZED_CLIENT_SERVICE = authorizedClientService; + this.spring.register(OAuth2AuthorizedClientArgumentResolverConfig.class).autowire(); - this.mockMvc.perform(get("/access-token").with(user(principalName))) - .andExpect(status().isOk()) - .andExpect(content().string("resolved")); this.mockMvc.perform(get("/authorized-client").with(user(principalName))) .andExpect(status().isOk()) .andExpect(content().string("resolved")); - this.mockMvc.perform(get("/client-registration").with(user(principalName))) - .andExpect(status().isOk()) - .andExpect(content().string("resolved")); } @EnableWebMvc @EnableWebSecurity - static class OAuth2ClientArgumentResolverConfig extends WebSecurityConfigurerAdapter { - static ClientRegistrationRepository CLIENT_REGISTRATION_REPOSITORY; + static class OAuth2AuthorizedClientArgumentResolverConfig extends WebSecurityConfigurerAdapter { static OAuth2AuthorizedClientService AUTHORIZED_CLIENT_SERVICE; @Override @@ -107,25 +81,10 @@ protected void configure(HttpSecurity http) throws Exception { @RestController public class Controller { - @GetMapping("/access-token") - public String accessToken(@OAuth2Client("client1") OAuth2AccessToken accessToken) { - return accessToken != null ? "resolved" : "not-resolved"; - } - @GetMapping("/authorized-client") - public String authorizedClient(@OAuth2Client("client1") OAuth2AuthorizedClient authorizedClient) { + public String authorizedClient(@RegisteredOAuth2AuthorizedClient("client1") OAuth2AuthorizedClient authorizedClient) { return authorizedClient != null ? "resolved" : "not-resolved"; } - - @GetMapping("/client-registration") - public String clientRegistration(@OAuth2Client("client1") ClientRegistration clientRegistration) { - return clientRegistration != null ? "resolved" : "not-resolved"; - } - } - - @Bean - public ClientRegistrationRepository clientRegistrationRepository() { - return CLIENT_REGISTRATION_REPOSITORY; } @Bean diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java index ca12f7556c6..dfc5a5f9f43 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java @@ -30,7 +30,7 @@ import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.annotation.OAuth2Client; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -205,7 +205,7 @@ public OAuth2AuthorizedClientService authorizedClientService() { @RestController public class ResourceController { @GetMapping("/resource1") - public String resource1(@OAuth2Client("registration-1") OAuth2AuthorizedClient authorizedClient) { + public String resource1(@RegisteredOAuth2AuthorizedClient("registration-1") OAuth2AuthorizedClient authorizedClient) { return "resource1"; } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/annotation/OAuth2Client.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/annotation/RegisteredOAuth2AuthorizedClient.java similarity index 62% rename from oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/annotation/OAuth2Client.java rename to oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/annotation/RegisteredOAuth2AuthorizedClient.java index c8bd1c7af66..7bc975fab16 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/annotation/OAuth2Client.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/annotation/RegisteredOAuth2AuthorizedClient.java @@ -17,9 +17,7 @@ import org.springframework.core.annotation.AliasFor; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.web.method.annotation.OAuth2ClientArgumentResolver; -import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.client.web.method.annotation.OAuth2AuthorizedClientArgumentResolver; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -28,40 +26,29 @@ import java.lang.annotation.Target; /** - * This annotation may be used to resolve a method parameter into an argument value - * for the following types: {@link ClientRegistration}, {@link OAuth2AuthorizedClient} - * and {@link OAuth2AccessToken}. + * This annotation may be used to resolve a method parameter + * to an argument value of type {@link OAuth2AuthorizedClient}. * *

    * For example: *

      * @Controller
      * public class MyController {
    - *     @GetMapping("/client-registration")
    - *     public String clientRegistration(@OAuth2Client("login-client") ClientRegistration clientRegistration) {
    - *         // do something with clientRegistration
    - *     }
    - *
      *     @GetMapping("/authorized-client")
    - *     public String authorizedClient(@OAuth2Client("login-client") OAuth2AuthorizedClient authorizedClient) {
    + *     public String authorizedClient(@RegisteredOAuth2AuthorizedClient("login-client") OAuth2AuthorizedClient authorizedClient) {
      *         // do something with authorizedClient
      *     }
    - *
    - *     @GetMapping("/access-token")
    - *     public String accessToken(@OAuth2Client("login-client") OAuth2AccessToken accessToken) {
    - *         // do something with accessToken
    - *     }
      * }
      * 
    * * @author Joe Grandja * @since 5.1 - * @see OAuth2ClientArgumentResolver + * @see OAuth2AuthorizedClientArgumentResolver */ @Target({ ElementType.PARAMETER, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented -public @interface OAuth2Client { +public @interface RegisteredOAuth2AuthorizedClient { /** * Sets the client registration identifier. @@ -74,8 +61,8 @@ /** * The default attribute for this annotation. * This is an alias for {@link #registrationId()}. - * For example, {@code @OAuth2Client("login-client")} is equivalent to - * {@code @OAuth2Client(registrationId="login-client")}. + * For example, {@code @RegisteredOAuth2AuthorizedClient("login-client")} is equivalent to + * {@code @RegisteredOAuth2AuthorizedClient(registrationId="login-client")}. * * @return the client registration identifier */ diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2ClientArgumentResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java similarity index 56% rename from oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2ClientArgumentResolver.java rename to oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java index 695af81b540..e5c0fd1b954 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2ClientArgumentResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java @@ -24,11 +24,8 @@ import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.annotation.OAuth2Client; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -38,60 +35,43 @@ /** * An implementation of a {@link HandlerMethodArgumentResolver} that is capable - * of resolving a method parameter into an argument value for the following types: - * {@link ClientRegistration}, {@link OAuth2AuthorizedClient} and {@link OAuth2AccessToken}. + * of resolving a method parameter to an argument value of type {@link OAuth2AuthorizedClient}. * *

    * For example: *

      * @Controller
      * public class MyController {
    - *     @GetMapping("/client-registration")
    - *     public String clientRegistration(@OAuth2Client("login-client") ClientRegistration clientRegistration) {
    - *         // do something with clientRegistration
    - *     }
    - *
      *     @GetMapping("/authorized-client")
    - *     public String authorizedClient(@OAuth2Client("login-client") OAuth2AuthorizedClient authorizedClient) {
    + *     public String authorizedClient(@RegisteredOAuth2AuthorizedClient("login-client") OAuth2AuthorizedClient authorizedClient) {
      *         // do something with authorizedClient
      *     }
    - *
    - *     @GetMapping("/access-token")
    - *     public String accessToken(@OAuth2Client("login-client") OAuth2AccessToken accessToken) {
    - *         // do something with accessToken
    - *     }
      * }
      * 
    * * @author Joe Grandja * @since 5.1 - * @see OAuth2Client + * @see RegisteredOAuth2AuthorizedClient */ -public final class OAuth2ClientArgumentResolver implements HandlerMethodArgumentResolver { - private final ClientRegistrationRepository clientRegistrationRepository; +public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMethodArgumentResolver { private final OAuth2AuthorizedClientService authorizedClientService; /** - * Constructs an {@code OAuth2ClientArgumentResolver} using the provided parameters. + * Constructs an {@code OAuth2AuthorizedClientArgumentResolver} using the provided parameters. * - * @param clientRegistrationRepository the repository of client registrations * @param authorizedClientService the authorized client service */ - public OAuth2ClientArgumentResolver(ClientRegistrationRepository clientRegistrationRepository, - OAuth2AuthorizedClientService authorizedClientService) { - Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); + public OAuth2AuthorizedClientArgumentResolver(OAuth2AuthorizedClientService authorizedClientService) { Assert.notNull(authorizedClientService, "authorizedClientService cannot be null"); - this.clientRegistrationRepository = clientRegistrationRepository; this.authorizedClientService = authorizedClientService; } @Override public boolean supportsParameter(MethodParameter parameter) { Class parameterType = parameter.getParameterType(); - return ((OAuth2AccessToken.class.isAssignableFrom(parameterType) || - OAuth2AuthorizedClient.class.isAssignableFrom(parameterType) || - ClientRegistration.class.isAssignableFrom(parameterType)) && - (AnnotatedElementUtils.findMergedAnnotation(parameter.getParameter(), OAuth2Client.class) != null)); + return (OAuth2AuthorizedClient.class.isAssignableFrom(parameterType) && + (AnnotatedElementUtils.findMergedAnnotation( + parameter.getParameter(), RegisteredOAuth2AuthorizedClient.class) != null)); } @NonNull @@ -101,30 +81,21 @@ public Object resolveArgument(MethodParameter parameter, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { - OAuth2Client oauth2ClientAnnotation = AnnotatedElementUtils.findMergedAnnotation( - parameter.getParameter(), OAuth2Client.class); + RegisteredOAuth2AuthorizedClient authorizedClientAnnotation = AnnotatedElementUtils.findMergedAnnotation( + parameter.getParameter(), RegisteredOAuth2AuthorizedClient.class); Authentication principal = SecurityContextHolder.getContext().getAuthentication(); String clientRegistrationId = null; - if (!StringUtils.isEmpty(oauth2ClientAnnotation.registrationId())) { - clientRegistrationId = oauth2ClientAnnotation.registrationId(); - } else if (!StringUtils.isEmpty(oauth2ClientAnnotation.value())) { - clientRegistrationId = oauth2ClientAnnotation.value(); + if (!StringUtils.isEmpty(authorizedClientAnnotation.registrationId())) { + clientRegistrationId = authorizedClientAnnotation.registrationId(); + } else if (!StringUtils.isEmpty(authorizedClientAnnotation.value())) { + clientRegistrationId = authorizedClientAnnotation.value(); } else if (principal != null && OAuth2AuthenticationToken.class.isAssignableFrom(principal.getClass())) { clientRegistrationId = ((OAuth2AuthenticationToken) principal).getAuthorizedClientRegistrationId(); } if (StringUtils.isEmpty(clientRegistrationId)) { throw new IllegalArgumentException("Unable to resolve the Client Registration Identifier. " + - "It must be provided via @OAuth2Client(\"client1\") or @OAuth2Client(registrationId = \"client1\")."); - } - - if (ClientRegistration.class.isAssignableFrom(parameter.getParameterType())) { - ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId); - if (clientRegistration == null) { - throw new IllegalArgumentException("Unable to find ClientRegistration with registration identifier \"" + - clientRegistrationId + "\"."); - } - return clientRegistration; + "It must be provided via @RegisteredOAuth2AuthorizedClient(\"client1\") or @RegisteredOAuth2AuthorizedClient(registrationId = \"client1\")."); } if (principal == null) { @@ -140,7 +111,6 @@ public Object resolveArgument(MethodParameter parameter, throw new ClientAuthorizationRequiredException(clientRegistrationId); } - return OAuth2AccessToken.class.isAssignableFrom(parameter.getParameterType()) ? - authorizedClient.getAccessToken() : authorizedClient; + return authorizedClient; } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2ClientArgumentResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolver.java similarity index 61% rename from oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2ClientArgumentResolver.java rename to oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolver.java index 4be413668a4..6b679cc6799 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2ClientArgumentResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolver.java @@ -24,92 +24,65 @@ import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.annotation.OAuth2Client; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; -import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.reactive.BindingContext; import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; import org.springframework.web.server.ServerWebExchange; - import reactor.core.publisher.Mono; /** * An implementation of a {@link HandlerMethodArgumentResolver} that is capable - * of resolving a method parameter into an argument value for the following types: - * {@link ClientRegistration}, {@link OAuth2AuthorizedClient} and {@link OAuth2AccessToken}. + * of resolving a method parameter to an argument value of type {@link OAuth2AuthorizedClient}. * *

    * For example: *

      * @Controller
      * public class MyController {
    - *     @GetMapping("/client-registration")
    - *     public Mono clientRegistration(@OAuth2Client("login-client") ClientRegistration clientRegistration) {
    - *         // do something with clientRegistration
    - *     }
    - *
      *     @GetMapping("/authorized-client")
    - *     public Mono authorizedClient(@OAuth2Client("login-client") OAuth2AuthorizedClient authorizedClient) {
    + *     public Mono<String> authorizedClient(@RegisteredOAuth2AuthorizedClient("login-client") OAuth2AuthorizedClient authorizedClient) {
      *         // do something with authorizedClient
      *     }
    - *
    - *     @GetMapping("/access-token")
    - *     public Mono accessToken(@OAuth2Client("login-client") OAuth2AccessToken accessToken) {
    - *         // do something with accessToken
    - *     }
      * }
      * 
    * * @author Rob Winch * @since 5.1 - * @see OAuth2Client + * @see RegisteredOAuth2AuthorizedClient */ -public final class OAuth2ClientArgumentResolver implements HandlerMethodArgumentResolver { - private final ReactiveClientRegistrationRepository clientRegistrationRepository; +public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMethodArgumentResolver { private final ReactiveOAuth2AuthorizedClientService authorizedClientService; /** - * Constructs an {@code OAuth2ClientArgumentResolver} using the provided parameters. + * Constructs an {@code OAuth2AuthorizedClientArgumentResolver} using the provided parameters. * - * @param clientRegistrationRepository the repository of client registrations * @param authorizedClientService the authorized client service */ - public OAuth2ClientArgumentResolver(ReactiveClientRegistrationRepository clientRegistrationRepository, - ReactiveOAuth2AuthorizedClientService authorizedClientService) { - Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); + public OAuth2AuthorizedClientArgumentResolver(ReactiveOAuth2AuthorizedClientService authorizedClientService) { Assert.notNull(authorizedClientService, "authorizedClientService cannot be null"); - this.clientRegistrationRepository = clientRegistrationRepository; this.authorizedClientService = authorizedClientService; } @Override public boolean supportsParameter(MethodParameter parameter) { - return AnnotatedElementUtils.findMergedAnnotation(parameter.getParameter(), OAuth2Client.class) != null; + return AnnotatedElementUtils.findMergedAnnotation(parameter.getParameter(), RegisteredOAuth2AuthorizedClient.class) != null; } @Override public Mono resolveArgument( MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) { return Mono.defer(() -> { - OAuth2Client oauth2ClientAnnotation = AnnotatedElementUtils - .findMergedAnnotation(parameter.getParameter(), OAuth2Client.class); + RegisteredOAuth2AuthorizedClient authorizedClientAnnotation = AnnotatedElementUtils + .findMergedAnnotation(parameter.getParameter(), RegisteredOAuth2AuthorizedClient.class); - Mono clientRegistrationId = Mono.justOrEmpty(oauth2ClientAnnotation.registrationId()) + Mono clientRegistrationId = Mono.justOrEmpty(authorizedClientAnnotation.registrationId()) .filter(id -> !StringUtils.isEmpty(id)) .switchIfEmpty(clientRegistrationId()) .switchIfEmpty(Mono.defer(() -> Mono.error(new IllegalArgumentException( - "Unable to resolve the Client Registration Identifier. It must be provided via @OAuth2Client(\"client1\") or @OAuth2Client(registrationId = \"client1\").")))); - - if (ClientRegistration.class.isAssignableFrom(parameter.getParameterType())) { - return clientRegistrationId.flatMap(id -> this.clientRegistrationRepository.findByRegistrationId(id) - .switchIfEmpty(Mono.defer(() -> Mono.error(new IllegalArgumentException( - "Unable to find ClientRegistration with registration identifier \"" - + id + "\"."))))).cast(Object.class); - } + "Unable to resolve the Client Registration Identifier. It must be provided via @RegisteredOAuth2AuthorizedClient(\"client1\") or @RegisteredOAuth2AuthorizedClient(registrationId = \"client1\").")))); Mono principalName = ReactiveSecurityContextHolder.getContext() .map(SecurityContext::getAuthentication).map(Authentication::getName); @@ -129,10 +102,6 @@ public Mono resolveArgument( registrationId)))); }).cast(OAuth2AuthorizedClient.class); - if (OAuth2AccessToken.class.isAssignableFrom(parameter.getParameterType())) { - return authorizedClient.map(OAuth2AuthorizedClient::getAccessToken); - } - return authorizedClient.cast(Object.class); }); } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2ClientArgumentResolverTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java similarity index 51% rename from oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2ClientArgumentResolverTests.java rename to oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java index 5ad0fed1574..65fd59725c9 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2ClientArgumentResolverTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java @@ -24,13 +24,8 @@ import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.annotation.OAuth2Client; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.core.ClientAuthenticationMethod; -import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.util.ReflectionUtils; import java.lang.reflect.Method; @@ -43,69 +38,32 @@ import static org.mockito.Mockito.when; /** - * Tests for {@link OAuth2ClientArgumentResolver}. + * Tests for {@link OAuth2AuthorizedClientArgumentResolver}. * * @author Joe Grandja */ -public class OAuth2ClientArgumentResolverTests { - private ClientRegistrationRepository clientRegistrationRepository; +public class OAuth2AuthorizedClientArgumentResolverTests { private OAuth2AuthorizedClientService authorizedClientService; - private OAuth2ClientArgumentResolver argumentResolver; - private ClientRegistration clientRegistration; + private OAuth2AuthorizedClientArgumentResolver argumentResolver; private OAuth2AuthorizedClient authorizedClient; - private OAuth2AccessToken accessToken; @Before public void setUp() { - this.clientRegistrationRepository = mock(ClientRegistrationRepository.class); this.authorizedClientService = mock(OAuth2AuthorizedClientService.class); - this.argumentResolver = new OAuth2ClientArgumentResolver( - this.clientRegistrationRepository, this.authorizedClientService); - this.clientRegistration = ClientRegistration.withRegistrationId("client1") - .clientId("client-id") - .clientSecret("secret") - .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUriTemplate("{baseUrl}/client1") - .scope("scope1", "scope2") - .authorizationUri("https://provider.com/oauth2/auth") - .tokenUri("https://provider.com/oauth2/token") - .clientName("Client 1") - .build(); - when(this.clientRegistrationRepository.findByRegistrationId(anyString())).thenReturn(this.clientRegistration); + this.argumentResolver = new OAuth2AuthorizedClientArgumentResolver(this.authorizedClientService); this.authorizedClient = mock(OAuth2AuthorizedClient.class); when(this.authorizedClientService.loadAuthorizedClient(anyString(), any())).thenReturn(this.authorizedClient); - this.accessToken = mock(OAuth2AccessToken.class); - when(this.authorizedClient.getAccessToken()).thenReturn(this.accessToken); SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); securityContext.setAuthentication(mock(Authentication.class)); SecurityContextHolder.setContext(securityContext); } - @Test - public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2ClientArgumentResolver(null, this.authorizedClientService)) - .isInstanceOf(IllegalArgumentException.class); - } - @Test public void constructorWhenOAuth2AuthorizedClientServiceIsNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2ClientArgumentResolver(this.clientRegistrationRepository, null)) + assertThatThrownBy(() -> new OAuth2AuthorizedClientArgumentResolver(null)) .isInstanceOf(IllegalArgumentException.class); } - @Test - public void supportsParameterWhenParameterTypeOAuth2AccessTokenThenTrue() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeAccessToken", OAuth2AccessToken.class); - assertThat(this.argumentResolver.supportsParameter(methodParameter)).isTrue(); - } - - @Test - public void supportsParameterWhenParameterTypeOAuth2AccessTokenWithoutAnnotationThenFalse() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeAccessTokenWithoutAnnotation", OAuth2AccessToken.class); - assertThat(this.argumentResolver.supportsParameter(methodParameter)).isFalse(); - } - @Test public void supportsParameterWhenParameterTypeOAuth2AuthorizedClientThenTrue() { MethodParameter methodParameter = this.getMethodParameter("paramTypeAuthorizedClient", OAuth2AuthorizedClient.class); @@ -118,18 +76,6 @@ public void supportsParameterWhenParameterTypeOAuth2AuthorizedClientWithoutAnnot assertThat(this.argumentResolver.supportsParameter(methodParameter)).isFalse(); } - @Test - public void supportsParameterWhenParameterTypeClientRegistrationThenTrue() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeClientRegistration", ClientRegistration.class); - assertThat(this.argumentResolver.supportsParameter(methodParameter)).isTrue(); - } - - @Test - public void supportsParameterWhenParameterTypeClientRegistrationWithoutAnnotationThenFalse() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeClientRegistrationWithoutAnnotation", ClientRegistration.class); - assertThat(this.argumentResolver.supportsParameter(methodParameter)).isFalse(); - } - @Test public void supportsParameterWhenParameterTypeUnsupportedThenFalse() { MethodParameter methodParameter = this.getMethodParameter("paramTypeUnsupported", String.class); @@ -144,10 +90,10 @@ public void supportsParameterWhenParameterTypeUnsupportedWithoutAnnotationThenFa @Test public void resolveArgumentWhenRegistrationIdEmptyAndNotOAuth2AuthenticationThenThrowIllegalArgumentException() throws Exception { - MethodParameter methodParameter = this.getMethodParameter("registrationIdEmpty", OAuth2AccessToken.class); + MethodParameter methodParameter = this.getMethodParameter("registrationIdEmpty", OAuth2AuthorizedClient.class); assertThatThrownBy(() -> this.argumentResolver.resolveArgument(methodParameter, null, null, null)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Unable to resolve the Client Registration Identifier. It must be provided via @OAuth2Client(\"client1\") or @OAuth2Client(registrationId = \"client1\")."); + .hasMessage("Unable to resolve the Client Registration Identifier. It must be provided via @RegisteredOAuth2AuthorizedClient(\"client1\") or @RegisteredOAuth2AuthorizedClient(registrationId = \"client1\")."); } @Test @@ -157,25 +103,10 @@ public void resolveArgumentWhenRegistrationIdEmptyAndOAuth2AuthenticationThenRes SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); securityContext.setAuthentication(authentication); SecurityContextHolder.setContext(securityContext); - MethodParameter methodParameter = this.getMethodParameter("registrationIdEmpty", OAuth2AccessToken.class); + MethodParameter methodParameter = this.getMethodParameter("registrationIdEmpty", OAuth2AuthorizedClient.class); this.argumentResolver.resolveArgument(methodParameter, null, null, null); } - @Test - public void resolveArgumentWhenClientRegistrationFoundThenResolves() throws Exception { - MethodParameter methodParameter = this.getMethodParameter("paramTypeClientRegistration", ClientRegistration.class); - assertThat(this.argumentResolver.resolveArgument(methodParameter, null, null, null)).isSameAs(this.clientRegistration); - } - - @Test - public void resolveArgumentWhenClientRegistrationNotFoundThenThrowIllegalArgumentException() throws Exception { - when(this.clientRegistrationRepository.findByRegistrationId(anyString())).thenReturn(null); - MethodParameter methodParameter = this.getMethodParameter("paramTypeClientRegistration", ClientRegistration.class); - assertThatThrownBy(() -> this.argumentResolver.resolveArgument(methodParameter, null, null, null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Unable to find ClientRegistration with registration identifier \"client1\"."); - } - @Test public void resolveArgumentWhenParameterTypeOAuth2AuthorizedClientAndCurrentAuthenticationNullThenThrowIllegalStateException() throws Exception { SecurityContextHolder.clearContext(); @@ -201,60 +132,25 @@ public void resolveArgumentWhenOAuth2AuthorizedClientNotFoundThenThrowClientAuth .isInstanceOf(ClientAuthorizationRequiredException.class); } - @Test - public void resolveArgumentWhenOAuth2AccessTokenAndOAuth2AuthorizedClientFoundThenResolves() throws Exception { - MethodParameter methodParameter = this.getMethodParameter("paramTypeAccessToken", OAuth2AccessToken.class); - assertThat(this.argumentResolver.resolveArgument(methodParameter, null, null, null)).isSameAs(this.authorizedClient.getAccessToken()); - } - - @Test - public void resolveArgumentWhenOAuth2AccessTokenAndOAuth2AuthorizedClientNotFoundThenThrowClientAuthorizationRequiredException() throws Exception { - when(this.authorizedClientService.loadAuthorizedClient(anyString(), any())).thenReturn(null); - MethodParameter methodParameter = this.getMethodParameter("paramTypeAccessToken", OAuth2AccessToken.class); - assertThatThrownBy(() -> this.argumentResolver.resolveArgument(methodParameter, null, null, null)) - .isInstanceOf(ClientAuthorizationRequiredException.class); - } - - @Test - public void resolveArgumentWhenOAuth2AccessTokenAndAnnotationRegistrationIdSetThenResolves() throws Exception { - MethodParameter methodParameter = this.getMethodParameter("paramTypeAccessTokenAnnotationRegistrationId", OAuth2AccessToken.class); - assertThat(this.argumentResolver.resolveArgument(methodParameter, null, null, null)).isSameAs(this.authorizedClient.getAccessToken()); - } - private MethodParameter getMethodParameter(String methodName, Class... paramTypes) { Method method = ReflectionUtils.findMethod(TestController.class, methodName, paramTypes); return new MethodParameter(method, 0); } static class TestController { - void paramTypeAccessToken(@OAuth2Client("client1") OAuth2AccessToken accessToken) { - } - - void paramTypeAccessTokenWithoutAnnotation(OAuth2AccessToken accessToken) { - } - - void paramTypeAuthorizedClient(@OAuth2Client("client1") OAuth2AuthorizedClient authorizedClient) { + void paramTypeAuthorizedClient(@RegisteredOAuth2AuthorizedClient("client1") OAuth2AuthorizedClient authorizedClient) { } void paramTypeAuthorizedClientWithoutAnnotation(OAuth2AuthorizedClient authorizedClient) { } - void paramTypeClientRegistration(@OAuth2Client("client1") ClientRegistration clientRegistration) { - } - - void paramTypeClientRegistrationWithoutAnnotation(ClientRegistration clientRegistration) { - } - - void paramTypeUnsupported(@OAuth2Client("client1") String param) { + void paramTypeUnsupported(@RegisteredOAuth2AuthorizedClient("client1") String param) { } void paramTypeUnsupportedWithoutAnnotation(String param) { } - void registrationIdEmpty(@OAuth2Client OAuth2AccessToken accessToken) { - } - - void paramTypeAccessTokenAnnotationRegistrationId(@OAuth2Client(registrationId = "client1") OAuth2AccessToken accessToken) { + void registrationIdEmpty(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) { } } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2ClientArgumentResolverTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java similarity index 52% rename from oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2ClientArgumentResolverTests.java rename to oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java index aedcbaae57d..10ba47c4b89 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2ClientArgumentResolverTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java @@ -16,15 +16,6 @@ package org.springframework.security.oauth2.client.web.reactive.result.method.annotation; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.lang.reflect.Method; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -37,83 +28,49 @@ import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.annotation.OAuth2Client; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.core.ClientAuthenticationMethod; -import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.util.ReflectionUtils; - import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; import reactor.util.context.Context; +import java.lang.reflect.Method; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + /** * @author Rob Winch * @since 5.1 */ @RunWith(MockitoJUnitRunner.class) -public class OAuth2ClientArgumentResolverTests { - @Mock - private ReactiveClientRegistrationRepository clientRegistrationRepository; +public class OAuth2AuthorizedClientArgumentResolverTests { @Mock private ReactiveOAuth2AuthorizedClientService authorizedClientService; - private OAuth2ClientArgumentResolver argumentResolver; - private ClientRegistration clientRegistration; + private OAuth2AuthorizedClientArgumentResolver argumentResolver; private OAuth2AuthorizedClient authorizedClient; - private OAuth2AccessToken accessToken; private Authentication authentication = new TestingAuthenticationToken("test", "this"); @Before public void setUp() { - this.argumentResolver = new OAuth2ClientArgumentResolver( - this.clientRegistrationRepository, this.authorizedClientService); - this.clientRegistration = ClientRegistration.withRegistrationId("client1") - .clientId("client-id") - .clientSecret("secret") - .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUriTemplate("{baseUrl}/client1") - .scope("scope1", "scope2") - .authorizationUri("https://provider.com/oauth2/auth") - .tokenUri("https://provider.com/oauth2/token") - .clientName("Client 1") - .build(); - when(this.clientRegistrationRepository.findByRegistrationId(anyString())).thenReturn(Mono.just(this.clientRegistration)); + this.argumentResolver = new OAuth2AuthorizedClientArgumentResolver(this.authorizedClientService); this.authorizedClient = mock(OAuth2AuthorizedClient.class); when(this.authorizedClientService.loadAuthorizedClient(anyString(), any())).thenReturn(Mono.just(this.authorizedClient)); - this.accessToken = mock(OAuth2AccessToken.class); - when(this.authorizedClient.getAccessToken()).thenReturn(this.accessToken); Hooks.onOperatorDebug(); } - @Test - public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2ClientArgumentResolver(null, this.authorizedClientService)) - .isInstanceOf(IllegalArgumentException.class); - } - @Test public void constructorWhenOAuth2AuthorizedClientServiceIsNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2ClientArgumentResolver(this.clientRegistrationRepository, null)) + assertThatThrownBy(() -> new OAuth2AuthorizedClientArgumentResolver(null)) .isInstanceOf(IllegalArgumentException.class); } - @Test - public void supportsParameterWhenParameterTypeOAuth2AccessTokenThenTrue() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeAccessToken", OAuth2AccessToken.class); - assertThat(this.argumentResolver.supportsParameter(methodParameter)).isTrue(); - } - - @Test - public void supportsParameterWhenParameterTypeOAuth2AccessTokenWithoutAnnotationThenFalse() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeAccessTokenWithoutAnnotation", OAuth2AccessToken.class); - assertThat(this.argumentResolver.supportsParameter(methodParameter)).isFalse(); - } - @Test public void supportsParameterWhenParameterTypeOAuth2AuthorizedClientThenTrue() { MethodParameter methodParameter = this.getMethodParameter("paramTypeAuthorizedClient", OAuth2AuthorizedClient.class); @@ -126,18 +83,6 @@ public void supportsParameterWhenParameterTypeOAuth2AuthorizedClientWithoutAnnot assertThat(this.argumentResolver.supportsParameter(methodParameter)).isFalse(); } - @Test - public void supportsParameterWhenParameterTypeClientRegistrationThenTrue() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeClientRegistration", ClientRegistration.class); - assertThat(this.argumentResolver.supportsParameter(methodParameter)).isTrue(); - } - - @Test - public void supportsParameterWhenParameterTypeClientRegistrationWithoutAnnotationThenFalse() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeClientRegistrationWithoutAnnotation", ClientRegistration.class); - assertThat(this.argumentResolver.supportsParameter(methodParameter)).isFalse(); - } - @Test public void supportsParameterWhenParameterTypeUnsupportedWithoutAnnotationThenFalse() { MethodParameter methodParameter = this.getMethodParameter("paramTypeUnsupportedWithoutAnnotation", String.class); @@ -146,10 +91,10 @@ public void supportsParameterWhenParameterTypeUnsupportedWithoutAnnotationThenFa @Test public void resolveArgumentWhenRegistrationIdEmptyAndNotOAuth2AuthenticationThenThrowIllegalArgumentException() { - MethodParameter methodParameter = this.getMethodParameter("registrationIdEmpty", OAuth2AccessToken.class); + MethodParameter methodParameter = this.getMethodParameter("registrationIdEmpty", OAuth2AuthorizedClient.class); assertThatThrownBy(() -> resolveArgument(methodParameter)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Unable to resolve the Client Registration Identifier. It must be provided via @OAuth2Client(\"client1\") or @OAuth2Client(registrationId = \"client1\")."); + .hasMessage("Unable to resolve the Client Registration Identifier. It must be provided via @RegisteredOAuth2AuthorizedClient(\"client1\") or @RegisteredOAuth2AuthorizedClient(registrationId = \"client1\")."); } @Test @@ -157,25 +102,10 @@ public void resolveArgumentWhenRegistrationIdEmptyAndOAuth2AuthenticationThenRes this.authentication = mock(OAuth2AuthenticationToken.class); when(this.authentication.getName()).thenReturn("client1"); when(((OAuth2AuthenticationToken) this.authentication).getAuthorizedClientRegistrationId()).thenReturn("client1"); - MethodParameter methodParameter = this.getMethodParameter("registrationIdEmpty", OAuth2AccessToken.class); + MethodParameter methodParameter = this.getMethodParameter("registrationIdEmpty", OAuth2AuthorizedClient.class); resolveArgument(methodParameter); } - @Test - public void resolveArgumentWhenClientRegistrationFoundThenResolves() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeClientRegistration", ClientRegistration.class); - assertThat(resolveArgument(methodParameter)).isSameAs(this.clientRegistration); - } - - @Test - public void resolveArgumentWhenClientRegistrationNotFoundThenThrowIllegalArgumentException() { - when(this.clientRegistrationRepository.findByRegistrationId(anyString())).thenReturn(Mono.empty()); - MethodParameter methodParameter = this.getMethodParameter("paramTypeClientRegistration", ClientRegistration.class); - assertThatThrownBy(() -> resolveArgument(methodParameter)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Unable to find ClientRegistration with registration identifier \"client1\"."); - } - @Test public void resolveArgumentWhenParameterTypeOAuth2AuthorizedClientAndCurrentAuthenticationNullThenThrowIllegalStateException() { this.authentication = null; @@ -201,26 +131,6 @@ public void resolveArgumentWhenOAuth2AuthorizedClientNotFoundThenThrowClientAuth .isInstanceOf(ClientAuthorizationRequiredException.class); } - @Test - public void resolveArgumentWhenOAuth2AccessTokenAndOAuth2AuthorizedClientFoundThenResolves() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeAccessToken", OAuth2AccessToken.class); - assertThat(resolveArgument(methodParameter)).isSameAs(this.authorizedClient.getAccessToken()); - } - - @Test - public void resolveArgumentWhenOAuth2AccessTokenAndOAuth2AuthorizedClientNotFoundThenThrowClientAuthorizationRequiredException() { - when(this.authorizedClientService.loadAuthorizedClient(anyString(), any())).thenReturn(Mono.empty()); - MethodParameter methodParameter = this.getMethodParameter("paramTypeAccessToken", OAuth2AccessToken.class); - assertThatThrownBy(() -> resolveArgument(methodParameter)) - .isInstanceOf(ClientAuthorizationRequiredException.class); - } - - @Test - public void resolveArgumentWhenOAuth2AccessTokenAndAnnotationRegistrationIdSetThenResolves() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeAccessTokenAnnotationRegistrationId", OAuth2AccessToken.class); - assertThat(resolveArgument(methodParameter)).isSameAs(this.authorizedClient.getAccessToken()); - } - private Object resolveArgument(MethodParameter methodParameter) { return this.argumentResolver.resolveArgument(methodParameter, null, null) .subscriberContext(this.authentication == null ? Context.empty() : ReactiveSecurityContextHolder.withAuthentication(this.authentication)) @@ -234,34 +144,19 @@ private MethodParameter getMethodParameter(String methodName, Class... paramT } static class TestController { - void paramTypeAccessToken(@OAuth2Client("client1") OAuth2AccessToken accessToken) { - } - - void paramTypeAccessTokenWithoutAnnotation(OAuth2AccessToken accessToken) { - } - - void paramTypeAuthorizedClient(@OAuth2Client("client1") OAuth2AuthorizedClient authorizedClient) { + void paramTypeAuthorizedClient(@RegisteredOAuth2AuthorizedClient("client1") OAuth2AuthorizedClient authorizedClient) { } void paramTypeAuthorizedClientWithoutAnnotation(OAuth2AuthorizedClient authorizedClient) { } - void paramTypeClientRegistration(@OAuth2Client("client1") ClientRegistration clientRegistration) { - } - - void paramTypeClientRegistrationWithoutAnnotation(ClientRegistration clientRegistration) { - } - - void paramTypeUnsupported(@OAuth2Client("client1") String param) { + void paramTypeUnsupported(@RegisteredOAuth2AuthorizedClient("client1") String param) { } void paramTypeUnsupportedWithoutAnnotation(String param) { } - void registrationIdEmpty(@OAuth2Client OAuth2AccessToken accessToken) { - } - - void paramTypeAccessTokenAnnotationRegistrationId(@OAuth2Client(registrationId = "client1") OAuth2AccessToken accessToken) { + void registrationIdEmpty(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) { } } } diff --git a/samples/boot/authcodegrant/src/main/java/sample/web/GitHubReposController.java b/samples/boot/authcodegrant/src/main/java/sample/web/GitHubReposController.java index fb1893fb8d3..76e5cab5029 100644 --- a/samples/boot/authcodegrant/src/main/java/sample/web/GitHubReposController.java +++ b/samples/boot/authcodegrant/src/main/java/sample/web/GitHubReposController.java @@ -16,7 +16,7 @@ package sample.web; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.annotation.OAuth2Client; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -44,7 +44,7 @@ public String index() { } @GetMapping("/repos") - public String gitHubRepos(Model model, @OAuth2Client("github") OAuth2AuthorizedClient authorizedClient) { + public String gitHubRepos(Model model, @RegisteredOAuth2AuthorizedClient("github") OAuth2AuthorizedClient authorizedClient) { String endpointUri = "https://api.github.com/user/repos"; List repos = this.webClient .get() diff --git a/samples/boot/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java b/samples/boot/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java index d781489baa6..a73325ebdd4 100644 --- a/samples/boot/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java +++ b/samples/boot/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java @@ -18,7 +18,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.annotation.OAuth2Client; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -32,7 +32,7 @@ public class OAuth2LoginController { @GetMapping("/") public String index(Model model, - @OAuth2Client OAuth2AuthorizedClient authorizedClient, + @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient, @AuthenticationPrincipal OAuth2User oauth2User) { model.addAttribute("userName", oauth2User.getName()); model.addAttribute("clientName", authorizedClient.getClientRegistration().getClientName()); diff --git a/samples/boot/oauth2login/src/main/java/sample/web/OAuth2LoginController.java b/samples/boot/oauth2login/src/main/java/sample/web/OAuth2LoginController.java index 867cd3703b9..10522edfff5 100644 --- a/samples/boot/oauth2login/src/main/java/sample/web/OAuth2LoginController.java +++ b/samples/boot/oauth2login/src/main/java/sample/web/OAuth2LoginController.java @@ -17,7 +17,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.annotation.OAuth2Client; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -32,7 +32,7 @@ public class OAuth2LoginController { @GetMapping("/") public String index(Model model, - @OAuth2Client OAuth2AuthorizedClient authorizedClient, + @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient, @AuthenticationPrincipal OAuth2User oauth2User) { model.addAttribute("userName", oauth2User.getName()); model.addAttribute("clientName", authorizedClient.getClientRegistration().getClientName()); From ecea9dcb830ce99ff94a7a3fc92558ab3070519d Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 11 Jun 2018 14:30:11 -0500 Subject: [PATCH 050/226] Add UserDetailsRepositoryReactiveAuthenticationManager.setScheduler Fixes: gh-5417 --- ...positoryReactiveAuthenticationManager.java | 21 ++++- ...oryReactiveAuthenticationManagerTests.java | 88 +++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java diff --git a/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java b/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java index 0dbab239782..c60ea15f5c5 100644 --- a/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java +++ b/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java @@ -23,6 +23,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.util.Assert; import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; /** @@ -37,6 +38,8 @@ public class UserDetailsRepositoryReactiveAuthenticationManager implements React private PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); + private Scheduler scheduler = Schedulers.parallel(); + public UserDetailsRepositoryReactiveAuthenticationManager(ReactiveUserDetailsService userDetailsService) { Assert.notNull(userDetailsService, "userDetailsService cannot be null"); this.userDetailsService = userDetailsService; @@ -46,7 +49,7 @@ public UserDetailsRepositoryReactiveAuthenticationManager(ReactiveUserDetailsSer public Mono authenticate(Authentication authentication) { final String username = authentication.getName(); return this.userDetailsService.findByUsername(username) - .publishOn(Schedulers.parallel()) + .publishOn(this.scheduler) .filter( u -> this.passwordEncoder.matches((String) authentication.getCredentials(), u.getPassword())) .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials")))) .map( u -> new UsernamePasswordAuthenticationToken(u, u.getPassword(), u.getAuthorities()) ); @@ -61,4 +64,20 @@ public void setPasswordEncoder(PasswordEncoder passwordEncoder) { Assert.notNull(passwordEncoder, "passwordEncoder cannot be null"); this.passwordEncoder = passwordEncoder; } + + /** + * Sets the {@link Scheduler} used by the {@link UserDetailsRepositoryReactiveAuthenticationManager}. + * The default is {@code Schedulers.parallel()} because modern password encoding is + * a CPU intensive task that is non blocking. This means validation is bounded by the + * number of CPUs. Some applications may want to customize the {@link Scheduler}. For + * example, if users are stuck using the insecure {@link org.springframework.security.crypto.password.NoOpPasswordEncoder} + * they might want to leverage {@code Schedulers.immediate()}. + * + * @param scheduler the {@link Scheduler} to use. Cannot be null. + * @since 5.0.6 + */ + public void setScheduler(Scheduler scheduler) { + Assert.notNull(scheduler, "scheduler cannot be null"); + this.scheduler = scheduler; + } } diff --git a/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java b/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java new file mode 100644 index 00000000000..238c075fc1a --- /dev/null +++ b/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authentication; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.PasswordEncoder; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + * @since 5.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class UserDetailsRepositoryReactiveAuthenticationManagerTests { + @Mock + private ReactiveUserDetailsService userDetailsService; + + @Mock + private PasswordEncoder encoder; + + @Mock + private Scheduler scheduler; + + private UserDetails user = User.withUsername("user") + .password("password") + .roles("USER") + .build(); + + private UserDetailsRepositoryReactiveAuthenticationManager manager; + + @Before + public void setup() { + this.manager = new UserDetailsRepositoryReactiveAuthenticationManager(this.userDetailsService); + when(this.scheduler.schedule(any())).thenAnswer(a -> { + Runnable r = a.getArgument(0); + return Schedulers.immediate().schedule(r); + }); + } + + @Test + public void setSchedulerWhenNullThenIllegalArgumentException() { + assertThatCode(() -> this.manager.setScheduler(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void authentiateWhenCustomSchedulerThenUsed() { + when(this.userDetailsService.findByUsername(any())).thenReturn(Mono.just(this.user)); + when(this.encoder.matches(any(), any())).thenReturn(true); + this.manager.setScheduler(this.scheduler); + this.manager.setPasswordEncoder(this.encoder); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + this.user, this.user.getPassword()); + + Authentication result = this.manager.authenticate(token).block(); + + verify(this.scheduler).schedule(any()); + } +} From b288f9ce4bcf855a4f0a3c870b3117d4f134e8de Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 11 Jun 2018 17:13:24 -0500 Subject: [PATCH 051/226] Add cross references to ReactorContextTestExecutionListener Fixes: gh-5418 --- docs/manual/src/docs/asciidoc/_includes/test/method.adoc | 1 + .../test/context/annotation/SecurityTestExecutionListeners.java | 2 ++ .../context/support/ReactorContextTestExecutionListener.java | 2 ++ .../support/WithSecurityContextTestExecutionListener.java | 2 ++ 4 files changed, 7 insertions(+) diff --git a/docs/manual/src/docs/asciidoc/_includes/test/method.adoc b/docs/manual/src/docs/asciidoc/_includes/test/method.adoc index a2cc64e51d2..e1550d0b382 100644 --- a/docs/manual/src/docs/asciidoc/_includes/test/method.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/test/method.adoc @@ -44,6 +44,7 @@ This is a basic example of how to setup Spring Security Test. The highlights are NOTE: Spring Security hooks into Spring Test support using the `WithSecurityContextTestExecutionListener` which will ensure our tests are ran with the correct user. It does this by populating the `SecurityContextHolder` prior to running our tests. +If you are using reactive method security, you will also need `ReactorContextTestExecutionListener` which populates `ReactiveSecurityContextHolder`. After the test is done, it will clear out the `SecurityContextHolder`. If you only need Spring Security related support, you can replace `@ContextConfiguration` with `@SecurityTestExecutionListeners`. diff --git a/test/src/main/java/org/springframework/security/test/context/annotation/SecurityTestExecutionListeners.java b/test/src/main/java/org/springframework/security/test/context/annotation/SecurityTestExecutionListeners.java index d8cab45f09a..406841eaee7 100644 --- a/test/src/main/java/org/springframework/security/test/context/annotation/SecurityTestExecutionListeners.java +++ b/test/src/main/java/org/springframework/security/test/context/annotation/SecurityTestExecutionListeners.java @@ -36,6 +36,8 @@ * * @author Rob Winch * @since 4.0.2 + * @see WithSecurityContextTestExecutionListener + * @see ReactorContextTestExecutionListener */ @Documented @Inherited diff --git a/test/src/main/java/org/springframework/security/test/context/support/ReactorContextTestExecutionListener.java b/test/src/main/java/org/springframework/security/test/context/support/ReactorContextTestExecutionListener.java index c6616897aff..d6e0f10faa3 100644 --- a/test/src/main/java/org/springframework/security/test/context/support/ReactorContextTestExecutionListener.java +++ b/test/src/main/java/org/springframework/security/test/context/support/ReactorContextTestExecutionListener.java @@ -37,6 +37,8 @@ * * @author Rob Winch * @since 5.0 + * @see WithSecurityContextTestExecutionListener + * @see org.springframework.security.test.context.annotation.SecurityTestExecutionListeners */ public class ReactorContextTestExecutionListener extends DelegatingTestExecutionListener { diff --git a/test/src/main/java/org/springframework/security/test/context/support/WithSecurityContextTestExecutionListener.java b/test/src/main/java/org/springframework/security/test/context/support/WithSecurityContextTestExecutionListener.java index f9fd4ecd087..a26552e3d6b 100644 --- a/test/src/main/java/org/springframework/security/test/context/support/WithSecurityContextTestExecutionListener.java +++ b/test/src/main/java/org/springframework/security/test/context/support/WithSecurityContextTestExecutionListener.java @@ -43,6 +43,8 @@ * @author Rob Winch * @author Eddú Meléndez * @since 4.0 + * @see ReactorContextTestExecutionListener + * @see org.springframework.security.test.context.annotation.SecurityTestExecutionListeners */ public class WithSecurityContextTestExecutionListener extends AbstractTestExecutionListener { From cf6f31d17b2156ac32798774dd5e10b5ecf5d694 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 11 Jun 2018 14:10:38 -0400 Subject: [PATCH 052/226] Associate Refresh Token to OAuth2AuthorizedClient Fixes gh-5416 --- .../oauth2/client/OAuth2AuthorizedClient.java | 30 +++++++++++- ...thorizationCodeAuthenticationProvider.java | 6 +-- ...2AuthorizationCodeAuthenticationToken.java | 29 ++++++++++++ .../OAuth2LoginAuthenticationProvider.java | 5 +- .../OAuth2LoginAuthenticationToken.java | 37 ++++++++++++++- ...th2LoginReactiveAuthenticationManager.java | 6 ++- ...sAuthorizationCodeTokenResponseClient.java | 8 +++- ...eAuthorizationCodeTokenResponseClient.java | 6 +++ ...thorizationCodeAuthenticationProvider.java | 10 ++-- .../OAuth2AuthorizationCodeGrantFilter.java | 3 +- .../web/OAuth2LoginAuthenticationFilter.java | 3 +- ...zationCodeAuthenticationProviderTests.java | 4 ++ ...Auth2LoginAuthenticationProviderTests.java | 6 ++- ...orizationCodeTokenResponseClientTests.java | 4 +- ...orizationCodeTokenResponseClientTests.java | 2 + ...zationCodeAuthenticationProviderTests.java | 7 ++- ...uth2AuthorizationCodeGrantFilterTests.java | 3 ++ .../OAuth2LoginAuthenticationFilterTests.java | 5 +- .../oauth2/core/AuthorizationGrantType.java | 3 +- .../oauth2/core/OAuth2RefreshToken.java | 46 +++++++++++++++++++ .../endpoint/OAuth2AccessTokenResponse.java | 37 ++++++++++++++- .../core/AuthorizationGrantTypeTests.java | 7 ++- .../OAuth2AccessTokenResponseTests.java | 5 +- 23 files changed, 245 insertions(+), 27 deletions(-) create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2RefreshToken.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClient.java index 392a947705a..298658324a0 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,10 @@ */ package org.springframework.security.oauth2.client; +import org.springframework.lang.Nullable; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.util.Assert; /** @@ -33,11 +35,13 @@ * @since 5.0 * @see ClientRegistration * @see OAuth2AccessToken + * @see OAuth2RefreshToken */ public class OAuth2AuthorizedClient { private final ClientRegistration clientRegistration; private final String principalName; private final OAuth2AccessToken accessToken; + private final OAuth2RefreshToken refreshToken; /** * Constructs an {@code OAuth2AuthorizedClient} using the provided parameters. @@ -47,12 +51,26 @@ public class OAuth2AuthorizedClient { * @param accessToken the access token credential granted */ public OAuth2AuthorizedClient(ClientRegistration clientRegistration, String principalName, OAuth2AccessToken accessToken) { + this(clientRegistration, principalName, accessToken, null); + } + + /** + * Constructs an {@code OAuth2AuthorizedClient} using the provided parameters. + * + * @param clientRegistration the authorized client's registration + * @param principalName the name of the End-User {@code Principal} (Resource Owner) + * @param accessToken the access token credential granted + * @param refreshToken the refresh token credential granted + */ + public OAuth2AuthorizedClient(ClientRegistration clientRegistration, String principalName, + OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken) { Assert.notNull(clientRegistration, "clientRegistration cannot be null"); Assert.hasText(principalName, "principalName cannot be empty"); Assert.notNull(accessToken, "accessToken cannot be null"); this.clientRegistration = clientRegistration; this.principalName = principalName; this.accessToken = accessToken; + this.refreshToken = refreshToken; } /** @@ -81,4 +99,14 @@ public String getPrincipalName() { public OAuth2AccessToken getAccessToken() { return this.accessToken; } + + /** + * Returns the {@link OAuth2RefreshToken refresh token} credential granted. + * + * @since 5.1 + * @return the {@link OAuth2RefreshToken} + */ + public @Nullable OAuth2RefreshToken getRefreshToken() { + return this.refreshToken; + } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java index c202a31f3e7..e88fc1e43ea 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java @@ -20,7 +20,6 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; -import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.util.Assert; @@ -69,13 +68,12 @@ public Authentication authenticate(Authentication authentication) throws Authent authorizationCodeAuthentication.getClientRegistration(), authorizationCodeAuthentication.getAuthorizationExchange())); - OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken(); - OAuth2AuthorizationCodeAuthenticationToken authenticationResult = new OAuth2AuthorizationCodeAuthenticationToken( authorizationCodeAuthentication.getClientRegistration(), authorizationCodeAuthentication.getAuthorizationExchange(), - accessToken); + accessTokenResponse.getAccessToken(), + accessTokenResponse.getRefreshToken()); authenticationResult.setDetails(authorizationCodeAuthentication.getDetails()); return authenticationResult; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java index bbc6d4aca1a..969a10a0a82 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java @@ -15,10 +15,12 @@ */ package org.springframework.security.oauth2.client.authentication; +import org.springframework.lang.Nullable; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.SpringSecurityCoreVersion; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.util.Assert; @@ -40,6 +42,7 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti private ClientRegistration clientRegistration; private OAuth2AuthorizationExchange authorizationExchange; private OAuth2AccessToken accessToken; + private OAuth2RefreshToken refreshToken; /** * This constructor should be used when the Authorization Request/Response is complete. @@ -67,9 +70,26 @@ public OAuth2AuthorizationCodeAuthenticationToken(ClientRegistration clientRegis public OAuth2AuthorizationCodeAuthenticationToken(ClientRegistration clientRegistration, OAuth2AuthorizationExchange authorizationExchange, OAuth2AccessToken accessToken) { + this(clientRegistration, authorizationExchange, accessToken, null); + } + + /** + * This constructor should be used when the Access Token Request/Response is complete, + * which indicates that the Authorization Code Grant flow has fully completed. + * + * @param clientRegistration the client registration + * @param authorizationExchange the authorization exchange + * @param accessToken the access token credential + * @param refreshToken the refresh token credential + */ + public OAuth2AuthorizationCodeAuthenticationToken(ClientRegistration clientRegistration, + OAuth2AuthorizationExchange authorizationExchange, + OAuth2AccessToken accessToken, + @Nullable OAuth2RefreshToken refreshToken) { this(clientRegistration, authorizationExchange); Assert.notNull(accessToken, "accessToken cannot be null"); this.accessToken = accessToken; + this.refreshToken = refreshToken; this.setAuthenticated(true); } @@ -111,4 +131,13 @@ public OAuth2AuthorizationExchange getAuthorizationExchange() { public OAuth2AccessToken getAccessToken() { return this.accessToken; } + + /** + * Returns the {@link OAuth2RefreshToken refresh token}. + * + * @return the {@link OAuth2RefreshToken} + */ + public @Nullable OAuth2RefreshToken getRefreshToken() { + return this.refreshToken; + } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java index d3c442f334b..843424df63e 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -113,7 +113,8 @@ public Authentication authenticate(Authentication authentication) throws Authent authorizationCodeAuthentication.getAuthorizationExchange(), oauth2User, mappedAuthorities, - accessToken); + accessToken, + accessTokenResponse.getRefreshToken()); authenticationResult.setDetails(authorizationCodeAuthentication.getDetails()); return authenticationResult; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationToken.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationToken.java index 0709ac7cd1a..32311da81e4 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationToken.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationToken.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +15,13 @@ */ package org.springframework.security.oauth2.client.authentication; +import org.springframework.lang.Nullable; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.SpringSecurityCoreVersion; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.util.Assert; @@ -46,6 +48,7 @@ public class OAuth2LoginAuthenticationToken extends AbstractAuthenticationToken private ClientRegistration clientRegistration; private OAuth2AuthorizationExchange authorizationExchange; private OAuth2AccessToken accessToken; + private OAuth2RefreshToken refreshToken; /** * This constructor should be used when the Authorization Request/Response is complete. @@ -80,6 +83,27 @@ public OAuth2LoginAuthenticationToken(ClientRegistration clientRegistration, OAuth2User principal, Collection authorities, OAuth2AccessToken accessToken) { + this(clientRegistration, authorizationExchange, principal, authorities, accessToken, null); + } + + /** + * This constructor should be used when the Access Token Request/Response is complete, + * which indicates that the Authorization Code Grant flow has fully completed + * and OAuth 2.0 Login has been achieved. + * + * @param clientRegistration the client registration + * @param authorizationExchange the authorization exchange + * @param principal the user {@code Principal} registered with the OAuth 2.0 Provider + * @param authorities the authorities granted to the user + * @param accessToken the access token credential + * @param refreshToken the refresh token credential + */ + public OAuth2LoginAuthenticationToken(ClientRegistration clientRegistration, + OAuth2AuthorizationExchange authorizationExchange, + OAuth2User principal, + Collection authorities, + OAuth2AccessToken accessToken, + @Nullable OAuth2RefreshToken refreshToken) { super(authorities); Assert.notNull(clientRegistration, "clientRegistration cannot be null"); Assert.notNull(authorizationExchange, "authorizationExchange cannot be null"); @@ -89,6 +113,7 @@ public OAuth2LoginAuthenticationToken(ClientRegistration clientRegistration, this.authorizationExchange = authorizationExchange; this.principal = principal; this.accessToken = accessToken; + this.refreshToken = refreshToken; this.setAuthenticated(true); } @@ -128,4 +153,14 @@ public OAuth2AuthorizationExchange getAuthorizationExchange() { public OAuth2AccessToken getAccessToken() { return this.accessToken; } + + /** + * Returns the {@link OAuth2RefreshToken refresh token}. + * + * @since 5.1 + * @return the {@link OAuth2RefreshToken} + */ + public @Nullable OAuth2RefreshToken getRefreshToken() { + return this.refreshToken; + } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManager.java index 8509db1f2f4..03aeca3397c 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManager.java @@ -120,11 +120,13 @@ private Mono authenticationResult(OAuth2LoginAuthenti authorizationCodeAuthentication.getAuthorizationExchange(), oauth2User, mappedAuthorities, - accessToken); + accessToken, + accessTokenResponse.getRefreshToken()); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( authenticationResult.getClientRegistration(), authenticationResult.getName(), - authenticationResult.getAccessToken()); + authenticationResult.getAccessToken(), + authenticationResult.getRefreshToken()); OAuth2AuthenticationToken result = new OAuth2AuthenticationToken( authenticationResult.getPrincipal(), authenticationResult.getAuthorities(), diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClient.java index ba791b7316a..1f1b4ee4c80 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -138,12 +138,18 @@ public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRe accessTokenResponse.getTokens().getAccessToken().getScope().toStringList()); } + String refreshToken = null; + if (accessTokenResponse.getTokens().getRefreshToken() != null) { + refreshToken = accessTokenResponse.getTokens().getRefreshToken().getValue(); + } + Map additionalParameters = new LinkedHashMap<>(accessTokenResponse.getCustomParameters()); return OAuth2AccessTokenResponse.withToken(accessToken) .tokenType(accessTokenType) .expiresIn(expiresIn) .scopes(scopes) + .refreshToken(refreshToken) .additionalParameters(additionalParameters) .build(); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClient.java index cb937ea12db..f92c6cf36f2 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClient.java @@ -118,6 +118,11 @@ public Mono getTokenResponse(OAuth2AuthorizationCodeG accessToken.getScope().toStringList()); } + String refreshToken = null; + if (accessTokenResponse.getTokens().getRefreshToken() != null) { + refreshToken = accessTokenResponse.getTokens().getRefreshToken().getValue(); + } + Map additionalParameters = new LinkedHashMap<>( accessTokenResponse.getCustomParameters()); @@ -125,6 +130,7 @@ public Mono getTokenResponse(OAuth2AuthorizationCodeG .tokenType(accessTokenType) .expiresIn(expiresIn) .scopes(scopes) + .refreshToken(refreshToken) .additionalParameters(additionalParameters) .build(); }); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java index 33f2be8840e..ba352769344 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,6 @@ import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; -import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; @@ -142,8 +141,6 @@ public Authentication authenticate(Authentication authentication) throws Authent authorizationCodeAuthentication.getClientRegistration(), authorizationCodeAuthentication.getAuthorizationExchange())); - OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken(); - ClientRegistration clientRegistration = authorizationCodeAuthentication.getClientRegistration(); if (!accessTokenResponse.getAdditionalParameters().containsKey(OidcParameterNames.ID_TOKEN)) { @@ -161,7 +158,7 @@ public Authentication authenticate(Authentication authentication) throws Authent this.validateIdToken(idToken, clientRegistration); OidcUser oidcUser = this.userService.loadUser( - new OidcUserRequest(clientRegistration, accessToken, idToken)); + new OidcUserRequest(clientRegistration, accessTokenResponse.getAccessToken(), idToken)); Collection mappedAuthorities = this.authoritiesMapper.mapAuthorities(oidcUser.getAuthorities()); @@ -171,7 +168,8 @@ public Authentication authenticate(Authentication authentication) throws Authent authorizationCodeAuthentication.getAuthorizationExchange(), oidcUser, mappedAuthorities, - accessToken); + accessTokenResponse.getAccessToken(), + accessTokenResponse.getRefreshToken()); authenticationResult.setDetails(authorizationCodeAuthentication.getDetails()); return authenticationResult; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java index 62ce8868a1a..eac55375a83 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java @@ -198,7 +198,8 @@ private void processAuthorizationResponse(HttpServletRequest request, HttpServle OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( authenticationResult.getClientRegistration(), currentAuthentication.getName(), - authenticationResult.getAccessToken()); + authenticationResult.getAccessToken(), + authenticationResult.getRefreshToken()); this.authorizedClientService.saveAuthorizedClient(authorizedClient, currentAuthentication); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java index 231b7f5c789..d9a3d9f9bc1 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java @@ -173,7 +173,8 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( authenticationResult.getClientRegistration(), oauth2Authentication.getName(), - authenticationResult.getAccessToken()); + authenticationResult.getAccessToken(), + authenticationResult.getRefreshToken()); this.authorizedClientService.saveAuthorizedClient(authorizedClient, oauth2Authentication); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java index ba88b9f570a..85176b0d081 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java @@ -27,6 +27,7 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; @@ -122,8 +123,10 @@ public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizatio @Test public void authenticateWhenAuthorizationSuccessResponseThenExchangedForAccessToken() { OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class); + OAuth2RefreshToken refreshToken = mock(OAuth2RefreshToken.class); OAuth2AccessTokenResponse accessTokenResponse = mock(OAuth2AccessTokenResponse.class); when(accessTokenResponse.getAccessToken()).thenReturn(accessToken); + when(accessTokenResponse.getRefreshToken()).thenReturn(refreshToken); when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); OAuth2AuthorizationCodeAuthenticationToken authenticationResult = @@ -137,5 +140,6 @@ public void authenticateWhenAuthorizationSuccessResponseThenExchangedForAccessTo assertThat(authenticationResult.getClientRegistration()).isEqualTo(this.clientRegistration); assertThat(authenticationResult.getAuthorizationExchange()).isEqualTo(this.authorizationExchange); assertThat(authenticationResult.getAccessToken()).isEqualTo(accessToken); + assertThat(authenticationResult.getRefreshToken()).isEqualTo(refreshToken); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProviderTests.java index 63c54dbf33c..668f007f156 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProviderTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; @@ -164,8 +165,10 @@ public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizatio @Test public void authenticateWhenLoginSuccessThenReturnAuthentication() { OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class); + OAuth2RefreshToken refreshToken = mock(OAuth2RefreshToken.class); OAuth2AccessTokenResponse accessTokenResponse = mock(OAuth2AccessTokenResponse.class); when(accessTokenResponse.getAccessToken()).thenReturn(accessToken); + when(accessTokenResponse.getRefreshToken()).thenReturn(refreshToken); when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); OAuth2User principal = mock(OAuth2User.class); @@ -185,6 +188,7 @@ public void authenticateWhenLoginSuccessThenReturnAuthentication() { assertThat(authentication.getClientRegistration()).isEqualTo(this.clientRegistration); assertThat(authentication.getAuthorizationExchange()).isEqualTo(this.authorizationExchange); assertThat(authentication.getAccessToken()).isEqualTo(accessToken); + assertThat(authentication.getRefreshToken()).isEqualTo(refreshToken); } @Test diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClientTests.java index 8c63eecb1a0..00d3f8b77be 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClientTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -91,6 +91,7 @@ public void getTokenResponseWhenSuccessResponseThenReturnAccessTokenResponse() t " \"token_type\": \"bearer\",\n" + " \"expires_in\": \"3600\",\n" + " \"scope\": \"openid profile\",\n" + + " \"refresh_token\": \"refresh-token-1234\",\n" + " \"custom_parameter_1\": \"custom-value-1\",\n" + " \"custom_parameter_2\": \"custom-value-2\"\n" + "}\n"; @@ -115,6 +116,7 @@ public void getTokenResponseWhenSuccessResponseThenReturnAccessTokenResponse() t assertThat(accessTokenResponse.getAccessToken().getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER); assertThat(accessTokenResponse.getAccessToken().getExpiresAt()).isBetween(expiresAtBefore, expiresAtAfter); assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("openid", "profile"); + assertThat(accessTokenResponse.getRefreshToken().getTokenValue()).isEqualTo("refresh-token-1234"); assertThat(accessTokenResponse.getAdditionalParameters().size()).isEqualTo(2); assertThat(accessTokenResponse.getAdditionalParameters()).containsEntry("custom_parameter_1", "custom-value-1"); assertThat(accessTokenResponse.getAdditionalParameters()).containsEntry("custom_parameter_2", "custom-value-2"); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClientTests.java index 6f3c93ad24d..2fe61e1e61d 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClientTests.java @@ -85,6 +85,7 @@ public void getTokenResponseWhenSuccessResponseThenReturnAccessTokenResponse() t " \"token_type\": \"bearer\",\n" + " \"expires_in\": \"3600\",\n" + " \"scope\": \"openid profile\",\n" + + " \"refresh_token\": \"refresh-token-1234\",\n" + " \"custom_parameter_1\": \"custom-value-1\",\n" + " \"custom_parameter_2\": \"custom-value-2\"\n" + "}\n"; @@ -102,6 +103,7 @@ public void getTokenResponseWhenSuccessResponseThenReturnAccessTokenResponse() t OAuth2AccessToken.TokenType.BEARER); assertThat(accessTokenResponse.getAccessToken().getExpiresAt()).isBetween(expiresAtBefore, expiresAtAfter); assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("openid", "profile"); + assertThat(accessTokenResponse.getRefreshToken().getTokenValue()).isEqualTo("refresh-token-1234"); assertThat(accessTokenResponse.getAdditionalParameters().size()).isEqualTo(2); assertThat(accessTokenResponse.getAdditionalParameters()).containsEntry("custom_parameter_1", "custom-value-1"); assertThat(accessTokenResponse.getAdditionalParameters()).containsEntry("custom_parameter_2", "custom-value-2"); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProviderTests.java index e963bc818e3..6be263b1bd4 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProviderTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; @@ -78,6 +79,7 @@ public class OidcAuthorizationCodeAuthenticationProviderTests { private OAuth2AccessTokenResponseClient accessTokenResponseClient; private OAuth2AccessTokenResponse accessTokenResponse; private OAuth2AccessToken accessToken; + private OAuth2RefreshToken refreshToken; private OAuth2UserService userService; private OidcAuthorizationCodeAuthenticationProvider authenticationProvider; @@ -95,6 +97,7 @@ public void setUp() throws Exception { this.accessTokenResponseClient = mock(OAuth2AccessTokenResponseClient.class); this.accessTokenResponse = mock(OAuth2AccessTokenResponse.class); this.accessToken = mock(OAuth2AccessToken.class); + this.refreshToken = mock(OAuth2RefreshToken.class); this.userService = mock(OAuth2UserService.class); this.authenticationProvider = PowerMockito.spy( new OidcAuthorizationCodeAuthenticationProvider(this.accessTokenResponseClient, this.userService)); @@ -109,6 +112,7 @@ public void setUp() throws Exception { when(this.authorizationRequest.getRedirectUri()).thenReturn("http://example.com"); when(this.authorizationResponse.getRedirectUri()).thenReturn("http://example.com"); when(this.accessTokenResponse.getAccessToken()).thenReturn(this.accessToken); + when(this.accessTokenResponse.getRefreshToken()).thenReturn(this.refreshToken); Map additionalParameters = new HashMap<>(); additionalParameters.put(OidcParameterNames.ID_TOKEN, "id-token"); when(this.accessTokenResponse.getAdditionalParameters()).thenReturn(additionalParameters); @@ -365,6 +369,7 @@ public void authenticateWhenLoginSuccessThenReturnAuthentication() throws Except assertThat(authentication.getClientRegistration()).isEqualTo(this.clientRegistration); assertThat(authentication.getAuthorizationExchange()).isEqualTo(this.authorizationExchange); assertThat(authentication.getAccessToken()).isEqualTo(this.accessToken); + assertThat(authentication.getRefreshToken()).isEqualTo(this.refreshToken); } @Test diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java index 6678d6e8b90..3093adc02b8 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java @@ -41,6 +41,7 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; @@ -238,6 +239,7 @@ public void doFilterWhenAuthorizationResponseSuccessThenAuthorizedClientSaved() assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1); assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principalName1); assertThat(authorizedClient.getAccessToken()).isNotNull(); + assertThat(authorizedClient.getRefreshToken()).isNotNull(); } @Test @@ -299,6 +301,7 @@ private void setUpAuthenticationResult(ClientRegistration registration) { when(authentication.getClientRegistration()).thenReturn(registration); when(authentication.getAuthorizationExchange()).thenReturn(mock(OAuth2AuthorizationExchange.class)); when(authentication.getAccessToken()).thenReturn(mock(OAuth2AccessToken.class)); + when(authentication.getRefreshToken()).thenReturn(mock(OAuth2RefreshToken.class)); when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn(authentication); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilterTests.java index 2b788c82456..9892851d57e 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,7 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; @@ -281,6 +282,7 @@ public void doFilterWhenAuthorizationResponseValidThenAuthorizedClientSaved() th assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1); assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principalName1); assertThat(authorizedClient.getAccessToken()).isNotNull(); + assertThat(authorizedClient.getRefreshToken()).isNotNull(); } @Test @@ -328,6 +330,7 @@ private void setUpAuthenticationResult(ClientRegistration registration) { when(loginAuthentication.getClientRegistration()).thenReturn(registration); when(loginAuthentication.getAuthorizationExchange()).thenReturn(mock(OAuth2AuthorizationExchange.class)); when(loginAuthentication.getAccessToken()).thenReturn(mock(OAuth2AccessToken.class)); + when(loginAuthentication.getRefreshToken()).thenReturn(mock(OAuth2RefreshToken.class)); when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn(loginAuthentication); } } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java index 61c596162eb..4e58a2c6f12 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ public final class AuthorizationGrantType implements Serializable { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; public static final AuthorizationGrantType AUTHORIZATION_CODE = new AuthorizationGrantType("authorization_code"); public static final AuthorizationGrantType IMPLICIT = new AuthorizationGrantType("implicit"); + public static final AuthorizationGrantType REFRESH_TOKEN = new AuthorizationGrantType("refresh_token"); private final String value; /** diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2RefreshToken.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2RefreshToken.java new file mode 100644 index 00000000000..7070c810248 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2RefreshToken.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.core; + +import java.time.Instant; + +/** + * An implementation of an {@link AbstractOAuth2Token} representing an OAuth 2.0 Refresh Token. + * + *

    + * A refresh token is a credential that represents an authorization + * granted by the resource owner to the client. + * It is used by the client to obtain a new access token when the current access token + * becomes invalid or expires, or to obtain additional access tokens with identical or narrower scope. + * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AccessToken + * @see Section 1.5 Refresh Token + */ +public class OAuth2RefreshToken extends AbstractOAuth2Token { + + /** + * Constructs an {@code OAuth2RefreshToken} using the provided parameters. + * + * @param tokenValue the token value + * @param issuedAt the time at which the token was issued + * @param expiresAt the expiration time on or after which the token MUST NOT be accepted + */ + public OAuth2RefreshToken(String tokenValue, Instant issuedAt, Instant expiresAt) { + super(tokenValue, issuedAt, expiresAt); + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java index 3630ee449c1..83015ee58b2 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,11 @@ */ package org.springframework.security.oauth2.core.endpoint; +import org.springframework.lang.Nullable; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; import java.time.Instant; import java.util.Collections; @@ -29,10 +32,12 @@ * @author Joe Grandja * @since 5.0 * @see OAuth2AccessToken + * @see OAuth2RefreshToken * @see Section 5.1 Access Token Response */ public final class OAuth2AccessTokenResponse { private OAuth2AccessToken accessToken; + private OAuth2RefreshToken refreshToken; private Map additionalParameters; private OAuth2AccessTokenResponse() { @@ -47,6 +52,16 @@ public OAuth2AccessToken getAccessToken() { return this.accessToken; } + /** + * Returns the {@link OAuth2RefreshToken Refresh Token}. + * + * @since 5.1 + * @return the {@link OAuth2RefreshToken} + */ + public @Nullable OAuth2RefreshToken getRefreshToken() { + return this.refreshToken; + } + /** * Returns the additional parameters returned in the response. * @@ -74,6 +89,7 @@ public static class Builder { private OAuth2AccessToken.TokenType tokenType; private long expiresIn; private Set scopes; + private String refreshToken; private Map additionalParameters; private Builder(String tokenValue) { @@ -113,6 +129,17 @@ public Builder scopes(Set scopes) { return this; } + /** + * Sets the refresh token associated to the access token. + * + * @param refreshToken the refresh token associated to the access token. + * @return the {@link Builder} + */ + public Builder refreshToken(String refreshToken) { + this.refreshToken = refreshToken; + return this; + } + /** * Sets the additional parameters returned in the response. * @@ -142,6 +169,14 @@ public OAuth2AccessTokenResponse build() { OAuth2AccessTokenResponse accessTokenResponse = new OAuth2AccessTokenResponse(); accessTokenResponse.accessToken = new OAuth2AccessToken( this.tokenType, this.tokenValue, issuedAt, expiresAt, this.scopes); + if (StringUtils.hasText(this.refreshToken)) { + // The Access Token response does not return an expires_in for the Refresh Token, + // therefore, we'll default to +1 second from issuedAt time. + // NOTE: + // The expiry or invalidity of a Refresh Token can only be determined by performing + // the refresh_token grant and if it fails than likely it has expired or has been invalidated. + accessTokenResponse.refreshToken = new OAuth2RefreshToken(this.refreshToken, issuedAt, issuedAt.plusSeconds(1)); + } accessTokenResponse.additionalParameters = Collections.unmodifiableMap( CollectionUtils.isEmpty(this.additionalParameters) ? Collections.emptyMap() : this.additionalParameters); return accessTokenResponse; diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthorizationGrantTypeTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthorizationGrantTypeTests.java index dea90e92b1e..2d94930c48c 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthorizationGrantTypeTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthorizationGrantTypeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,4 +40,9 @@ public void getValueWhenAuthorizationCodeGrantTypeThenReturnAuthorizationCode() public void getValueWhenImplicitGrantTypeThenReturnImplicit() { assertThat(AuthorizationGrantType.IMPLICIT.getValue()).isEqualTo("implicit"); } + + @Test + public void getValueWhenRefreshTokenGrantTypeThenReturnRefreshToken() { + assertThat(AuthorizationGrantType.REFRESH_TOKEN.getValue()).isEqualTo("refresh_token"); + } } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseTests.java index 2346347aad2..cfd5a0dedf6 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ */ public class OAuth2AccessTokenResponseTests { private static final String TOKEN_VALUE = "access-token"; + private static final String REFRESH_TOKEN_VALUE = "refresh-token"; private static final long EXPIRES_IN = Instant.now().plusSeconds(5).toEpochMilli(); @Test(expected = IllegalArgumentException.class) @@ -88,6 +89,7 @@ public void buildWhenAllAttributesProvidedThenAllAttributesAreSet() { .tokenType(OAuth2AccessToken.TokenType.BEARER) .expiresIn(expiresAt.toEpochMilli()) .scopes(scopes) + .refreshToken(REFRESH_TOKEN_VALUE) .additionalParameters(additionalParameters) .build(); @@ -97,6 +99,7 @@ public void buildWhenAllAttributesProvidedThenAllAttributesAreSet() { assertThat(tokenResponse.getAccessToken().getIssuedAt()).isNotNull(); assertThat(tokenResponse.getAccessToken().getExpiresAt()).isAfterOrEqualTo(expiresAt); assertThat(tokenResponse.getAccessToken().getScopes()).isEqualTo(scopes); + assertThat(tokenResponse.getRefreshToken().getTokenValue()).isEqualTo(REFRESH_TOKEN_VALUE); assertThat(tokenResponse.getAdditionalParameters()).isEqualTo(additionalParameters); } } From e1a4a513afdd7fe7a99ac74a67a859711ee8abd8 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 12 Jun 2018 16:53:31 -0500 Subject: [PATCH 053/226] Update GAE to 1.9.64 Fixes: gh-5422 --- gradle.properties | 2 +- gradle/dependency-management.gradle | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gradle.properties b/gradle.properties index ca6c51d374c..42df749d112 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -gaeVersion=1.9.63 +gaeVersion=1.9.64 springBootVersion=2.0.2.RELEASE version=5.1.0.BUILD-SNAPSHOT diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index ee0de7b7016..8b2675425f5 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -45,10 +45,10 @@ dependencyManagement { dependency 'com.fasterxml.jackson.core:jackson-databind:2.9.5' dependency 'com.fasterxml:classmate:1.3.4' dependency 'com.github.stephenc.jcip:jcip-annotations:1.0-1' - dependency 'com.google.appengine:appengine-api-1.0-sdk:1.9.63' - dependency 'com.google.appengine:appengine-api-labs:1.9.63' - dependency 'com.google.appengine:appengine-api-stubs:1.9.63' - dependency 'com.google.appengine:appengine-testing:1.9.63' + dependency 'com.google.appengine:appengine-api-1.0-sdk:1.9.64' + dependency 'com.google.appengine:appengine-api-labs:1.9.64' + dependency 'com.google.appengine:appengine-api-stubs:1.9.64' + dependency 'com.google.appengine:appengine-testing:1.9.64' dependency 'com.google.appengine:appengine:1.9.63' dependency 'com.google.code.gson:gson:2.8.2' dependency 'com.google.guava:guava:20.0' From 94ad9a339a1a10cf22c150d7f8fb35f132851714 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 12 Jun 2018 12:18:45 -0500 Subject: [PATCH 054/226] Update to Jackson 2.9.6 Fixes: gh-5424 --- gradle/dependency-management.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 8b2675425f5..7b28db16be2 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -40,9 +40,9 @@ dependencyManagement { dependency 'asm:asm:3.1' dependency 'ch.qos.logback:logback-classic:1.2.3' dependency 'ch.qos.logback:logback-core:1.2.3' - dependency 'com.fasterxml.jackson.core:jackson-annotations:2.9.5' - dependency 'com.fasterxml.jackson.core:jackson-core:2.9.5' - dependency 'com.fasterxml.jackson.core:jackson-databind:2.9.5' + dependency 'com.fasterxml.jackson.core:jackson-annotations:2.9.6' + dependency 'com.fasterxml.jackson.core:jackson-core:2.9.6' + dependency 'com.fasterxml.jackson.core:jackson-databind:2.9.6' dependency 'com.fasterxml:classmate:1.3.4' dependency 'com.github.stephenc.jcip:jcip-annotations:1.0-1' dependency 'com.google.appengine:appengine-api-1.0-sdk:1.9.64' From d8fa3c3ce3dc7694f4e9c58a15a1cb118126ff71 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 12 Jun 2018 12:37:51 -0500 Subject: [PATCH 055/226] Update to nimbus-jose-jwt:5.11 Fixes: gh-5436 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 7b28db16be2..75928fd7d50 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -54,7 +54,7 @@ dependencyManagement { dependency 'com.google.guava:guava:20.0' dependency 'com.google.inject:guice:3.0' dependency 'com.nimbusds:lang-tag:1.4.3' - dependency 'com.nimbusds:nimbus-jose-jwt:5.10' + dependency 'com.nimbusds:nimbus-jose-jwt:5.11' dependency 'com.nimbusds:oauth2-oidc-sdk:5.61' dependency 'com.squareup.okhttp3:okhttp:3.9.0' dependency 'com.squareup.okio:okio:1.13.0' From 3b83b6f255e0d93cc6e49843b38ae32023749525 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 12 Jun 2018 12:38:08 -0500 Subject: [PATCH 056/226] Update to oauth2-oidc-sdk:5.62 Fixes: gh-5435 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 75928fd7d50..30a1f73ddd5 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -55,7 +55,7 @@ dependencyManagement { dependency 'com.google.inject:guice:3.0' dependency 'com.nimbusds:lang-tag:1.4.3' dependency 'com.nimbusds:nimbus-jose-jwt:5.11' - dependency 'com.nimbusds:oauth2-oidc-sdk:5.61' + dependency 'com.nimbusds:oauth2-oidc-sdk:5.62' dependency 'com.squareup.okhttp3:okhttp:3.9.0' dependency 'com.squareup.okio:okio:1.13.0' dependency 'com.sun.xml.bind:jaxb-core:2.3.0' From 275f7e2fae2dafffe0f9634a82b4ec361ee2ad74 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 12 Jun 2018 12:38:25 -0500 Subject: [PATCH 057/226] Update to unboundid-ldapsdk:4.0.6 Fixes: gh-54234 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 30a1f73ddd5..3425e360e74 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -60,7 +60,7 @@ dependencyManagement { dependency 'com.squareup.okio:okio:1.13.0' dependency 'com.sun.xml.bind:jaxb-core:2.3.0' dependency 'com.sun.xml.bind:jaxb-impl:2.3.0' - dependency 'com.unboundid:unboundid-ldapsdk:4.0.5' + dependency 'com.unboundid:unboundid-ldapsdk:4.0.6' dependency 'com.vaadin.external.google:android-json:0.0.20131108.vaadin1' dependency 'commons-cli:commons-cli:1.4' dependency 'commons-codec:commons-codec:1.11' From 62a48c974dbe9dd9077d1f3df5b87a496f4c4f7d Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 12 Jun 2018 12:38:38 -0500 Subject: [PATCH 058/226] Update to htmlunit:2.31 Fixes: gh-5433 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 3425e360e74..e43a41c3e8f 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -89,7 +89,7 @@ dependencyManagement { dependency 'net.sf.ehcache:ehcache:2.10.4' dependency 'net.sourceforge.cssparser:cssparser:0.9.24' dependency 'net.sourceforge.htmlunit:htmlunit-core-js:2.28' - dependency 'net.sourceforge.htmlunit:htmlunit:2.30' + dependency 'net.sourceforge.htmlunit:htmlunit:2.31' dependency 'net.sourceforge.htmlunit:neko-htmlunit:2.28' dependency 'net.sourceforge.nekohtml:nekohtml:1.9.22' dependency 'nz.net.ultraq.thymeleaf:thymeleaf-expression-processor:1.1.3' From a142c789d8b5b2b591cdd5dfc8594a6629511f44 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 12 Jun 2018 12:38:49 -0500 Subject: [PATCH 059/226] Update to assertj-core:3.10.0 Fixes: gh-5432 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index e43a41c3e8f..28cb6cf4643 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -145,7 +145,7 @@ dependencyManagement { dependency 'org.aspectj:aspectjrt:1.9.1' dependency 'org.aspectj:aspectjtools:1.9.1' dependency 'org.aspectj:aspectjweaver:1.9.1' - dependency 'org.assertj:assertj-core:3.9.1' + dependency 'org.assertj:assertj-core:3.10.0' dependency 'org.attoparser:attoparser:2.0.4.RELEASE' dependency 'org.bouncycastle:bcpkix-jdk15on:1.59' dependency 'org.bouncycastle:bcprov-jdk15on:1.58' From 28adae9eb81389400d644ce7abf40bdf4f1eb83b Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 12 Jun 2018 12:39:07 -0500 Subject: [PATCH 060/226] Update to hsqldb:2.4.1 Fixes: gh-5431 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 28cb6cf4643..a4a28eadaa4 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -175,7 +175,7 @@ dependencyManagement { dependency 'org.hibernate:hibernate-core:5.2.12.Final' dependency 'org.hibernate:hibernate-entitymanager:5.2.16.Final' dependency 'org.hibernate:hibernate-validator:6.0.9.Final' - dependency 'org.hsqldb:hsqldb:2.4.0' + dependency 'org.hsqldb:hsqldb:2.4.1' dependency 'org.jasig.cas.client:cas-client-core:3.5.0' dependency 'org.javassist:javassist:3.22.0-CR2' dependency 'org.jboss.logging:jboss-logging:3.3.1.Final' From 356c73c7200b473ee97defb56441699676497ad7 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 12 Jun 2018 12:39:43 -0500 Subject: [PATCH 061/226] Update to Hibernate 5.2.17 Fixes: gh-5430 --- gradle/dependency-management.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index a4a28eadaa4..91e53e5a1b7 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -172,8 +172,8 @@ dependencyManagement { dependency 'org.hamcrest:hamcrest-core:1.3' dependency 'org.hibernate.common:hibernate-commons-annotations:5.0.1.Final' dependency 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final' - dependency 'org.hibernate:hibernate-core:5.2.12.Final' - dependency 'org.hibernate:hibernate-entitymanager:5.2.16.Final' + dependency 'org.hibernate:hibernate-core:5.2.17.Final' + dependency 'org.hibernate:hibernate-entitymanager:5.2.17.Final' dependency 'org.hibernate:hibernate-validator:6.0.9.Final' dependency 'org.hsqldb:hsqldb:2.4.1' dependency 'org.jasig.cas.client:cas-client-core:3.5.0' From 005b4af4339b8e00c9c7319db3cc54eced89fb5c Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 12 Jun 2018 12:40:18 -0500 Subject: [PATCH 062/226] Update to hibernate-validator:6.0.10.Final Fixes: gh-5429 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 91e53e5a1b7..178b4657048 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -174,7 +174,7 @@ dependencyManagement { dependency 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final' dependency 'org.hibernate:hibernate-core:5.2.17.Final' dependency 'org.hibernate:hibernate-entitymanager:5.2.17.Final' - dependency 'org.hibernate:hibernate-validator:6.0.9.Final' + dependency 'org.hibernate:hibernate-validator:6.0.10.Final' dependency 'org.hsqldb:hsqldb:2.4.1' dependency 'org.jasig.cas.client:cas-client-core:3.5.0' dependency 'org.javassist:javassist:3.22.0-CR2' From 777778b244edffc64055c64a77c9ab44c9a3b51e Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 12 Jun 2018 12:41:12 -0500 Subject: [PATCH 063/226] Update to htmlunit-driver:2.31.0 Fixes: gh-5428 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 178b4657048..a69040fd1e2 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -186,7 +186,7 @@ dependencyManagement { dependency 'org.openid4java:openid4java-nodeps:0.9.6' dependency 'org.ow2.asm:asm:6.0' dependency 'org.reactivestreams:reactive-streams:1.0.1' - dependency 'org.seleniumhq.selenium:htmlunit-driver:2.30.0' + dependency 'org.seleniumhq.selenium:htmlunit-driver:2.31.0' dependency 'org.seleniumhq.selenium:selenium-api:3.8.1' dependency 'org.seleniumhq.selenium:selenium-java:3.11.0' dependency 'org.seleniumhq.selenium:selenium-support:3.11.0' From 6bcf96d72714c48ce85f0073938fce8d7a1ee58d Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 12 Jun 2018 12:41:27 -0500 Subject: [PATCH 064/226] Update to Selenium 3.12.0 Fixes: gh-5427 --- gradle/dependency-management.gradle | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index a69040fd1e2..1062d0653b9 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -187,9 +187,8 @@ dependencyManagement { dependency 'org.ow2.asm:asm:6.0' dependency 'org.reactivestreams:reactive-streams:1.0.1' dependency 'org.seleniumhq.selenium:htmlunit-driver:2.31.0' - dependency 'org.seleniumhq.selenium:selenium-api:3.8.1' - dependency 'org.seleniumhq.selenium:selenium-java:3.11.0' - dependency 'org.seleniumhq.selenium:selenium-support:3.11.0' + dependency 'org.seleniumhq.selenium:selenium-java:3.12.0' + dependency 'org.seleniumhq.selenium:selenium-support:3.12.0' dependency 'org.skyscreamer:jsonassert:1.5.0' dependency 'org.slf4j:jcl-over-slf4j:1.7.25' dependency 'org.slf4j:jul-to-slf4j:1.7.25' From a86115faa89a7bec9656cd36205379abfd9f32ef Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 12 Jun 2018 14:44:16 -0500 Subject: [PATCH 065/226] Fix htmlunit Fixes: gh-5426 --- gradle/dependency-management.gradle | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 1062d0653b9..8bd35d5deab 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -87,10 +87,8 @@ dependencyManagement { dependency 'net.minidev:accessors-smart:1.2' dependency 'net.minidev:json-smart:2.3' dependency 'net.sf.ehcache:ehcache:2.10.4' - dependency 'net.sourceforge.cssparser:cssparser:0.9.24' - dependency 'net.sourceforge.htmlunit:htmlunit-core-js:2.28' dependency 'net.sourceforge.htmlunit:htmlunit:2.31' - dependency 'net.sourceforge.htmlunit:neko-htmlunit:2.28' + dependency 'net.sourceforge.htmlunit:neko-htmlunit:2.31' dependency 'net.sourceforge.nekohtml:nekohtml:1.9.22' dependency 'nz.net.ultraq.thymeleaf:thymeleaf-expression-processor:1.1.3' dependency 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:2.3.0' From ffefd6dea6301d114c300c8bd1bfef9eeb99c448 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 15 Jun 2018 11:27:28 -0500 Subject: [PATCH 066/226] Override toString() in all RequestMatcher It makes it easier to debug having custom toString(). Fixes: gh-5446 --- .../util/matcher/MvcRequestMatcher.java | 18 +++++++++++++ .../web/util/matcher/AnyRequestMatcher.java | 5 ++++ .../web/util/matcher/ELRequestMatcher.java | 7 +++++ .../web/util/matcher/RegexRequestMatcher.java | 14 ++++++++++ .../util/matcher/MvcRequestMatcherTests.java | 27 +++++++++++++++++++ .../util/matcher/ELRequestMatcherTests.java | 6 +++++ .../matcher/RegexRequestMatcherTests.java | 6 +++++ 7 files changed, 83 insertions(+) diff --git a/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java b/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java index facc8208880..5205dfc34c4 100644 --- a/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java @@ -123,6 +123,24 @@ protected final String getServletPath() { return this.servletPath; } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Mvc [pattern='").append(this.pattern).append("'"); + + if (this.servletPath != null) { + sb.append(", servletPath='").append(this.servletPath).append("'"); + } + + if (this.method != null) { + sb.append(", ").append(this.method); + } + + sb.append("]"); + + return sb.toString(); + } + private class DefaultMatcher implements RequestMatcher, RequestVariablesExtractor { private final UrlPathHelper pathHelper = new UrlPathHelper(); diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/AnyRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/AnyRequestMatcher.java index c18268d9f66..f3931d8ac03 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/AnyRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/AnyRequestMatcher.java @@ -44,6 +44,11 @@ public int hashCode() { return 1; } + @Override + public String toString() { + return "any request"; + } + private AnyRequestMatcher() { } } diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/ELRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/ELRequestMatcher.java index 501efe08b01..3a77019570c 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/ELRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/ELRequestMatcher.java @@ -65,4 +65,11 @@ public EvaluationContext createELContext(HttpServletRequest request) { return new StandardEvaluationContext(new ELRequestMatcherContext(request)); } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("EL [el=\"").append(this.expression.getExpressionString()).append("\""); + sb.append("]"); + return sb.toString(); + } } diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/RegexRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/RegexRequestMatcher.java index 64659649a90..0c54f8ad011 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/RegexRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/RegexRequestMatcher.java @@ -130,4 +130,18 @@ private static HttpMethod valueOf(String method) { return null; } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Regex [pattern='").append(this.pattern).append("'"); + + if (this.httpMethod != null) { + sb.append(", ").append(this.httpMethod); + } + + sb.append("]"); + + return sb.toString(); + } } diff --git a/web/src/test/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcherTests.java index 7d3e260ac44..5cdd1346633 100644 --- a/web/src/test/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcherTests.java @@ -216,4 +216,31 @@ public void matchesGetMatchableHandlerMappingThrows() throws Exception { new HttpRequestMethodNotSupportedException(this.request.getMethod())); assertThat(this.matcher.matches(this.request)).isTrue(); } + + @Test + public void toStringWhenAll() { + this.matcher.setMethod(HttpMethod.GET); + this.matcher.setServletPath("/spring"); + + assertThat(this.matcher.toString()).isEqualTo("Mvc [pattern='/path', servletPath='/spring', GET]"); + } + + @Test + public void toStringWhenHttpMethod() { + this.matcher.setMethod(HttpMethod.GET); + + assertThat(this.matcher.toString()).isEqualTo("Mvc [pattern='/path', GET]"); + } + + @Test + public void toStringWhenServletPath() { + this.matcher.setServletPath("/spring"); + + assertThat(this.matcher.toString()).isEqualTo("Mvc [pattern='/path', servletPath='/spring']"); + } + + @Test + public void toStringWhenOnlyPattern() { + assertThat(this.matcher.toString()).isEqualTo("Mvc [pattern='/path']"); + } } diff --git a/web/src/test/java/org/springframework/security/web/util/matcher/ELRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/util/matcher/ELRequestMatcherTests.java index 66f70e2f0ad..b4b53dacee3 100644 --- a/web/src/test/java/org/springframework/security/web/util/matcher/ELRequestMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/util/matcher/ELRequestMatcherTests.java @@ -90,4 +90,10 @@ public void testHasHeaderNull() throws Exception { assertThat(requestMatcher.matches(request)).isFalse(); } + @Test + public void toStringThenFormatted() { + ELRequestMatcher requestMatcher = new ELRequestMatcher( + "hasHeader('User-Agent','MSIE')"); + assertThat(requestMatcher.toString()).isEqualTo("EL [el=\"hasHeader('User-Agent','MSIE')\"]"); + } } diff --git a/web/src/test/java/org/springframework/security/web/util/matcher/RegexRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/util/matcher/RegexRequestMatcherTests.java index 78b1c2becd9..676be4310e8 100644 --- a/web/src/test/java/org/springframework/security/web/util/matcher/RegexRequestMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/util/matcher/RegexRequestMatcherTests.java @@ -108,6 +108,12 @@ public void matchesWithInvalidMethod() { assertThat(matcher.matches(request)).isFalse(); } + @Test + public void toStringThenFormatted() { + RegexRequestMatcher matcher = new RegexRequestMatcher("/blah", "GET"); + assertThat(matcher.toString()).isEqualTo("Regex [pattern='/blah', GET]"); + } + private HttpServletRequest createRequestWithNullMethod(String path) { when(request.getQueryString()).thenReturn("doesntMatter"); when(request.getServletPath()).thenReturn(path); From 4f5924e202a8808547b17a2b44eae3f560744f87 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 6 Jun 2018 14:57:04 -0500 Subject: [PATCH 067/226] Polish OidcUserService Fixes: gh-5449 --- .../oauth2/client/oidc/userinfo/OidcUserService.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java index 28eb750fd9e..aaae86454a7 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java @@ -15,6 +15,10 @@ */ package org.springframework.security.oauth2.client.oidc.userinfo; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; @@ -29,12 +33,9 @@ import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - /** * An implementation of an {@link OAuth2UserService} that supports OpenID Connect 1.0 Provider's. * @@ -108,7 +109,7 @@ private boolean shouldRetrieveUserInfo(OidcUserRequest userRequest) { userRequest.getClientRegistration().getAuthorizationGrantType())) { // Return true if there is at least one match between the authorized scope(s) and UserInfo scope(s) - return userRequest.getAccessToken().getScopes().stream().anyMatch(userInfoScopes::contains); + return CollectionUtils.containsAny(userRequest.getAccessToken().getScopes(), this.userInfoScopes); } return false; From 979d65c022977e0e5e504627cb3603df431a641a Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 6 Jun 2018 13:30:39 -0500 Subject: [PATCH 068/226] Add DelegatingReactiveAuthenticationManager Fixes: gh-5448 --- ...legatingReactiveAuthenticationManager.java | 55 +++++++++++++ ...ingReactiveAuthenticationManagerTests.java | 80 +++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java create mode 100644 core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java diff --git a/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java b/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java new file mode 100644 index 00000000000..80676d8a3ac --- /dev/null +++ b/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authentication; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * A {@link ReactiveAuthenticationManager} that delegates to other {@link ReactiveAuthenticationManager} instances using + * the result from the first non empty result. + * + * @author Rob Winch + * @since 5.1 + */ +public class DelegatingReactiveAuthenticationManager + implements ReactiveAuthenticationManager { + private final List delegates; + + public DelegatingReactiveAuthenticationManager( + ReactiveAuthenticationManager... entryPoints) { + this(Arrays.asList(entryPoints)); + } + + public DelegatingReactiveAuthenticationManager( + List entryPoints) { + Assert.notEmpty(entryPoints, "entryPoints cannot be null"); + this.delegates = entryPoints; + } + + public Mono authenticate(Authentication authentication) { + return Flux.fromIterable(this.delegates) + .concatMap(m -> m.authenticate(authentication)) + .next(); + } +} diff --git a/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java b/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java new file mode 100644 index 00000000000..3ac64b93dc3 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authentication; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.core.Authentication; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + * @since 5.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class DelegatingReactiveAuthenticationManagerTests { + @Mock + ReactiveAuthenticationManager delegate1; + + @Mock + ReactiveAuthenticationManager delegate2; + + @Mock + Authentication authentication; + + @Test + public void authenticateWhenEmptyAndNotThenReturnsNotEmpty() { + when(this.delegate1.authenticate(any())).thenReturn(Mono.empty()); + when(this.delegate2.authenticate(any())).thenReturn(Mono.just(this.authentication)); + + DelegatingReactiveAuthenticationManager manager = new DelegatingReactiveAuthenticationManager(this.delegate1, this.delegate2); + + assertThat(manager.authenticate(this.authentication).block()).isEqualTo(this.authentication); + } + + @Test + public void authenticateWhenNotEmptyThenOtherDelegatesNotSubscribed() { + // delay to try and force delegate2 to finish (i.e. make sure we didn't use flatMap) + when(this.delegate1.authenticate(any())).thenReturn(Mono.just(this.authentication).delayElement(Duration.ofMillis(100))); + + DelegatingReactiveAuthenticationManager manager = new DelegatingReactiveAuthenticationManager(this.delegate1, this.delegate2); + + StepVerifier.create(manager.authenticate(this.authentication)) + .expectNext(this.authentication) + .verifyComplete(); + } + + @Test + public void authenticateWhenBadCredentialsThenDelegate2NotInvokedAndError() { + when(this.delegate1.authenticate(any())).thenReturn(Mono.error(new BadCredentialsException("Test"))); + + DelegatingReactiveAuthenticationManager manager = new DelegatingReactiveAuthenticationManager(this.delegate1, this.delegate2); + + StepVerifier.create(manager.authenticate(this.authentication)) + .expectError(BadCredentialsException.class) + .verify(); + } +} From 9adae4d859a2eb8064774a3d0767e5e44c717629 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 25 May 2018 16:07:59 -0500 Subject: [PATCH 069/226] Extract OidcUserRequestUtils This logic is shared by both reactive and non-reactive clients. Issue: gh-5330 --- .../oidc/userinfo/OidcUserRequestUtils.java | 70 +++++++++++++++ .../userinfo/OidcUserRequestUtilsTests.java | 89 +++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtils.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtilsTests.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtils.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtils.java new file mode 100644 index 00000000000..797528ceb4c --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtils.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.oidc.userinfo; + +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * Utilities for working with the {@link OidcUserRequest} + * + * @author Rob Winch + * @since 5.1 + */ +final class OidcUserRequestUtils { + + /** + * Determines if an {@link OidcUserRequest} should attempt to retrieve the user info endpoint. Will return true if + * all of the following are true: + * + *

      + *
    • The user info endpoint is defined on the ClientRegistration
    • + *
    • The Client Registration uses the {@link AuthorizationGrantType#AUTHORIZATION_CODE} and scopes in the + * access token are defined in the {@link ClientRegistration}
    • + *
    + * @param userRequest + * @return + */ + static boolean shouldRetrieveUserInfo(OidcUserRequest userRequest) { + // Auto-disabled if UserInfo Endpoint URI is not provided + ClientRegistration clientRegistration = userRequest.getClientRegistration(); + if (StringUtils.isEmpty(clientRegistration.getProviderDetails() + .getUserInfoEndpoint().getUri())) { + + return false; + } + + // The Claims requested by the profile, email, address, and phone scope values + // are returned from the UserInfo Endpoint (as described in Section 5.3.2), + // when a response_type value is used that results in an Access Token being issued. + // However, when no Access Token is issued, which is the case for the response_type=id_token, + // the resulting Claims are returned in the ID Token. + // The Authorization Code Grant Flow, which is response_type=code, results in an Access Token being issued. + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { + + // Return true if there is at least one match between the authorized scope(s) and UserInfo scope(s) + return CollectionUtils + .containsAny(userRequest.getAccessToken().getScopes(), userRequest.getClientRegistration().getScopes()); + } + + return false; + } + + private OidcUserRequestUtils() {} +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtilsTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtilsTests.java new file mode 100644 index 00000000000..9851c7fdd2a --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtilsTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.oidc.userinfo; + +import org.junit.Test; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; + +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.*; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class OidcUserRequestUtilsTests { + private ClientRegistration.Builder registration = ClientRegistration.withRegistrationId("id") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationUri("https://example.com/oauth2/authorize") + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .userInfoUri("https://example.com/users/me") + .clientId("client-id") + .clientName("client-name") + .clientSecret("client-secret") + .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("user") + .tokenUri("https://example.com/oauth/access_token"); + + OidcIdToken idToken = new OidcIdToken("token123", Instant.now(), + Instant.now().plusSeconds(3600), Collections + .singletonMap(IdTokenClaimNames.SUB, "sub123")); + + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + "token", + Instant.now(), + Instant.now().plus(Duration.ofDays(1)), + Collections.singleton("user")); + + @Test + public void shouldRetrieveUserInfoWhenEndpointDefinedAndScopesOverlapThenTrue() { + assertThat(OidcUserRequestUtils.shouldRetrieveUserInfo(userRequest())).isTrue(); + } + + @Test + public void shouldRetrieveUserInfoWhenNoUserInfoUriThenFalse() { + this.registration.userInfoUri(null); + + assertThat(OidcUserRequestUtils.shouldRetrieveUserInfo(userRequest())).isFalse(); + } + + @Test + public void shouldRetrieveUserInfoWhenDifferentScopesThenFalse() { + this.registration.scope("notintoken"); + + assertThat(OidcUserRequestUtils.shouldRetrieveUserInfo(userRequest())).isFalse(); + } + + @Test + public void shouldRetrieveUserInfoWhenNotAuthorizationCodeThenFalse() { + this.registration.authorizationGrantType(AuthorizationGrantType.IMPLICIT); + + assertThat(OidcUserRequestUtils.shouldRetrieveUserInfo(userRequest())).isFalse(); + } + + private OidcUserRequest userRequest() { + return new OidcUserRequest(this.registration.build(), this.accessToken, this.idToken); + } +} From 7c37371725e8ce329887b6fa96817ca9775b7593 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 25 May 2018 16:26:40 -0500 Subject: [PATCH 070/226] Extract OidcTokenValidator Issue: gh-5330 --- ...thorizationCodeAuthenticationProvider.java | 119 +++----------- .../authentication/OidcTokenValidator.java | 121 ++++++++++++++ .../OidcTokenValidatorTests.java | 148 ++++++++++++++++++ 3 files changed, 287 insertions(+), 101 deletions(-) create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcTokenValidator.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcTokenValidatorTests.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java index ba352769344..ff1361f4d04 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java @@ -15,6 +15,10 @@ */ package org.springframework.security.oauth2.client.oidc.authentication; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -40,16 +44,8 @@ import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; -import java.net.URL; -import java.time.Instant; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - /** * An implementation of an {@link AuthenticationProvider} * for the OpenID Connect Core 1.0 Authorization Code Grant Flow. @@ -151,11 +147,7 @@ public Authentication authenticate(Authentication authentication) throws Authent throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString()); } - JwtDecoder jwtDecoder = this.getJwtDecoder(clientRegistration); - Jwt jwt = jwtDecoder.decode((String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN)); - OidcIdToken idToken = new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims()); - - this.validateIdToken(idToken, clientRegistration); + OidcIdToken idToken = createOidcToken(clientRegistration, accessTokenResponse); OidcUser oidcUser = this.userService.loadUser( new OidcUserRequest(clientRegistration, accessTokenResponse.getAccessToken(), idToken)); @@ -191,15 +183,24 @@ public boolean supports(Class authentication) { return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication); } + private OidcIdToken createOidcToken(ClientRegistration clientRegistration, OAuth2AccessTokenResponse accessTokenResponse) { + JwtDecoder jwtDecoder = getJwtDecoder(clientRegistration); + Jwt jwt = jwtDecoder.decode((String) accessTokenResponse.getAdditionalParameters().get( + OidcParameterNames.ID_TOKEN)); + OidcIdToken idToken = new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims()); + OidcTokenValidator.validateIdToken(idToken, clientRegistration); + return idToken; + } + private JwtDecoder getJwtDecoder(ClientRegistration clientRegistration) { JwtDecoder jwtDecoder = this.jwtDecoders.get(clientRegistration.getRegistrationId()); if (jwtDecoder == null) { if (!StringUtils.hasText(clientRegistration.getProviderDetails().getJwkSetUri())) { OAuth2Error oauth2Error = new OAuth2Error( - MISSING_SIGNATURE_VERIFIER_ERROR_CODE, - "Failed to find a Signature Verifier for Client Registration: '" + - clientRegistration.getRegistrationId() + "'. Check to ensure you have configured the JwkSet URI.", - null + MISSING_SIGNATURE_VERIFIER_ERROR_CODE, + "Failed to find a Signature Verifier for Client Registration: '" + + clientRegistration.getRegistrationId() + "'. Check to ensure you have configured the JwkSet URI.", + null ); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } @@ -208,88 +209,4 @@ private JwtDecoder getJwtDecoder(ClientRegistration clientRegistration) { } return jwtDecoder; } - - private void validateIdToken(OidcIdToken idToken, ClientRegistration clientRegistration) { - // 3.1.3.7 ID Token Validation - // http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation - - // Validate REQUIRED Claims - URL issuer = idToken.getIssuer(); - if (issuer == null) { - this.throwInvalidIdTokenException(); - } - String subject = idToken.getSubject(); - if (subject == null) { - this.throwInvalidIdTokenException(); - } - List audience = idToken.getAudience(); - if (CollectionUtils.isEmpty(audience)) { - this.throwInvalidIdTokenException(); - } - Instant expiresAt = idToken.getExpiresAt(); - if (expiresAt == null) { - this.throwInvalidIdTokenException(); - } - Instant issuedAt = idToken.getIssuedAt(); - if (issuedAt == null) { - this.throwInvalidIdTokenException(); - } - - // 2. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) - // MUST exactly match the value of the iss (issuer) Claim. - // TODO Depends on gh-4413 - - // 3. The Client MUST validate that the aud (audience) Claim contains its client_id value - // registered at the Issuer identified by the iss (issuer) Claim as an audience. - // The aud (audience) Claim MAY contain an array with more than one element. - // The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, - // or if it contains additional audiences not trusted by the Client. - if (!audience.contains(clientRegistration.getClientId())) { - this.throwInvalidIdTokenException(); - } - - // 4. If the ID Token contains multiple audiences, - // the Client SHOULD verify that an azp Claim is present. - String authorizedParty = idToken.getAuthorizedParty(); - if (audience.size() > 1 && authorizedParty == null) { - this.throwInvalidIdTokenException(); - } - - // 5. If an azp (authorized party) Claim is present, - // the Client SHOULD verify that its client_id is the Claim Value. - if (authorizedParty != null && !authorizedParty.equals(clientRegistration.getClientId())) { - this.throwInvalidIdTokenException(); - } - - // 7. The alg value SHOULD be the default of RS256 or the algorithm sent by the Client - // in the id_token_signed_response_alg parameter during Registration. - // TODO Depends on gh-4413 - - // 9. The current time MUST be before the time represented by the exp Claim. - Instant now = Instant.now(); - if (!now.isBefore(expiresAt)) { - this.throwInvalidIdTokenException(); - } - - // 10. The iat Claim can be used to reject tokens that were issued too far away from the current time, - // limiting the amount of time that nonces need to be stored to prevent attacks. - // The acceptable range is Client specific. - Instant maxIssuedAt = Instant.now().plusSeconds(30); - if (issuedAt.isAfter(maxIssuedAt)) { - this.throwInvalidIdTokenException(); - } - - // 11. If a nonce value was sent in the Authentication Request, - // a nonce Claim MUST be present and its value checked to verify - // that it is the same value as the one that was sent in the Authentication Request. - // The Client SHOULD check the nonce value for replay attacks. - // The precise method for detecting replay attacks is Client specific. - // TODO Depends on gh-4442 - - } - - private void throwInvalidIdTokenException() { - OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE); - throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString()); - } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcTokenValidator.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcTokenValidator.java new file mode 100644 index 00000000000..b646015a667 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcTokenValidator.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.oidc.authentication; + +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.util.CollectionUtils; + +import java.net.URL; +import java.time.Instant; +import java.util.List; + +/** + * @author Rob Winch + * @since 5.1 + */ +final class OidcTokenValidator { + private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token"; + + static void validateIdToken(OidcIdToken idToken, ClientRegistration clientRegistration) { + // 3.1.3.7 ID Token Validation + // http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + + // Validate REQUIRED Claims + URL issuer = idToken.getIssuer(); + if (issuer == null) { + throwInvalidIdTokenException(); + } + String subject = idToken.getSubject(); + if (subject == null) { + throwInvalidIdTokenException(); + } + List audience = idToken.getAudience(); + if (CollectionUtils.isEmpty(audience)) { + throwInvalidIdTokenException(); + } + Instant expiresAt = idToken.getExpiresAt(); + if (expiresAt == null) { + throwInvalidIdTokenException(); + } + Instant issuedAt = idToken.getIssuedAt(); + if (issuedAt == null) { + throwInvalidIdTokenException(); + } + + // 2. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) + // MUST exactly match the value of the iss (issuer) Claim. + // TODO Depends on gh-4413 + + // 3. The Client MUST validate that the aud (audience) Claim contains its client_id value + // registered at the Issuer identified by the iss (issuer) Claim as an audience. + // The aud (audience) Claim MAY contain an array with more than one element. + // The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, + // or if it contains additional audiences not trusted by the Client. + if (!audience.contains(clientRegistration.getClientId())) { + throwInvalidIdTokenException(); + } + + // 4. If the ID Token contains multiple audiences, + // the Client SHOULD verify that an azp Claim is present. + String authorizedParty = idToken.getAuthorizedParty(); + if (audience.size() > 1 && authorizedParty == null) { + throwInvalidIdTokenException(); + } + + // 5. If an azp (authorized party) Claim is present, + // the Client SHOULD verify that its client_id is the Claim Value. + if (authorizedParty != null && !authorizedParty.equals(clientRegistration.getClientId())) { + throwInvalidIdTokenException(); + } + + // 7. The alg value SHOULD be the default of RS256 or the algorithm sent by the Client + // in the id_token_signed_response_alg parameter during Registration. + // TODO Depends on gh-4413 + + // 9. The current time MUST be before the time represented by the exp Claim. + Instant now = Instant.now(); + if (!now.isBefore(expiresAt)) { + throwInvalidIdTokenException(); + } + + // 10. The iat Claim can be used to reject tokens that were issued too far away from the current time, + // limiting the amount of time that nonces need to be stored to prevent attacks. + // The acceptable range is Client specific. + Instant maxIssuedAt = now.plusSeconds(30); + if (issuedAt.isAfter(maxIssuedAt)) { + throwInvalidIdTokenException(); + } + + // 11. If a nonce value was sent in the Authentication Request, + // a nonce Claim MUST be present and its value checked to verify + // that it is the same value as the one that was sent in the Authentication Request. + // The Client SHOULD check the nonce value for replay attacks. + // The precise method for detecting replay attacks is Client specific. + // TODO Depends on gh-4442 + + } + + private static void throwInvalidIdTokenException() { + OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE); + throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString()); + } + + private OidcTokenValidator() {} +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcTokenValidatorTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcTokenValidatorTests.java new file mode 100644 index 00000000000..08d4545fe70 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcTokenValidatorTests.java @@ -0,0 +1,148 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.oidc.authentication; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; + +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class OidcTokenValidatorTests { + private ClientRegistration.Builder registration = ClientRegistration.withRegistrationId("client-foo-bar") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationUri("https://example.com/oauth2/authorize") + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .userInfoUri("https://example.com/users/me") + .clientId("client-id") + .clientName("client-name") + .clientSecret("client-secret") + .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("user") + .tokenUri("https://example.com/oauth/access_token"); + + private Map claims = new HashMap<>(); + private Instant issuedAt = Instant.now(); + private Instant expiresAt = Instant.now().plusSeconds(3600); + + @Before + public void setup() { + this.claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com"); + this.claims.put(IdTokenClaimNames.SUB, "rob"); + this.claims.put(IdTokenClaimNames.AUD, Arrays.asList("client-id")); + } + + @Test + public void validateIdTokenWhenValidThenNoException() { + assertThatCode(() -> validateIdToken()) + .doesNotThrowAnyException(); + } + + @Test + public void validateIdTokenWhenIssuerNullThenException() { + this.claims.remove(IdTokenClaimNames.ISS); + assertThatCode(() -> validateIdToken()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void validateIdTokenWhenSubNullThenException() { + this.claims.remove(IdTokenClaimNames.SUB); + assertThatCode(() -> validateIdToken()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void validateIdTokenWhenAudNullThenException() { + this.claims.remove(IdTokenClaimNames.AUD); + assertThatCode(() -> validateIdToken()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void validateIdTokenWhenIssuedAtNullThenException() { + this.issuedAt = null; + assertThatCode(() -> validateIdToken()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void validateIdTokenWhenExpiresAtNullThenException() { + this.expiresAt = null; + assertThatCode(() -> validateIdToken()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void validateIdTokenWhenAudMultipleAndAzpNullThenException() { + this.claims.put(IdTokenClaimNames.AUD, Arrays.asList("client-id", "other")); + assertThatCode(() -> validateIdToken()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void validateIdTokenWhenAzpNotClientIdThenException() { + this.claims.put(IdTokenClaimNames.AZP, "other"); + assertThatCode(() -> validateIdToken()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void validateIdTokenWhenMulitpleAudAzpClientIdThenNoException() { + this.claims.put(IdTokenClaimNames.AUD, Arrays.asList("client-id", "other")); + this.claims.put(IdTokenClaimNames.AZP, "client-id"); + assertThatCode(() -> validateIdToken()) + .doesNotThrowAnyException(); + } + + @Test + public void validateIdTokenWhenExpiredThenException() { + this.issuedAt = Instant.now().minus(Duration.ofMinutes(1)); + this.expiresAt = this.issuedAt.plus(Duration.ofSeconds(1)); + assertThatCode(() -> validateIdToken()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void validateIdTokenWhenIssuedAtWayInFutureThenException() { + this.issuedAt = Instant.now().plus(Duration.ofMinutes(5)); + this.expiresAt = this.issuedAt.plus(Duration.ofSeconds(1)); + assertThatCode(() -> validateIdToken()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + private void validateIdToken() { + OidcIdToken token = new OidcIdToken("token123", this.issuedAt, this.expiresAt, this.claims); + OidcTokenValidator.validateIdToken(token, this.registration.build()); + } + +} From 278e595d726817cdb16a7154557f6b996189d919 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 14 Jun 2018 16:32:11 -0500 Subject: [PATCH 071/226] Add JWKSelectorFactory Issue: gh-5330 --- .../oauth2/jwt/JWKSelectorFactory.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKSelectorFactory.java diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKSelectorFactory.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKSelectorFactory.java new file mode 100644 index 00000000000..1ba116fbf9f --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKSelectorFactory.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.jwt; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.KeySourceException; +import com.nimbusds.jose.jwk.JWKMatcher; +import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.proc.JWSVerificationKeySelector; + +/** + * @author Rob Winch + * @since 5.1 + */ +class JWKSelectorFactory { + private final DelegateSelectorFactory delegate; + + JWKSelectorFactory(JWSAlgorithm expectedJWSAlgorithm) { + this.delegate = new DelegateSelectorFactory(expectedJWSAlgorithm); + } + + JWKSelector createSelector(JWSHeader jwsHeader) { + return new JWKSelector(this.delegate.createJWKMatcher(jwsHeader)); + } + + /** + * Used to expose the protected {@link #createJWKMatcher(JWSHeader)} method. + */ + private static class DelegateSelectorFactory extends JWSVerificationKeySelector { + /** + * Creates a new JWS verification key selector. + * + * @param jwsAlg The expected JWS algorithm for the objects to be + * verified. Must not be {@code null}. + */ + public DelegateSelectorFactory(JWSAlgorithm jwsAlg) { + super(jwsAlg, (jwkSelector, context) -> { + throw new KeySourceException("JWKSelectorFactory is only intended for creating a selector"); + }); + } + + @Override + public JWKMatcher createJWKMatcher(JWSHeader jwsHeader) { + return super.createJWKMatcher(jwsHeader); + } + } +} From 8ab89127a443c935187f129bea6bb9b299d4b6ae Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 14 Jun 2018 17:04:02 -0500 Subject: [PATCH 072/226] Add JWKContext Issue: gh-5330 --- .../security/oauth2/jwt/JWKContext.java | 43 +++++++++++++++ .../security/oauth2/jwt/JWKContextTests.java | 54 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKContext.java create mode 100644 oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JWKContextTests.java diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKContext.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKContext.java new file mode 100644 index 00000000000..17a0ab20f31 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKContext.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.jwt; + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.proc.SecurityContext; +import org.springframework.util.Assert; + +import java.util.List; + +/** + * A {@link SecurityContext} that is used by {@link JWKContextJWKSource}. + * + * @author Rob Winch + * @since 5.1 + * @see JWKContextJWKSource + */ +class JWKContext implements SecurityContext { + private final List jwkList; + + JWKContext(List jwkList) { + Assert.notNull(jwkList, "jwkList cannot be null"); + this.jwkList = jwkList; + } + + public List getJwkList() { + return this.jwkList; + } +} diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JWKContextTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JWKContextTests.java new file mode 100644 index 00000000000..cce04d9bd43 --- /dev/null +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JWKContextTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.jwt; + +import com.nimbusds.jose.jwk.JWK; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.mock; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class JWKContextTests { + + @Test + public void constructorWhenNullThenIllegalArgumentException() { + List jwkList = null; + assertThatCode(() -> new JWKContext(jwkList)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void getJwkListWhenEmpty() { + JWKContext jwkContext = new JWKContext(Collections.emptyList()); + assertThat(jwkContext.getJwkList()).isEmpty(); + } + + @Test + public void getJwkListWhenNotEmpty() { + JWK key = mock(JWK.class); + JWKContext jwkContext = new JWKContext(Arrays.asList(key)); + assertThat(jwkContext.getJwkList()).containsOnly(key); + } +} From 453d9e067c2bf8ab4c9194e75713396609c6e8e6 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 14 Jun 2018 17:04:14 -0500 Subject: [PATCH 073/226] Add JWKContextJWKSource Issue: gh-5330 --- .../oauth2/jwt/JWKContextJWKSource.java | 43 +++++++++++++++++++ .../oauth2/jwt/JWKContextJWKSourceTests.java | 42 ++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKContextJWKSource.java create mode 100644 oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JWKContextJWKSourceTests.java diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKContextJWKSource.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKContextJWKSource.java new file mode 100644 index 00000000000..e444d39982a --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKContextJWKSource.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.jwt; + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.jwk.source.JWKSource; + +import java.util.List; + +/** + * A {@link JWKSource} used for reactive applications that returns the {@link JWK} from the {@link JWKContext}. + * + *

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

    + * + * @author Rob Winch + * @since 5.1 + */ +class JWKContextJWKSource implements JWKSource { + + @Override + public List get(JWKSelector jwkSelector, JWKContext context) { + return context.getJwkList(); + } +} diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JWKContextJWKSourceTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JWKContextJWKSourceTests.java new file mode 100644 index 00000000000..1da696bfdf7 --- /dev/null +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JWKContextJWKSourceTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.jwt; + +import com.nimbusds.jose.jwk.JWK; +import org.junit.Test; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.mock; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class JWKContextJWKSourceTests { + private JWKContextJWKSource source = new JWKContextJWKSource(); + + @Test + public void getWhenKeysNotEmptyThenContainsKeys() { + JWK key = mock(JWK.class); + JWKContext jwkContext = new JWKContext(Arrays.asList(key)); + + assertThat(this.source.get(null, jwkContext)).containsOnly(key); + } + +} From f3d860f32dcf6538b08317d525bfa5a638e4bcb3 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 14 Jun 2018 16:33:27 -0500 Subject: [PATCH 074/226] Add ReactiveRemoteJWKSource Issue: gh-5330 --- .../spring-security-oauth2-jose.gradle | 5 + .../oauth2/jwt/ReactiveRemoteJWKSource.java | 136 +++++++++++++++ .../jwt/ReactiveRemoteJWKSourceTests.java | 165 ++++++++++++++++++ 3 files changed, 306 insertions(+) create mode 100644 oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSource.java create mode 100644 oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSourceTests.java diff --git a/oauth2/oauth2-jose/spring-security-oauth2-jose.gradle b/oauth2/oauth2-jose/spring-security-oauth2-jose.gradle index 7ad87dbc3ff..3336a2ffdd4 100644 --- a/oauth2/oauth2-jose/spring-security-oauth2-jose.gradle +++ b/oauth2/oauth2-jose/spring-security-oauth2-jose.gradle @@ -6,5 +6,10 @@ dependencies { compile springCoreDependency compile 'com.nimbusds:nimbus-jose-jwt' + optional 'io.projectreactor:reactor-core' + optional 'org.springframework:spring-webflux' + testCompile powerMock2Dependencies + testCompile 'com.squareup.okhttp3:mockwebserver' + testCompile 'io.projectreactor.netty:reactor-netty' } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSource.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSource.java new file mode 100644 index 00000000000..c26a3a9f4b1 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSource.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.jwt; + +import com.nimbusds.jose.RemoteKeySourceException; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKMatcher; +import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.jwk.JWKSet; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.text.ParseException; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author Rob Winch + * @since 5.1 + */ +class ReactiveRemoteJWKSource { + /** + * The cached JWK set. + */ + private final AtomicReference> cachedJWKSet = new AtomicReference<>(Mono.empty()); + + private WebClient webClient = WebClient.create(); + + private final String jwkSetURL; + + ReactiveRemoteJWKSource(String jwkSetURL) { + this.jwkSetURL = jwkSetURL; + } + + Mono> get(JWKSelector jwkSelector) { + return this.cachedJWKSet.get() + .switchIfEmpty(getJWKSet()) + .flatMap(jwkSet -> get(jwkSelector, jwkSet)) + .switchIfEmpty(getJWKSet().map(jwkSet -> jwkSelector.select(jwkSet))); + } + + private Mono> get(JWKSelector jwkSelector, JWKSet jwkSet) { + return Mono.defer(() -> { + // Run the selector on the JWK set + List matches = jwkSelector.select(jwkSet); + + if (!matches.isEmpty()) { + // Success + return Mono.just(matches); + } + + // Refresh the JWK set if the sought key ID is not in the cached JWK set + + // Looking for JWK with specific ID? + String soughtKeyID = getFirstSpecifiedKeyID(jwkSelector.getMatcher()); + if (soughtKeyID == null) { + // No key ID specified, return no matches + return Mono.just(Collections.emptyList()); + } + + if (jwkSet.getKeyByKeyId(soughtKeyID) != null) { + // The key ID exists in the cached JWK set, matching + // failed for some other reason, return no matches + return Mono.just(Collections.emptyList()); + } + + return Mono.empty(); + + }); + } + + /** + * Updates the cached JWK set from the configured URL. + * + * @return The updated JWK set. + * + * @throws RemoteKeySourceException If JWK retrieval failed. + */ + private Mono getJWKSet() { + return this.webClient.get() + .uri(this.jwkSetURL) + .retrieve() + .bodyToMono(String.class) + .map(this::parse) + .doOnNext(jwkSet -> this.cachedJWKSet.set(Mono.just(jwkSet))) + .cache(); + } + + private JWKSet parse(String body) { + try { + return JWKSet.parse(body); + } + catch (ParseException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns the first specified key ID (kid) for a JWK matcher. + * + * @param jwkMatcher The JWK matcher. Must not be {@code null}. + * + * @return The first key ID, {@code null} if none. + */ + protected static String getFirstSpecifiedKeyID(final JWKMatcher jwkMatcher) { + + Set keyIDs = jwkMatcher.getKeyIDs(); + + if (keyIDs == null || keyIDs.isEmpty()) { + return null; + } + + for (String id: keyIDs) { + if (id != null) { + return id; + } + } + return null; // No kid in matcher + } +} diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSourceTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSourceTests.java new file mode 100644 index 00000000000..58b20a28363 --- /dev/null +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSourceTests.java @@ -0,0 +1,165 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.jwt; + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKMatcher; +import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.jwk.KeyType; +import com.nimbusds.jose.jwk.KeyUse; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + * @since 5.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class ReactiveRemoteJWKSourceTests { + @Mock + private JWKMatcher matcher; + + private ReactiveRemoteJWKSource source; + + private JWKSelector selector; + + private MockWebServer server; + + private String keys = "{\n" + + " \"keys\": [\n" + + " {\n" + + " \"alg\": \"RS256\", \n" + + " \"e\": \"AQAB\", \n" + + " \"kid\": \"1923397381d9574bb873202a90c32b7ceeaed027\", \n" + + " \"kty\": \"RSA\", \n" + + " \"n\": \"m4I5Dk5GnbzzUtqaljDVbpMONi1JLNJ8ZuXE8VvjCAVebDg5vTYhQ33jUwGgbn1wFmytUMgMmvK8A8Gpshl0sO2GBIZoh6_pwLrk657ZEtv-hx9fYKnzwyrfHqxtSswMAyr7XtKl8Ha1I03uFMSaYaaBTwVXCHByhzr4PVXfKAYJNbbcteUZfE8ODlBQkjQLI0IB78Nu8XIRrdzTF_5LCuM6rLUNtX6_KdzPpeX9KEtB7OBAfkdZEtBzGI-aYNLtIaL4qO6cVxBeVDLMoj9kVsRPylrwhEFQcGOjtJhwJwXFzTMZVhkiLFCHxZkkjoMrK5osSRlhduuGI9ot8XTUKQ\", \n" + + " \"use\": \"sig\"\n" + + " }, \n" + + " {\n" + + " \"alg\": \"RS256\", \n" + + " \"e\": \"AQAB\", \n" + + " \"kid\": \"7ddf54d3032d1f0d48c3618892ca74c1ac30ad77\", \n" + + " \"kty\": \"RSA\", \n" + + " \"n\": \"yLlYyux949b7qS-DdqTNjdZb4NtqiNH-Jt7DtRxmfW9XZLOQ6Q2NYgmPe9hyy5GHG7W3zsd6Q-rzq5eGRNEUx1767K1dS5PtkVWPiPG_M7rDqCu3HsLmKQKhRjHYaCWl5NuiMB5mXoPhSwrHd2yeGE7QHIV7_CiQFc1xQsXeiC-nTeJohJO3HI97w0GXE8pHspLYq9oG87f5IHxFr89abmwRug-D7QWQyW5b4doe4ZL-52J-8WHd52kGrGfu4QyV83oAad3I_9Q-yiWOXUr_0GIrzz4_-u5HgqYexnodFhZZSaKuRSg_b5qCnPhW8gBDLAHkmQzQMaWsN14L0pokbQ\", \n" + + " \"use\": \"sig\"\n" + + " }\n" + + " ]\n" + + "}\n"; + + + private String keys2 = "{\n" + + " \"keys\": [\n" + + " {\n" + + " \"alg\": \"RS256\", \n" + + " \"e\": \"AQAB\", \n" + + " \"kid\": \"rotated\", \n" + + " \"kty\": \"RSA\", \n" + + " \"n\": \"m4I5Dk5GnbzzUtqaljDVbpMONi1JLNJ8ZuXE8VvjCAVebDg5vTYhQ33jUwGgbn1wFmytUMgMmvK8A8Gpshl0sO2GBIZoh6_pwLrk657ZEtv-hx9fYKnzwyrfHqxtSswMAyr7XtKl8Ha1I03uFMSaYaaBTwVXCHByhzr4PVXfKAYJNbbcteUZfE8ODlBQkjQLI0IB78Nu8XIRrdzTF_5LCuM6rLUNtX6_KdzPpeX9KEtB7OBAfkdZEtBzGI-aYNLtIaL4qO6cVxBeVDLMoj9kVsRPylrwhEFQcGOjtJhwJwXFzTMZVhkiLFCHxZkkjoMrK5osSRlhduuGI9ot8XTUKQ\", \n" + + " \"use\": \"sig\"\n" + + " }\n" + + " ]\n" + + "}\n"; + + @Before + public void setup() { + this.server = new MockWebServer(); + this.source = new ReactiveRemoteJWKSource(this.server.url("/").toString()); + + this.server.enqueue(new MockResponse().setBody(this.keys)); + this.selector = new JWKSelector(this.matcher); + } + + @Test + public void getWhenMultipleRequestThenCached() { + when(this.matcher.matches(any())).thenReturn(true); + + this.source.get(this.selector).block(); + this.source.get(this.selector).block(); + + assertThat(this.server.getRequestCount()).isEqualTo(1); + } + + @Test + public void getWhenMatchThenCreatesKeys() { + when(this.matcher.matches(any())).thenReturn(true); + + List keys = this.source.get(this.selector).block(); + assertThat(keys).hasSize(2); + JWK key1 = keys.get(0); + assertThat(key1.getKeyID()).isEqualTo("1923397381d9574bb873202a90c32b7ceeaed027"); + assertThat(key1.getAlgorithm().getName()).isEqualTo("RS256"); + assertThat(key1.getKeyType()).isEqualTo(KeyType.RSA); + assertThat(key1.getKeyUse()).isEqualTo(KeyUse.SIGNATURE); + + JWK key2 = keys.get(1); + assertThat(key2.getKeyID()).isEqualTo("7ddf54d3032d1f0d48c3618892ca74c1ac30ad77"); + assertThat(key2.getAlgorithm().getName()).isEqualTo("RS256"); + assertThat(key2.getKeyType()).isEqualTo(KeyType.RSA); + assertThat(key2.getKeyUse()).isEqualTo(KeyUse.SIGNATURE); + } + + @Test + public void getWhenNoMatchAndNoKeyIdThenEmpty() { + when(this.matcher.matches(any())).thenReturn(false); + when(this.matcher.getKeyIDs()).thenReturn(Collections.emptySet()); + + assertThat(this.source.get(this.selector).block()).isEmpty(); + } + + @Test + public void getWhenNoMatchAndKeyIdNotMatchThenRefreshAndFoundThenFound() { + this.server.enqueue(new MockResponse().setBody(this.keys2)); + when(this.matcher.matches(any())).thenReturn(false, false, true); + when(this.matcher.getKeyIDs()).thenReturn(Collections.singleton("rotated")); + + List keys = this.source.get(this.selector).block(); + + assertThat(keys).hasSize(1); + assertThat(keys.get(0).getKeyID()).isEqualTo("rotated"); + } + + @Test + public void getWhenNoMatchAndKeyIdNotMatchThenRefreshAndNotFoundThenEmpty() { + this.server.enqueue(new MockResponse().setBody(this.keys2)); + when(this.matcher.matches(any())).thenReturn(false, false, false); + when(this.matcher.getKeyIDs()).thenReturn(Collections.singleton("rotated")); + + List keys = this.source.get(this.selector).block(); + + assertThat(keys).isEmpty(); + } + + @Test + public void getWhenNoMatchAndKeyIdMatchThenEmpty() { + when(this.matcher.matches(any())).thenReturn(false); + when(this.matcher.getKeyIDs()).thenReturn(Collections.singleton("7ddf54d3032d1f0d48c3618892ca74c1ac30ad77")); + + assertThat(this.source.get(this.selector).block()).isEmpty(); + } +} From a3920efb089595473295e106da2cab5d368bb83b Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 13 Jun 2018 15:11:44 -0500 Subject: [PATCH 075/226] Add NimbusReactiveJwtDecoder Issue: gh-5330 --- .../jwt/NimbusJwkReactiveJwtDecoder.java | 157 ++++++++++++++++++ .../oauth2/jwt/ReactiveJwtDecoder.java | 50 ++++++ 2 files changed, 207 insertions(+) create mode 100644 oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoder.java create mode 100644 oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoder.java diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoder.java new file mode 100644 index 00000000000..1f68843ede8 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoder.java @@ -0,0 +1,157 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.jwt; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jose.proc.JWSKeySelector; +import com.nimbusds.jose.proc.JWSVerificationKeySelector; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import com.nimbusds.jwt.proc.JWTProcessor; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; +import org.springframework.util.Assert; +import reactor.core.publisher.Mono; + +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * An implementation of a {@link JwtDecoder} that "decodes" a + * JSON Web Token (JWT) and additionally verifies it's digital signature if the JWT is a + * JSON Web Signature (JWS). The public key used for verification is obtained from the + * JSON Web Key (JWK) Set {@code URL} supplied via the constructor. + * + *

    + * NOTE: This implementation uses the Nimbus JOSE + JWT SDK internally. + * + * @author Rob Winch + * @since 5.1 + * @see JwtDecoder + * @see JSON Web Token (JWT) + * @see JSON Web Signature (JWS) + * @see JSON Web Key (JWK) + * @see Nimbus JOSE + JWT SDK + */ +public final class NimbusJwkReactiveJwtDecoder implements ReactiveJwtDecoder { + private final JWTProcessor jwtProcessor; + + private final ReactiveRemoteJWKSource reactiveJwkSource; + + private final JWKSelectorFactory jwkSelectorFactory; + + /** + * Constructs a {@code NimbusJwtDecoderJwkSupport} using the provided parameters. + * + * @param jwkSetUrl the JSON Web Key (JWK) Set {@code URL} + */ + public NimbusJwkReactiveJwtDecoder(String jwkSetUrl) { + this(jwkSetUrl, JwsAlgorithms.RS256); + } + + /** + * Constructs a {@code NimbusJwtDecoderJwkSupport} using the provided parameters. + * + * @param jwkSetUrl the JSON Web Key (JWK) Set {@code URL} + * @param jwsAlgorithm the JSON Web Algorithm (JWA) used for verifying the digital signatures + */ + public NimbusJwkReactiveJwtDecoder(String jwkSetUrl, String jwsAlgorithm) { + Assert.hasText(jwkSetUrl, "jwkSetUrl cannot be empty"); + Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty"); + + JWSAlgorithm algorithm = JWSAlgorithm.parse(jwsAlgorithm); + JWKSource jwkSource = new JWKContextJWKSource(); + JWSKeySelector jwsKeySelector = + new JWSVerificationKeySelector<>(algorithm, jwkSource); + + DefaultJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); + jwtProcessor.setJWSKeySelector(jwsKeySelector); + this.jwtProcessor = jwtProcessor; + + this.reactiveJwkSource = new ReactiveRemoteJWKSource(jwkSetUrl); + + this.jwkSelectorFactory = new JWKSelectorFactory(algorithm); + + } + + @Override + public Mono decode(String token) throws JwtException { + JWT jwt = parse(token); + if (jwt instanceof SignedJWT) { + return this.decode((SignedJWT) jwt); + } + return Mono.empty(); + } + + private JWT parse(String token) { + try { + return JWTParser.parse(token); + } catch (Exception ex) { + throw new JwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex); + } + } + + private Mono decode(SignedJWT parsedToken) { + try { + JWKSelector selector = this.jwkSelectorFactory + .createSelector(parsedToken.getHeader()); + return this.reactiveJwkSource.get(selector) + .map(jwkList -> createJwkSet(parsedToken, jwkList)) + .map(set -> createJwt(parsedToken, set)); + } catch (Exception ex) { + throw new JwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex); + } + } + + private JWTClaimsSet createJwkSet(JWT parsedToken, List jwkList) { + try { + return this.jwtProcessor.process(parsedToken, new JWKContext(jwkList)); + } + catch (BadJOSEException e) { + throw new RuntimeException(e); + } + catch (JOSEException e) { + throw new RuntimeException(e); + } + } + + private Jwt createJwt(JWT parsedJwt, JWTClaimsSet jwtClaimsSet) { + Instant expiresAt = null; + if (jwtClaimsSet.getExpirationTime() != null) { + expiresAt = jwtClaimsSet.getExpirationTime().toInstant(); + } + Instant issuedAt = null; + if (jwtClaimsSet.getIssueTime() != null) { + issuedAt = jwtClaimsSet.getIssueTime().toInstant(); + } else if (expiresAt != null) { + // Default to expiresAt - 1 second + issuedAt = Instant.from(expiresAt).minusSeconds(1); + } + + Map headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject()); + + return new Jwt(parsedJwt.getParsedString(), issuedAt, expiresAt, headers, jwtClaimsSet.getClaims()); + } +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoder.java new file mode 100644 index 00000000000..8ecf5d3e631 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoder.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.jwt; + +import reactor.core.publisher.Mono; + +/** + * Implementations of this interface are responsible for "decoding" + * a JSON Web Token (JWT) from it's compact claims representation format to a {@link Jwt}. + * + *

    + * JWTs may be represented using the JWS Compact Serialization format for a + * JSON Web Signature (JWS) structure or JWE Compact Serialization format for a + * JSON Web Encryption (JWE) structure. Therefore, implementors are responsible + * for verifying a JWS and/or decrypting a JWE. + * + * @author Rob Winch + * @since 5.1 + * @see Jwt + * @see JSON Web Token (JWT) + * @see JSON Web Signature (JWS) + * @see JSON Web Encryption (JWE) + * @see JWS Compact Serialization + * @see JWE Compact Serialization + */ +public interface ReactiveJwtDecoder { + + /** + * Decodes the JWT from it's compact claims representation format and returns a {@link Jwt}. + * + * @param token the JWT value + * @return a {@link Jwt} + * @throws JwtException if an error occurs while attempting to decode the JWT + */ + Mono decode(String token) throws JwtException; + +} From 565acfe72f94a9703920722df36d5335e87d4e4b Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 6 Jun 2018 13:43:48 -0500 Subject: [PATCH 076/226] Add OidcReactiveOAuth2UserService Issue: gh-5330 --- .../OidcReactiveOAuth2UserService.java | 95 +++++++++++ .../OidcReactiveOAuth2UserServiceTests.java | 157 ++++++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserService.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserServiceTests.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserService.java new file mode 100644 index 00000000000..222a1e396d4 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserService.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.client.oidc.userinfo; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.client.userinfo.DefaultReactiveOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import reactor.core.publisher.Mono; + +/** + * An implementation of an {@link ReactiveOAuth2UserService} that supports OpenID Connect 1.0 Provider's. + * + * @author Rob Winch + * @since 5.1 + * @see ReactiveOAuth2UserService + * @see OidcUserRequest + * @see OidcUser + * @see DefaultOidcUser + * @see OidcUserInfo + */ +public class OidcReactiveOAuth2UserService implements + ReactiveOAuth2UserService { + + private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response"; + + private ReactiveOAuth2UserService oauth2UserService = new DefaultReactiveOAuth2UserService(); + + @Override + public Mono loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { + Assert.notNull(userRequest, "userRequest cannot be null"); + return getUserInfo(userRequest) + .map(userInfo -> new OidcUserAuthority(userRequest.getIdToken(), userInfo)) + .defaultIfEmpty(new OidcUserAuthority(userRequest.getIdToken(), null)) + .map(authority -> { + OidcUserInfo userInfo = authority.getUserInfo(); + Set authorities = new HashSet<>(); + authorities.add(authority); + String userNameAttributeName = userRequest.getClientRegistration() + .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); + if (StringUtils.hasText(userNameAttributeName)) { + return new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo, userNameAttributeName); + } else { + return new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo); + } + }); + } + + private Mono getUserInfo(OidcUserRequest userRequest) { + if (!OidcUserRequestUtils.shouldRetrieveUserInfo(userRequest)) { + return Mono.empty(); + } + return this.oauth2UserService.loadUser(userRequest) + .map(OAuth2User::getAttributes) + .map(OidcUserInfo::new) + .doOnNext(userInfo -> { + String subject = userInfo.getSubject(); + if (subject == null || !subject.equals(userRequest.getIdToken().getSubject())) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + }); + } + + public void setOauth2UserService(ReactiveOAuth2UserService oauth2UserService) { + Assert.notNull(oauth2UserService, "oauth2UserService cannot be null"); + this.oauth2UserService = oauth2UserService; + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserServiceTests.java new file mode 100644 index 00000000000..6da77ea8a30 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserServiceTests.java @@ -0,0 +1,157 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.oidc.userinfo; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.StandardClaimNames; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + * @since 5.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class OidcReactiveOAuth2UserServiceTests { + @Mock + private ReactiveOAuth2UserService oauth2UserService; + + private ClientRegistration.Builder registration = ClientRegistration.withRegistrationId("id") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationUri("https://example.com/oauth2/authorize") + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .userInfoUri("https://example.com/users/me") + .clientId("client-id") + .clientName("client-name") + .clientSecret("client-secret") + .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("user") + .tokenUri("https://example.com/oauth/access_token"); + + private OidcIdToken idToken = new OidcIdToken("token123", Instant.now(), + Instant.now().plusSeconds(3600), Collections + .singletonMap(IdTokenClaimNames.SUB, "sub123")); + + private OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + "token", + Instant.now(), + Instant.now().plus(Duration.ofDays(1)), + Collections.singleton("user")); + + private OidcReactiveOAuth2UserService userService = new OidcReactiveOAuth2UserService(); + + @Before + public void setup() { + this.userService.setOauth2UserService(this.oauth2UserService); + } + + @Test + public void loadUserWhenUserInfoUriNullThenUserInfoNotRetrieved() { + this.registration.userInfoUri(null); + + OidcUser user = this.userService.loadUser(userRequest()).block(); + + assertThat(user.getUserInfo()).isNull(); + } + + @Test + public void loadUserWhenOAuth2UserEmptyThenNullUserInfo() { + when(this.oauth2UserService.loadUser(any())).thenReturn(Mono.empty()); + + OidcUser user = this.userService.loadUser(userRequest()).block(); + + assertThat(user.getUserInfo()).isNull(); + } + + @Test + public void loadUserWhenOAuth2UserSubjectNullThenOAuth2AuthenticationException() { + OAuth2User oauth2User = new DefaultOAuth2User(AuthorityUtils.createAuthorityList("ROLE_USER"), Collections.singletonMap("user", "rob"), "user"); + when(this.oauth2UserService.loadUser(any())).thenReturn(Mono.just(oauth2User)); + + assertThatCode(() -> this.userService.loadUser(userRequest()).block()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void loadUserWhenOAuth2UserSubjectNotEqualThenOAuth2AuthenticationException() { + Map attributes = new HashMap<>(); + attributes.put(StandardClaimNames.SUB, "not-equal"); + attributes.put("user", "rob"); + OAuth2User oauth2User = new DefaultOAuth2User(AuthorityUtils.createAuthorityList("ROLE_USER"), + attributes, "user"); + when(this.oauth2UserService.loadUser(any())).thenReturn(Mono.just(oauth2User)); + + assertThatCode(() -> this.userService.loadUser(userRequest()).block()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void loadUserWhenOAuth2UserThenUserInfoNotNull() { + Map attributes = new HashMap<>(); + attributes.put(StandardClaimNames.SUB, "sub123"); + attributes.put("user", "rob"); + OAuth2User oauth2User = new DefaultOAuth2User(AuthorityUtils.createAuthorityList("ROLE_USER"), + attributes, "user"); + when(this.oauth2UserService.loadUser(any())).thenReturn(Mono.just(oauth2User)); + + assertThat(this.userService.loadUser(userRequest()).block().getUserInfo()).isNotNull(); + } + + @Test + public void loadUserWhenOAuth2UserAndUser() { + this.registration.userNameAttributeName("user"); + Map attributes = new HashMap<>(); + attributes.put(StandardClaimNames.SUB, "sub123"); + attributes.put("user", "rob"); + OAuth2User oauth2User = new DefaultOAuth2User(AuthorityUtils.createAuthorityList("ROLE_USER"), + attributes, "user"); + when(this.oauth2UserService.loadUser(any())).thenReturn(Mono.just(oauth2User)); + + assertThat(this.userService.loadUser(userRequest()).block().getName()).isEqualTo("rob"); + } + + private OidcUserRequest userRequest() { + return new OidcUserRequest(this.registration.build(), this.accessToken, this.idToken); + } +} From 95c870208a952a8d3838e0c80cfd417a49b58636 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 25 May 2018 14:54:15 -0500 Subject: [PATCH 077/226] Add OidcReactiveAuthenticationManager Fixes: gh-5330 --- .../OidcReactiveAuthenticationManager.java | 229 +++++++++++++++++ ...idcReactiveAuthenticationManagerTests.java | 237 ++++++++++++++++++ 2 files changed, 466 insertions(+) create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManager.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManagerTests.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManager.java new file mode 100644 index 00000000000..dfb30ca3c0f --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManager.java @@ -0,0 +1,229 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.client.oidc.authentication; + +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.oauth2.jwt.NimbusJwkReactiveJwtDecoder; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Mono; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** + * An implementation of an {@link org.springframework.security.authentication.AuthenticationProvider} for OAuth 2.0 Login, + * which leverages the OAuth 2.0 Authorization Code Grant Flow. + * + * This {@link org.springframework.security.authentication.AuthenticationProvider} is responsible for authenticating + * an Authorization Code credential with the Authorization Server's Token Endpoint + * and if valid, exchanging it for an Access Token credential. + *

    + * It will also obtain the user attributes of the End-User (Resource Owner) + * from the UserInfo Endpoint using an {@link org.springframework.security.oauth2.client.userinfo.OAuth2UserService}, + * which will create a {@code Principal} in the form of an {@link OAuth2User}. + * The {@code OAuth2User} is then associated to the {@link OAuth2LoginAuthenticationToken} + * to complete the authentication. + * + * @author Rob Winch + * @since 5.1 + * @see OAuth2LoginAuthenticationToken + * @see ReactiveOAuth2AccessTokenResponseClient + * @see ReactiveOAuth2UserService + * @see OAuth2User + * @see Section 4.1 Authorization Code Grant Flow + * @see Section 4.1.3 Access Token Request + * @see Section 4.1.4 Access Token Response + */ +public class OidcReactiveAuthenticationManager implements + ReactiveAuthenticationManager { + + private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter"; + private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter"; + private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token"; + private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier"; + + private final ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient; + + private final ReactiveOAuth2UserService userService; + + private final ReactiveOAuth2AuthorizedClientService authorizedClientService; + + private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities); + + private Function decoderFactory = new DefaultDecoderFactory(); + + public OidcReactiveAuthenticationManager( + ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient, + ReactiveOAuth2UserService userService, + ReactiveOAuth2AuthorizedClientService authorizedClientService) { + Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null"); + Assert.notNull(userService, "userService cannot be null"); + Assert.notNull(authorizedClientService, "authorizedClientService"); + this.accessTokenResponseClient = accessTokenResponseClient; + this.userService = userService; + this.authorizedClientService = authorizedClientService; + } + + @Override + public Mono authenticate(Authentication authentication) { + return Mono.defer(() -> { + OAuth2LoginAuthenticationToken authorizationCodeAuthentication = (OAuth2LoginAuthenticationToken) authentication; + + // Section 3.1.2.1 Authentication Request - http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + // scope REQUIRED. OpenID Connect requests MUST contain the "openid" scope value. + if (!authorizationCodeAuthentication.getAuthorizationExchange() + .getAuthorizationRequest().getScopes().contains("openid")) { + // This is an OpenID Connect Authentication Request so return empty + // and let OAuth2LoginReactiveAuthenticationManager handle it instead + return Mono.empty(); + } + + + OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication + .getAuthorizationExchange().getAuthorizationRequest(); + OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication + .getAuthorizationExchange().getAuthorizationResponse(); + + if (authorizationResponse.statusError()) { + throw new OAuth2AuthenticationException( + authorizationResponse.getError(), authorizationResponse.getError().toString()); + } + + if (!authorizationResponse.getState().equals(authorizationRequest.getState())) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + + if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + + OAuth2AuthorizationCodeGrantRequest authzRequest = new OAuth2AuthorizationCodeGrantRequest( + authorizationCodeAuthentication.getClientRegistration(), + authorizationCodeAuthentication.getAuthorizationExchange()); + + return this.accessTokenResponseClient.getTokenResponse(authzRequest) + .flatMap(accessTokenResponse -> authenticationResult(authorizationCodeAuthentication, accessTokenResponse)); + }); + } + + /** + * Provides a way to customize the {@link ReactiveJwtDecoder} given a {@link ClientRegistration} + * @param decoderFactory the {@link Function} used to create {@link ReactiveJwtDecoder} instance. Cannot be null. + */ + void setDecoderFactory( + Function decoderFactory) { + Assert.notNull(decoderFactory, "decoderFactory cannot be null"); + this.decoderFactory = decoderFactory; + } + + private Mono authenticationResult(OAuth2LoginAuthenticationToken authorizationCodeAuthentication, OAuth2AccessTokenResponse accessTokenResponse) { + OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken(); + + ClientRegistration clientRegistration = authorizationCodeAuthentication.getClientRegistration(); + + if (!accessTokenResponse.getAdditionalParameters().containsKey(OidcParameterNames.ID_TOKEN)) { + OAuth2Error invalidIdTokenError = new OAuth2Error( + INVALID_ID_TOKEN_ERROR_CODE, + "Missing (required) ID Token in Token Response for Client Registration: " + clientRegistration.getRegistrationId(), + null); + throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString()); + } + + return createOidcToken(clientRegistration, accessTokenResponse) + .map(idToken -> new OidcUserRequest(clientRegistration, accessToken, idToken)) + .flatMap(this.userService::loadUser) + .flatMap(oauth2User -> { + Collection mappedAuthorities = + this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities()); + + OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken( + authorizationCodeAuthentication.getClientRegistration(), + authorizationCodeAuthentication.getAuthorizationExchange(), + oauth2User, + mappedAuthorities, + accessToken); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + authenticationResult.getClientRegistration(), + authenticationResult.getName(), + authenticationResult.getAccessToken()); + OAuth2AuthenticationToken result = new OAuth2AuthenticationToken( + authenticationResult.getPrincipal(), + authenticationResult.getAuthorities(), + authenticationResult.getClientRegistration().getRegistrationId()); + return this.authorizedClientService.saveAuthorizedClient(authorizedClient, authenticationResult) + .thenReturn(result); + }); + } + + private Mono createOidcToken(ClientRegistration clientRegistration, OAuth2AccessTokenResponse accessTokenResponse) { + ReactiveJwtDecoder jwtDecoder = this.decoderFactory.apply(clientRegistration); + String rawIdToken = (String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN); + return jwtDecoder.decode(rawIdToken) + .map(jwt -> new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims())) + .doOnNext(idToken -> OidcTokenValidator.validateIdToken(idToken, clientRegistration)); + } + + private static class DefaultDecoderFactory implements Function { + private final Map jwtDecoders = new ConcurrentHashMap<>(); + + @Override + public ReactiveJwtDecoder apply(ClientRegistration clientRegistration) { + ReactiveJwtDecoder jwtDecoder = this.jwtDecoders.get(clientRegistration.getRegistrationId()); + if (jwtDecoder == null) { + if (!StringUtils.hasText(clientRegistration.getProviderDetails().getJwkSetUri())) { + OAuth2Error oauth2Error = new OAuth2Error( + MISSING_SIGNATURE_VERIFIER_ERROR_CODE, + "Failed to find a Signature Verifier for Client Registration: '" + + clientRegistration.getRegistrationId() + "'. Check to ensure you have configured the JwkSet URI.", + null + ); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + jwtDecoder = new NimbusJwkReactiveJwtDecoder(clientRegistration.getProviderDetails().getJwkSetUri()); + this.jwtDecoders.put(clientRegistration.getRegistrationId(), jwtDecoder); + } + return jwtDecoder; + } + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManagerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManagerTests.java new file mode 100644 index 00000000000..44120c712a3 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManagerTests.java @@ -0,0 +1,237 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.oidc.authentication; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import reactor.core.publisher.Mono; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + * @since 5.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class OidcReactiveAuthenticationManagerTests { + @Mock + private ReactiveOAuth2UserService userService; + + @Mock + private ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient; + + @Mock + private ReactiveOAuth2AuthorizedClientService authorizedClientService; + + @Mock + private ReactiveJwtDecoder jwtDecoder; + + private ClientRegistration.Builder registration = ClientRegistration.withRegistrationId("github") + .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .scope("openid") + .authorizationUri("https://github.com/login/oauth/authorize") + .tokenUri("https://github.com/login/oauth/access_token") + .userInfoUri("https://api.github.com/user") + .userNameAttributeName("id") + .clientName("GitHub") + .clientId("clientId") + .jwkSetUri("https://example.com/oauth2/jwk") + .clientSecret("clientSecret"); + + private OAuth2AuthorizationResponse.Builder authorizationResponseBldr = OAuth2AuthorizationResponse + .success("code") + .state("state"); + + private OidcIdToken idToken = new OidcIdToken("token123", Instant.now(), + Instant.now().plusSeconds(3600), Collections.singletonMap(IdTokenClaimNames.SUB, "sub123")); + + private OidcReactiveAuthenticationManager manager; + + @Before + public void setup() { + this.manager = new OidcReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService, + this.authorizedClientService); + when(this.authorizedClientService.saveAuthorizedClient(any(), any())).thenReturn( + Mono.empty()); + } + + @Test + public void constructorWhenNullAccessTokenResponseClientThenIllegalArgumentException() { + this.accessTokenResponseClient = null; + assertThatThrownBy(() -> new OidcReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService, + this.authorizedClientService)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenNullUserServiceThenIllegalArgumentException() { + this.userService = null; + assertThatThrownBy(() -> new OidcReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService, + this.authorizedClientService)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenNullAuthorizedClientServiceThenIllegalArgumentException() { + this.authorizedClientService = null; + assertThatThrownBy(() -> new OidcReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService, + this.authorizedClientService)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void authenticateWhenNoSubscriptionThenDoesNothing() { + // we didn't do anything because it should cause a ClassCastException (as verified below) + TestingAuthenticationToken token = new TestingAuthenticationToken("a", "b"); + + assertThatCode(()-> this.manager.authenticate(token)) + .doesNotThrowAnyException(); + + assertThatThrownBy(() -> this.manager.authenticate(token).block()) + .isInstanceOf(Throwable.class); + } + + @Test + public void authenticationWhenNotOidcThenEmpty() { + this.registration.scope("notopenid"); + assertThat(this.manager.authenticate(loginToken()).block()).isNull(); + } + + @Test + public void authenticationWhenErrorThenOAuth2AuthenticationException() { + this.authorizationResponseBldr = OAuth2AuthorizationResponse + .error("error") + .state("state"); + assertThatThrownBy(() -> this.manager.authenticate(loginToken()).block()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void authenticationWhenStateDoesNotMatchThenOAuth2AuthenticationException() { + this.authorizationResponseBldr.state("notmatch"); + assertThatThrownBy(() -> this.manager.authenticate(loginToken()).block()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void authenticationWhenOAuth2UserNotFoundThenEmpty() { + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("foo") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .additionalParameters(Collections.singletonMap(OidcParameterNames.ID_TOKEN, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.")) + .build(); + + Map claims = new HashMap<>(); + claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com"); + claims.put(IdTokenClaimNames.SUB, "rob"); + claims.put(IdTokenClaimNames.AUD, Arrays.asList("clientId")); + Instant issuedAt = Instant.now(); + Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600); + Jwt idToken = new Jwt("id-token", issuedAt, expiresAt, claims, claims); + + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse)); + when(this.userService.loadUser(any())).thenReturn(Mono.empty()); + when(this.jwtDecoder.decode(any())).thenReturn(Mono.just(idToken)); + this.manager.setDecoderFactory(c -> this.jwtDecoder); + assertThat(this.manager.authenticate(loginToken()).block()).isNull(); + } + + @Test + public void authenticationWhenOAuth2UserFoundThenSuccess() { + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("foo") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .additionalParameters(Collections.singletonMap(OidcParameterNames.ID_TOKEN, this.idToken.getTokenValue())) + .build(); + + Map claims = new HashMap<>(); + claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com"); + claims.put(IdTokenClaimNames.SUB, "rob"); + claims.put(IdTokenClaimNames.AUD, Arrays.asList("clientId")); + Instant issuedAt = Instant.now(); + Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600); + Jwt idToken = new Jwt("id-token", issuedAt, expiresAt, claims, claims); + + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse)); + DefaultOidcUser user = new DefaultOidcUser(AuthorityUtils.createAuthorityList("ROLE_USER"), this.idToken); + when(this.userService.loadUser(any())).thenReturn(Mono.just(user)); + when(this.jwtDecoder.decode(any())).thenReturn(Mono.just(idToken)); + this.manager.setDecoderFactory(c -> this.jwtDecoder); + + OAuth2AuthenticationToken result = (OAuth2AuthenticationToken) this.manager.authenticate(loginToken()).block(); + + assertThat(result.getPrincipal()).isEqualTo(user); + assertThat(result.getAuthorities()).containsOnlyElementsOf(user.getAuthorities()); + assertThat(result.isAuthenticated()).isTrue(); + } + + private OAuth2LoginAuthenticationToken loginToken() { + ClientRegistration clientRegistration = this.registration.build(); + OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest + .authorizationCode() + .state("state") + .clientId(clientRegistration.getClientId()) + .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()) + .redirectUri(clientRegistration.getRedirectUriTemplate()) + .scopes(clientRegistration.getScopes()) + .build(); + OAuth2AuthorizationResponse authorizationResponse = this.authorizationResponseBldr + .redirectUri(clientRegistration.getRedirectUriTemplate()) + .build(); + OAuth2AuthorizationExchange authorizationExchange = new OAuth2AuthorizationExchange(authorizationRequest, + authorizationResponse); + return new OAuth2LoginAuthenticationToken(clientRegistration, authorizationExchange); + } +} From bf9c5bb0b68ad12b68ee25277022817a1fdd19db Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 6 Jun 2018 13:31:48 -0500 Subject: [PATCH 078/226] ServerHttpSecurity oauth leverages OidcReactiveAuthenticationManager Issue: gh-5330 --- .../config/web/server/ServerHttpSecurity.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 0de6031ee14..52edbc37c25 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -35,6 +35,7 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager; import org.springframework.security.authorization.AuthorityReactiveAuthorizationManager; @@ -45,6 +46,8 @@ import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2LoginReactiveAuthenticationManager; import org.springframework.security.oauth2.client.endpoint.NimbusReactiveAuthorizationCodeTokenResponseClient; +import org.springframework.security.oauth2.client.oidc.authentication.OidcReactiveAuthenticationManager; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.userinfo.DefaultReactiveOAuth2UserService; @@ -101,6 +104,7 @@ import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcherEntry; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; @@ -371,8 +375,16 @@ protected void configure(ServerHttpSecurity http) { NimbusReactiveAuthorizationCodeTokenResponseClient client = new NimbusReactiveAuthorizationCodeTokenResponseClient(); ReactiveOAuth2UserService userService = new DefaultReactiveOAuth2UserService(); - OAuth2LoginReactiveAuthenticationManager manager = new OAuth2LoginReactiveAuthenticationManager(client, userService, + ReactiveAuthenticationManager manager = new OAuth2LoginReactiveAuthenticationManager(client, userService, authorizedClientService); + + boolean oidcAuthenticationProviderEnabled = ClassUtils.isPresent( + "org.springframework.security.oauth2.jwt.JwtDecoder", this.getClass().getClassLoader()); + if (oidcAuthenticationProviderEnabled) { + OidcReactiveAuthenticationManager oidc = new OidcReactiveAuthenticationManager(client, new OidcReactiveOAuth2UserService(), authorizedClientService); + manager = new DelegatingReactiveAuthenticationManager(oidc, manager); + } + AuthenticationWebFilter authenticationFilter = new AuthenticationWebFilter(manager); authenticationFilter.setRequiresAuthenticationMatcher(new PathPatternParserServerWebExchangeMatcher("/login/oauth2/code/{registrationId}")); authenticationFilter.setAuthenticationConverter(new ServerOAuth2LoginAuthenticationTokenConverter(clientRegistrationRepository)); From 886d614db10b380c75da7e911e5cdc0b017ee44f Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 18 Jun 2018 19:31:06 -0400 Subject: [PATCH 079/226] Polish OAuth2Configurer --- .../annotation/web/builders/HttpSecurity.java | 4 +- .../configurers/oauth2/OAuth2Configurer.java | 38 ++++++++----------- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index ac22562cd43..e46c5202e47 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -1000,7 +1000,9 @@ public OAuth2LoginConfigurer oauth2Login() throws Exception { * @throws Exception */ public OAuth2Configurer oauth2() throws Exception { - return getOrApply(new OAuth2Configurer<>()); + OAuth2Configurer configurer = getOrApply(new OAuth2Configurer<>()); + this.postProcess(configurer); + return configurer; } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java index a1cba10d999..57d42deb2c6 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java @@ -15,6 +15,7 @@ */ package org.springframework.security.config.annotation.web.configurers.oauth2; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -34,49 +35,40 @@ public final class OAuth2Configurer> extends AbstractHttpConfigurer, B> { - private final OAuth2ClientConfigurer clientConfigurer = new OAuth2ClientConfigurer<>(); - private boolean clientEnabled; + @Autowired + private ObjectPostProcessor objectPostProcessor; + + private OAuth2ClientConfigurer clientConfigurer; /** * Returns the {@link OAuth2ClientConfigurer} for configuring OAuth 2.0 Client support. * * @return the {@link OAuth2ClientConfigurer} - * @throws Exception */ - public OAuth2ClientConfigurer client() throws Exception { - this.clientEnabled = true; + public OAuth2ClientConfigurer client() { + if (this.clientConfigurer == null) { + this.initClientConfigurer(); + } return this.clientConfigurer; } @Override public void init(B builder) throws Exception { - if (this.clientEnabled) { + if (this.clientConfigurer != null) { this.clientConfigurer.init(builder); } } @Override public void configure(B builder) throws Exception { - if (this.clientEnabled) { + if (this.clientConfigurer != null) { this.clientConfigurer.configure(builder); } } - @Override - public void setBuilder(B builder) { - this.clientConfigurer.setBuilder(builder); - super.setBuilder(builder); - } - - @Override - public void addObjectPostProcessor(ObjectPostProcessor objectPostProcessor) { - this.clientConfigurer.addObjectPostProcessor(objectPostProcessor); - super.addObjectPostProcessor(objectPostProcessor); - } - - @Override - public OAuth2Configurer withObjectPostProcessor(ObjectPostProcessor objectPostProcessor) { - this.clientConfigurer.withObjectPostProcessor(objectPostProcessor); - return super.withObjectPostProcessor(objectPostProcessor); + private void initClientConfigurer() { + this.clientConfigurer = new OAuth2ClientConfigurer<>(); + this.clientConfigurer.setBuilder(this.getBuilder()); + this.clientConfigurer.addObjectPostProcessor(this.objectPostProcessor); } } From a34195cc21fa9f3c5c017b621cb3ef38d5dba5c5 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Wed, 20 Jun 2018 07:53:22 -0600 Subject: [PATCH 080/226] HttpHeadersConfigTests groovy->java Also, slightly modified the approach when asserting headers. In the previous incarnation, the tests would assert an exact match against the list of headers, which is more brittle than confirming that the expected headers are there and the unexpected ones are not. Now, should Spring Security add other headers that are outside the purview of the secure headers configuration, the assertions won't break. Issue: gh-4939 --- .../config/http/HttpHeadersConfigTests.groovy | 961 ------------------ .../config/http/HttpHeadersConfigTests.java | 776 ++++++++++++++ ...eadersConfigTests-CacheControlDisabled.xml | 36 + ...ntentSecurityPolicyWithEmptyDirectives.xml | 36 + ...tentSecurityPolicyWithPolicyDirectives.xml | 36 + ...ts-ContentSecurityPolicyWithReportOnly.xml | 38 + ...ConfigTests-ContentTypeOptionsDisabled.xml | 36 + .../HttpHeadersConfigTests-DefaultConfig.xml | 32 + ...Tests-DefaultsDisabledWithCacheControl.xml | 36 + ...aultsDisabledWithContentSecurityPolicy.xml | 36 + ...DefaultsDisabledWithContentTypeOptions.xml | 36 + ...Tests-DefaultsDisabledWithCustomHeader.xml | 37 + ...DefaultsDisabledWithCustomHeaderWriter.xml | 41 + ...tsDisabledWithCustomHstsRequestMatcher.xml | 38 + ...figTests-DefaultsDisabledWithEmptyHpkp.xml | 36 + ...figTests-DefaultsDisabledWithEmptyPins.xml | 36 + ...Tests-DefaultsDisabledWithFrameOptions.xml | 36 + ...aultsDisabledWithFrameOptionsAllowFrom.xml | 36 + ...edWithFrameOptionsAllowFromBlankOrigin.xml | 36 + ...abledWithFrameOptionsAllowFromNoOrigin.xml | 36 + ...bledWithFrameOptionsAllowFromWhitelist.xml | 36 + ...s-DefaultsDisabledWithFrameOptionsDeny.xml | 36 + ...ultsDisabledWithFrameOptionsSameOrigin.xml | 36 + ...rsConfigTests-DefaultsDisabledWithHpkp.xml | 40 + ...Tests-DefaultsDisabledWithHpkpDefaults.xml | 40 + ...aultsDisabledWithHpkpIncludeSubdomains.xml | 40 + ...igTests-DefaultsDisabledWithHpkpMaxAge.xml | 40 + ...igTests-DefaultsDisabledWithHpkpReport.xml | 40 + ...ests-DefaultsDisabledWithHpkpReportUri.xml | 40 + ...rsConfigTests-DefaultsDisabledWithHsts.xml | 36 + ...igTests-DefaultsDisabledWithNoOverride.xml | 34 + ...sts-DefaultsDisabledWithOnlyHeaderName.xml | 36 + ...ts-DefaultsDisabledWithOnlyHeaderValue.xml | 36 + ...sts-DefaultsDisabledWithReferrerPolicy.xml | 36 + ...tsDisabledWithReferrerPolicySameOrigin.xml | 36 + ...ests-DefaultsDisabledWithXssProtection.xml | 36 + ...aultsDisabledWithXssProtectionDisabled.xml | 36 + ...edWithXssProtectionDisabledAndBlockSet.xml | 36 + ...faultsDisabledWithXssProtectionEnabled.xml | 36 + ...eadersConfigTests-FrameOptionsDisabled.xml | 36 + ...s-FrameOptionsDisabledSpecifyingPolicy.xml | 36 + ...HttpHeadersConfigTests-HeadersDisabled.xml | 34 + ...ests-HeadersDisabledHavingChildElement.xml | 32 + ...adersDisabledWithContentSecurityPolicy.xml | 36 + .../HttpHeadersConfigTests-HeadersEnabled.xml | 34 + .../HttpHeadersConfigTests-HpkpDisabled.xml | 40 + .../HttpHeadersConfigTests-HstsDisabled.xml | 36 + ...stsDisabledSpecifyingIncludeSubdomains.xml | 36 + ...nfigTests-HstsDisabledSpecifyingMaxAge.xml | 36 + ...s-HstsDisabledSpecifyingRequestMatcher.xml | 36 + ...ttpHeadersConfigTests-WithFrameOptions.xml | 36 + ...adersConfigTests-XssProtectionDisabled.xml | 36 + ...gTests-XssProtectionDisabledAndEnabled.xml | 36 + ...s-XssProtectionDisabledSpecifyingBlock.xml | 36 + 54 files changed, 2672 insertions(+), 961 deletions(-) delete mode 100644 config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy create mode 100644 config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-CacheControlDisabled.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentSecurityPolicyWithEmptyDirectives.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentSecurityPolicyWithPolicyDirectives.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentSecurityPolicyWithReportOnly.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentTypeOptionsDisabled.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultConfig.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCacheControl.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithContentSecurityPolicy.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithContentTypeOptions.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCustomHeader.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCustomHeaderWriter.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCustomHstsRequestMatcher.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithEmptyHpkp.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithEmptyPins.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptions.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFrom.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFromBlankOrigin.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFromNoOrigin.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFromWhitelist.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsDeny.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsSameOrigin.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkp.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpDefaults.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpIncludeSubdomains.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpMaxAge.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpReport.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpReportUri.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHsts.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithNoOverride.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithOnlyHeaderName.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithOnlyHeaderValue.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithReferrerPolicy.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithReferrerPolicySameOrigin.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtection.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtectionDisabled.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtectionDisabledAndBlockSet.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtectionEnabled.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-FrameOptionsDisabled.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-FrameOptionsDisabledSpecifyingPolicy.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersDisabled.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersDisabledHavingChildElement.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersDisabledWithContentSecurityPolicy.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersEnabled.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HpkpDisabled.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabled.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabledSpecifyingIncludeSubdomains.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabledSpecifyingMaxAge.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabledSpecifyingRequestMatcher.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-WithFrameOptions.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-XssProtectionDisabled.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-XssProtectionDisabledAndEnabled.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-XssProtectionDisabledSpecifyingBlock.xml diff --git a/config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy deleted file mode 100644 index ca3c807cc7e..00000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy +++ /dev/null @@ -1,961 +0,0 @@ -/* - * Copyright 2002-2016 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ -package org.springframework.security.config.http - -import org.springframework.beans.factory.BeanCreationException -import org.springframework.beans.factory.parsing.BeanDefinitionParsingException -import org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException -import org.springframework.mock.web.MockFilterChain -import org.springframework.mock.web.MockHttpServletRequest -import org.springframework.mock.web.MockHttpServletResponse -import org.springframework.security.web.FilterChainProxy -import org.springframework.security.web.header.HeaderWriterFilter -import org.springframework.security.web.header.writers.StaticHeadersWriter -import org.springframework.security.web.util.matcher.AnyRequestMatcher - -/** - * - * @author Rob Winch - * @author Tim Ysewyn - */ -class HttpHeadersConfigTests extends AbstractHttpConfigTests { - def defaultHeaders = ['X-Content-Type-Options':'nosniff', - 'X-Frame-Options':'DENY', - 'Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains', - 'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate', - 'Expires' : '0', - 'Pragma':'no-cache', - 'X-XSS-Protection' : '1; mode=block'] - def 'headers disabled'() { - setup: - httpAutoConfig { - 'headers'(disabled:true) - } - createAppContext() - - when: - def hf = getFilter(HeaderWriterFilter) - then: - !hf - } - - def 'headers disabled with child fails'() { - when: - httpAutoConfig { - 'headers'(disabled:true) { - 'content-type-options'() - } - } - createAppContext() - then: - thrown(BeanDefinitionParsingException) - } - - def 'default headers'() { - httpAutoConfig { - } - createAppContext() - - when: - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) - then: - assertHeaders(response, defaultHeaders) - } - - def 'http headers with empty headers'() { - setup: - httpAutoConfig { - 'headers'() - } - createAppContext() - when: - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) - then: - assertHeaders(response, defaultHeaders) - } - - def 'http headers frame-options@policy=SAMEORIGIN with defaults'() { - httpAutoConfig { - 'headers'() { - 'frame-options'(policy:'SAMEORIGIN') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) - def expectedHeaders = [:] << defaultHeaders - expectedHeaders['X-Frame-Options'] = 'SAMEORIGIN' - - expect: - assertHeaders(response, expectedHeaders) - } - - - // --- defaults disabled - - // gh-3986 - def 'http headers defaults-disabled with no override'() { - httpAutoConfig { - 'headers'('defaults-disabled':true) { - } - } - createAppContext() - - expect: - getFilter(HeaderWriterFilter) == null - } - - def 'http headers content-type-options'() { - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'content-type-options'() - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) - - expect: - assertHeaders(response, ['X-Content-Type-Options':'nosniff']) - } - - def 'http headers frame-options defaults to DENY'() { - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'frame-options'() - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) - - expect: - assertHeaders(response, ['X-Frame-Options':'DENY']) - } - - def 'http headers frame-options DENY'() { - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'frame-options'(policy : 'DENY') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) - - expect: - assertHeaders(response, ['X-Frame-Options':'DENY']) - } - - def 'http headers frame-options SAMEORIGIN'() { - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'frame-options'(policy : 'SAMEORIGIN') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) - - expect: - assertHeaders(response, ['X-Frame-Options':'SAMEORIGIN']) - } - - def 'http headers frame-options ALLOW-FROM no origin reports error'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'frame-options'(policy : 'ALLOW-FROM', strategy : 'static') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - - then: - BeanDefinitionParsingException e = thrown() - e.message.contains "Strategy requires a 'value' to be set." // FIME better error message? - } - - def 'http headers frame-options ALLOW-FROM spaces only origin reports error'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'frame-options'(policy : 'ALLOW-FROM', strategy: 'static', value : ' ') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - - then: - BeanDefinitionParsingException e = thrown() - e.message.contains "Strategy requires a 'value' to be set." // FIME better error message? - } - - def 'http headers frame-options ALLOW-FROM'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'frame-options'(policy : 'ALLOW-FROM', strategy: 'static', value : 'https://example.com') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) - - then: - assertHeaders(response, ['X-Frame-Options':'ALLOW-FROM https://example.com']) - } - - def 'http headers frame-options ALLOW-FROM with whitelist strategy'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'frame-options'(policy : 'ALLOW-FROM', strategy: 'whitelist', value : 'https://example.com') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - - def request = new MockHttpServletRequest("GET", "") - request.setParameter("from", "https://example.com"); - hf.doFilter(request, response, new MockFilterChain()) - - then: - assertHeaders(response, ['X-Frame-Options':'ALLOW-FROM https://example.com']) - } - - def 'http headers header a=b'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'header'(name : 'a', value: 'b') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) - - then: - assertHeaders(response, ['a':'b']) - } - - def 'http headers header a=b and c=d'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'header'(name : 'a', value: 'b') - 'header'(name : 'c', value: 'd') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) - - then: - assertHeaders(response , ['a':'b', 'c':'d']) - } - - def 'http headers with ref'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'header'(ref:'headerWriter') - } - } - xml.'b:bean'(id: 'headerWriter', 'class': StaticHeadersWriter.name) { - 'b:constructor-arg'(value:'abc') {} - 'b:constructor-arg'(value:'def') {} - } - createAppContext() - when: - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) - then: - assertHeaders(response, ['abc':'def']) - } - - def 'http headers header no name produces error'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'header'(value: 'b') - } - } - createAppContext() - - then: - thrown(BeanCreationException) - } - - def 'http headers header no value produces error'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'header'(name: 'a') - } - } - createAppContext() - - then: - thrown(BeanCreationException) - } - - def 'http headers xss-protection defaults'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'xss-protection'() - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) - - then: - assertHeaders(response, ['X-XSS-Protection':'1; mode=block']) - } - - def 'http headers xss-protection enabled=true'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'xss-protection'(enabled:'true') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) - - then: - assertHeaders(response, ['X-XSS-Protection':'1; mode=block']) - } - - def 'http headers xss-protection enabled=false'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'xss-protection'(enabled:'false') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) - - then: - assertHeaders(response, ['X-XSS-Protection':'0']) - } - - def 'http headers xss-protection enabled=false and block=true produces error'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'xss-protection'(enabled:'false', block:'true') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - - then: - BeanCreationException e = thrown() - e.message.contains 'Cannot set block to true with enabled false' - } - - def 'http headers cache-control'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'cache-control'() - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) - then: - assertHeaders(response, ['Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate', - 'Expires' : '0', - 'Pragma':'no-cache']) - } - - def 'http headers hsts'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hsts'() - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) - then: - assertHeaders(response, ['Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains']) - } - - def 'http headers hsts default only invokes on HttpServletRequest.isSecure = true'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hsts'() - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) - then: - response.headerNames.empty - } - - def 'http headers hsts custom'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hsts'('max-age-seconds':'1','include-subdomains':false, 'request-matcher-ref' : 'matcher') - } - } - - xml.'b:bean'(id: 'matcher', 'class': AnyRequestMatcher.name) - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) - then: - assertHeaders(response, ['Strict-Transport-Security': 'max-age=1']) - } - - def 'http headers hpkp no pins'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hpkp'() - } - } - when: - createAppContext() - then: - XmlBeanDefinitionStoreException expected = thrown() - expected.message.contains 'The content of element \'hpkp\' is not complete' - } - - def 'http headers hpkp no pin'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hpkp'() { - 'pins'() - } - } - } - when: - createAppContext() - then: - XmlBeanDefinitionStoreException expected = thrown() - expected.message.contains 'The content of element \'pins\' is not complete' - } - - def 'http headers hpkp'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hpkp'() { - 'pins'() { - 'pin'('algorithm':'sha256', 'd6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=') - } - } - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) - then: - assertHeaders(response, ['Public-Key-Pins-Report-Only': 'max-age=5184000 ; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM="']) - } - - def 'http headers hpkp with default algorithm'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hpkp'() { - 'pins'() { - 'pin'('d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=') - } - } - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) - then: - assertHeaders(response, ['Public-Key-Pins-Report-Only': 'max-age=5184000 ; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM="']) - } - - def 'http headers hpkp only invokes on HttpServletRequest.isSecure = true'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hpkp'() { - 'pins'() { - 'pin'('algorithm':'sha256', 'E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=') - } - } - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) - then: - response.headerNames.empty - } - - def 'http headers hpkp with custom max age'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hpkp'('max-age-seconds':'604800') { - 'pins'() { - 'pin'('algorithm':'sha256', 'd6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=') - } - } - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) - then: - assertHeaders(response, ['Public-Key-Pins-Report-Only': 'max-age=604800 ; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM="']) - } - - def 'http headers hpkp@reportOnly=false'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hpkp'('report-only':'false') { - 'pins'() { - 'pin'('algorithm':'sha256', 'E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=') - } - } - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure: true, method: "GET"), response, new MockFilterChain()) - then: - assertHeaders(response, ['Public-Key-Pins': 'max-age=5184000 ; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="']) - } - - def 'http headers hpkp@includeSubDomains=true'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hpkp'('include-subdomains':'true') { - 'pins'() { - 'pin'('algorithm':'sha256', 'E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=') - } - } - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure: true, method: "GET"), response, new MockFilterChain()) - then: - assertHeaders(response, ['Public-Key-Pins-Report-Only': 'max-age=5184000 ; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=" ; includeSubDomains']) - } - - def 'http headers hpkp with report-uri'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hpkp'('report-uri':'http://example.net/pkp-report') { - 'pins'() { - 'pin'('algorithm':'sha256', 'E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=') - } - } - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure: true, method: "GET"), response, new MockFilterChain()) - then: - assertHeaders(response, ['Public-Key-Pins-Report-Only': 'max-age=5184000 ; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=" ; report-uri="http://example.net/pkp-report"']) - } - - // --- disable single default header --- - - def 'http headers cache-controls@disabled=true'() { - setup: - httpAutoConfig { - 'headers'() { - 'cache-control'(disabled:true) - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - def expectedHeaders = [:] << defaultHeaders - expectedHeaders.remove('Cache-Control') - expectedHeaders.remove('Expires') - expectedHeaders.remove('Pragma') - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) - then: - assertHeaders(response, expectedHeaders) - } - - def 'http headers content-type-options@disabled=true'() { - setup: - httpAutoConfig { - 'headers'() { - 'content-type-options'(disabled:true) - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - def expectedHeaders = [:] << defaultHeaders - expectedHeaders.remove('X-Content-Type-Options') - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) - then: - assertHeaders(response, expectedHeaders) - } - - def 'http headers hsts@disabled=true'() { - setup: - httpAutoConfig { - 'headers'() { - 'hsts'(disabled:true) - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - def expectedHeaders = [:] << defaultHeaders - expectedHeaders.remove('Strict-Transport-Security') - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) - then: - assertHeaders(response, expectedHeaders) - } - - def 'http headers hpkp@disabled=true'() { - setup: - httpAutoConfig { - 'headers'() { - 'hpkp'(disabled:true) { - 'pins'() { - 'pin'('algorithm':'sha256', 'E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=') - } - } - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - def expectedHeaders = [:] << defaultHeaders - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) - then: - assertHeaders(response, expectedHeaders) - } - - def 'http headers frame-options@disabled=true'() { - setup: - httpAutoConfig { - 'headers'() { - 'frame-options'(disabled:true) - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - def expectedHeaders = [:] << defaultHeaders - expectedHeaders.remove('X-Frame-Options') - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) - then: - assertHeaders(response, expectedHeaders) - } - - def 'http headers xss-protection@disabled=true'() { - setup: - httpAutoConfig { - 'headers'() { - 'xss-protection'(disabled:true) - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - def expectedHeaders = [:] << defaultHeaders - expectedHeaders.remove('X-XSS-Protection') - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) - then: - assertHeaders(response, expectedHeaders) - } - - // --- disable error handling --- - - def 'http headers hsts@disabled=true no include-subdomains'() { - setup: - httpAutoConfig { - 'headers'() { - 'hsts'(disabled:true,'include-subdomains':true) - } - } - when: - createAppContext() - then: - BeanDefinitionParsingException expected = thrown() - expected.message.contains 'include-subdomains' - } - - def 'http headers hsts@disabled=true no max-age'() { - setup: - httpAutoConfig { - 'headers'() { - 'hsts'(disabled:true,'max-age-seconds':123) - } - } - when: - createAppContext() - then: - BeanDefinitionParsingException expected = thrown() - expected.message.contains 'max-age' - } - - def 'http headers hsts@disabled=true no matcher-ref'() { - setup: - httpAutoConfig { - 'headers'() { - 'hsts'(disabled:true,'request-matcher-ref':'matcher') - } - } - xml.'b:bean'(id: 'matcher', 'class': AnyRequestMatcher.name) - when: - createAppContext() - then: - BeanDefinitionParsingException expected = thrown() - expected.message.contains 'request-matcher-ref' - } - - def 'http xss@disabled=true no enabled'() { - setup: - httpAutoConfig { - 'headers'() { - 'xss-protection'(disabled:true,'enabled':true) - } - } - when: - createAppContext() - then: - BeanDefinitionParsingException expected = thrown() - expected.message.contains 'enabled' - } - - def 'http xss@disabled=true no block'() { - setup: - httpAutoConfig { - 'headers'() { - 'xss-protection'(disabled:true,'block':true) - } - } - when: - createAppContext() - then: - BeanDefinitionParsingException expected = thrown() - expected.message.contains 'block' - } - - def 'http frame-options@disabled=true no policy'() { - setup: - httpAutoConfig { - 'headers'() { - 'frame-options'(disabled:true,'policy':'DENY') - } - } - when: - createAppContext() - then: - BeanDefinitionParsingException expected = thrown() - expected.message.contains 'policy' - } - - def 'http headers defaults : content-security-policy'() { - setup: - httpAutoConfig { - 'headers'() { - 'content-security-policy'('policy-directives':'default-src \'self\'') - } - } - createAppContext() - when: - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) - def expectedHeaders = [:] << defaultHeaders - expectedHeaders['Content-Security-Policy'] = 'default-src \'self\'' - then: - assertHeaders(response, expectedHeaders) - } - - def 'http headers disabled : content-security-policy not included'() { - setup: - httpAutoConfig { - 'headers'(disabled:true) { - 'content-security-policy'('policy-directives':'default-src \'self\'') - } - } - createAppContext() - when: - def hf = getFilter(HeaderWriterFilter) - then: - !hf - } - - def 'http headers defaults disabled : content-security-policy only'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'content-security-policy'('policy-directives':'default-src \'self\'') - } - } - createAppContext() - when: - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) - then: - assertHeaders(response, ['Content-Security-Policy':'default-src \'self\'']) - } - - def 'http headers defaults : content-security-policy with empty directives'() { - when: - httpAutoConfig { - 'headers'() { - 'content-security-policy'('policy-directives':'') - } - } - createAppContext() - then: - thrown(BeanDefinitionParsingException) - } - - def 'http headers defaults : content-security-policy report-only=true'() { - setup: - httpAutoConfig { - 'headers'() { - 'content-security-policy'('policy-directives':'default-src https:; report-uri https://example.com/', 'report-only':true) - } - } - createAppContext() - when: - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(secure:true, method: "GET"), response, new MockFilterChain()) - def expectedHeaders = [:] << defaultHeaders - expectedHeaders['Content-Security-Policy-Report-Only'] = 'default-src https:; report-uri https://example.com/' - then: - assertHeaders(response, expectedHeaders) - } - - def 'http headers defaults : referrer-policy'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'referrer-policy'() - } - } - createAppContext() - when: - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) - then: - assertHeaders(response, ['Referrer-Policy': 'no-referrer']) - } - - def 'http headers defaults : referrer-policy same-origin'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'referrer-policy'('policy': 'same-origin') - } - } - createAppContext() - when: - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()) - then: - assertHeaders(response, ['Referrer-Policy': 'same-origin']) - } - - def assertHeaders(MockHttpServletResponse response, Map expected) { - assert response.headerNames == expected.keySet() - expected.each { headerName, value -> - assert response.getHeaderValues(headerName) == [value] - } - } -} diff --git a/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java b/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java new file mode 100644 index 00000000000..6e34b5f5bc6 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java @@ -0,0 +1,776 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.http; + +import com.google.common.collect.ImmutableMap; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * + * @author Rob Winch + * @author Tim Ysewyn + * @author Josh Cummings + */ +public class HttpHeadersConfigTests { + + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/HttpHeadersConfigTests"; + + static final Map defaultHeaders = + ImmutableMap.builder() + .put("X-Content-Type-Options", "nosniff") + .put("X-Frame-Options", "DENY") + .put("Strict-Transport-Security", "max-age=31536000 ; includeSubDomains") + .put("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate") + .put("Expires", "0") + .put("Pragma", "no-cache") + .put("X-XSS-Protection", "1; mode=block") + .build(); + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + MockMvc mvc; + + @Test + public void requestWhenHeadersDisabledThenResponseExcludesAllSecureHeaders() + throws Exception { + + this.spring.configLocations(this.xml("HeadersDisabled")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()); + } + + @Test + public void configureWhenHeadersDisabledHavingChildElementThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("HeadersDisabledHavingChildElement")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class) + .hasMessageContaining("Cannot specify with child elements"); + } + + @Test + public void requestWhenHeadersEnabledThenResponseContainsAllSecureHeaders() + throws Exception { + + this.spring.configLocations(this.xml("DefaultConfig")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includesDefaults()); + } + + @Test + public void requestWhenHeadersElementUsedThenResponseContainsAllSecureHeaders() + throws Exception { + + this.spring.configLocations(this.xml("HeadersEnabled")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includesDefaults()); + } + + @Test + public void requestWhenFrameOptionsConfiguredThenIncludesHeader() + throws Exception { + + Map headers = new HashMap(defaultHeaders); + headers.put("X-Frame-Options", "SAMEORIGIN"); + + this.spring.configLocations(this.xml("WithFrameOptions")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includes(headers)); + } + + // -- defaults disabled + + /** + * gh-3986 + */ + @Test + public void requestWhenDefaultsDisabledWithNoOverrideThenExcludesAllSecureHeaders() + throws Exception { + + this.spring.configLocations(this.xml("DefaultsDisabledWithNoOverride")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()); + } + + @Test + public void requestWhenUsingContentTypeOptionsThenDefaultsToNoSniff() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("X-Content-Type-Options"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithContentTypeOptions")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("X-Content-Type-Options", "nosniff")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void requestWhenUsingFrameOptionsThenDefaultsToDeny() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("X-Frame-Options"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithFrameOptions")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("X-Frame-Options", "DENY")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void requestWhenUsingFrameOptionsDenyThenRespondsWithDeny() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("X-Frame-Options"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithFrameOptionsDeny")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("X-Frame-Options", "DENY")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void requestWhenUsingFrameOptionsSameOriginThenRespondsWithSameOrigin() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("X-Frame-Options"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithFrameOptionsSameOrigin")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("X-Frame-Options", "SAMEORIGIN")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void configureWhenUsingFrameOptionsAllowFromNoOriginThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("DefaultsDisabledWithFrameOptionsAllowFromNoOrigin")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class) + .hasMessageContaining("Strategy requires a 'value' to be set."); // FIXME better error message? + } + + @Test + public void configureWhenUsingFrameOptionsAllowFromBlankOriginThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("DefaultsDisabledWithFrameOptionsAllowFromBlankOrigin")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class) + .hasMessageContaining("Strategy requires a 'value' to be set."); // FIXME better error message? + } + + @Test + public void requestWhenUsingFrameOptionsAllowFromThenRespondsWithAllowFrom() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("X-Frame-Options"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithFrameOptionsAllowFrom")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("X-Frame-Options", "ALLOW-FROM https://example.org")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void requestWhenUsingFrameOptionsAllowFromWhitelistThenRespondsWithAllowFrom() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("X-Frame-Options"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithFrameOptionsAllowFromWhitelist")).autowire(); + + this.mvc.perform(get("/").param("from", "https://example.org")) + .andExpect(status().isOk()) + .andExpect(header().string("X-Frame-Options", "ALLOW-FROM https://example.org")) + .andExpect(excludes(excludedHeaders)); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("X-Frame-Options", "DENY")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void requestWhenUsingCustomHeaderThenRespondsWithThatHeader() + throws Exception { + + this.spring.configLocations(this.xml("DefaultsDisabledWithCustomHeader")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("a", "b")) + .andExpect(header().string("c", "d")) + .andExpect(excludesDefaults()); + } + + @Test + public void requestWhenUsingCustomHeaderWriterThenRespondsWithThatHeader() + throws Exception { + + this.spring.configLocations(this.xml("DefaultsDisabledWithCustomHeaderWriter")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("abc", "def")) + .andExpect(excludesDefaults()); + } + + @Test + public void configureWhenUsingCustomHeaderNameOnlyThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("DefaultsDisabledWithOnlyHeaderName")).autowire()) + .isInstanceOf(BeanCreationException.class); + } + + @Test + public void configureWhenUsingCustomHeaderValueOnlyThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("DefaultsDisabledWithOnlyHeaderValue")).autowire()) + .isInstanceOf(BeanCreationException.class); + } + + @Test + public void requestWhenUsingXssProtectionThenDefaultsToModeBlock() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("X-XSS-Protection"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithXssProtection")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("X-XSS-Protection", "1; mode=block")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void requestWhenEnablingXssProtectionThenDefaultsToModeBlock() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("X-XSS-Protection"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithXssProtectionEnabled")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("X-XSS-Protection", "1; mode=block")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void requestWhenDisablingXssProtectionThenDefaultsToZero() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("X-XSS-Protection"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithXssProtectionDisabled")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("X-XSS-Protection", "0")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void configureWhenXssProtectionDisabledAndBlockSetThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("DefaultsDisabledWithXssProtectionDisabledAndBlockSet")).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasMessageContaining("Cannot set block to true with enabled false"); + } + + @Test + public void requestWhenUsingCacheControlThenRespondsWithCorrespondingHeaders() + throws Exception { + + Map includedHeaders = ImmutableMap.builder() + .put("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate") + .put("Expires", "0") + .put("Pragma", "no-cache") + .build(); + + this.spring.configLocations(this.xml("DefaultsDisabledWithCacheControl")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(includes(includedHeaders)); + } + + @Test + public void requestWhenUsingHstsThenRespondsWithHstsHeader() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("Strict-Transport-Security"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithHsts")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(header().string("Strict-Transport-Security", "max-age=31536000 ; includeSubDomains")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void insecureRequestWhenUsingHstsThenExcludesHstsHeader() + throws Exception { + + this.spring.configLocations(this.xml("DefaultsDisabledWithHsts")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()); + } + + @Test + public void insecureRequestWhenUsingCustomHstsRequestMatcherThenIncludesHstsHeader() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("Strict-Transport-Security"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithCustomHstsRequestMatcher")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("Strict-Transport-Security", "max-age=1")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void configureWhenUsingHpkpWithoutPinsThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("DefaultsDisabledWithEmptyHpkp")).autowire()) + .isInstanceOf(XmlBeanDefinitionStoreException.class) + .hasMessageContaining("The content of element 'hpkp' is not complete"); + } + + @Test + public void configureWhenUsingHpkpWithEmptyPinsThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("DefaultsDisabledWithEmptyPins")).autowire()) + .isInstanceOf(XmlBeanDefinitionStoreException.class) + .hasMessageContaining("The content of element 'pins' is not complete"); + } + + @Test + public void requestWhenUsingHpkpThenIncludesHpkpHeader() + throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithHpkp")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(header().string( + "Public-Key-Pins-Report-Only", + "max-age=5184000 ; pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\"")) + .andExpect(excludesDefaults()); + } + + @Test + public void requestWhenUsingHpkpDefaultsThenIncludesHpkpHeaderUsingSha256() + throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithHpkpDefaults")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(header().string( + "Public-Key-Pins-Report-Only", + "max-age=5184000 ; pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\"")) + .andExpect(excludesDefaults()); + } + + @Test + public void insecureRequestWhenUsingHpkpThenExcludesHpkpHeader() + throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithHpkpDefaults")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().doesNotExist("Public-Key-Pins-Report-Only")) + .andExpect(excludesDefaults()); + } + + @Test + public void requestWhenUsingHpkpCustomMaxAgeThenIncludesHpkpHeaderAccordingly() + throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithHpkpMaxAge")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(header().string( + "Public-Key-Pins-Report-Only", + "max-age=604800 ; pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\"")) + .andExpect(excludesDefaults()); + } + + @Test + public void requestWhenUsingHpkpReportThenIncludesHpkpHeaderAccordingly() + throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithHpkpReport")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(header().string( + "Public-Key-Pins", + "max-age=5184000 ; pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\"")) + .andExpect(excludesDefaults()); + } + + @Test + public void requestWhenUsingHpkpIncludeSubdomainsThenIncludesHpkpHeaderAccordingly() + throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithHpkpIncludeSubdomains")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(header().string( + "Public-Key-Pins-Report-Only", + "max-age=5184000 ; pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\" ; includeSubDomains")) + .andExpect(excludesDefaults()); + } + + @Test + public void requestWhenUsingHpkpReportUriThenIncludesHpkpHeaderAccordingly() + throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithHpkpReportUri")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(header().string( + "Public-Key-Pins-Report-Only", + "max-age=5184000 ; pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\" ; report-uri=\"http://example.net/pkp-report\"")) + .andExpect(excludesDefaults()); + } + + // -- single-header disabled + + @Test + public void requestWhenCacheControlDisabledThenExcludesHeader() + throws Exception { + + Collection cacheControl = Arrays.asList("Cache-Control", "Expires", "Pragma"); + Map allButCacheControl = remove(defaultHeaders, cacheControl); + + this.spring.configLocations(this.xml("CacheControlDisabled")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includes(allButCacheControl)) + .andExpect(excludes(cacheControl)); + } + + @Test + public void requestWhenContentTypeOptionsDisabledThenExcludesHeader() + throws Exception { + + Collection contentTypeOptions = Arrays.asList("X-Content-Type-Options"); + Map allButContentTypeOptions = remove(defaultHeaders, contentTypeOptions); + + this.spring.configLocations(this.xml("ContentTypeOptionsDisabled")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includes(allButContentTypeOptions)) + .andExpect(excludes(contentTypeOptions)); + } + + @Test + public void requestWhenHstsDisabledThenExcludesHeader() + throws Exception { + + Collection hsts = Arrays.asList("Strict-Transport-Security"); + Map allButHsts = remove(defaultHeaders, hsts); + + this.spring.configLocations(this.xml("HstsDisabled")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includes(allButHsts)) + .andExpect(excludes(hsts)); + } + + @Test + public void requestWhenHpkpDisabledThenExcludesHeader() + throws Exception { + + this.spring.configLocations(this.xml("HpkpDisabled")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includesDefaults()); + } + + @Test + public void requestWhenFrameOptionsDisabledThenExcludesHeader() + throws Exception { + + Collection frameOptions = Arrays.asList("X-Frame-Options"); + Map allButFrameOptions = remove(defaultHeaders, frameOptions); + + this.spring.configLocations(this.xml("FrameOptionsDisabled")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includes(allButFrameOptions)) + .andExpect(excludes(frameOptions)); + } + + @Test + public void requestWhenXssProtectionDisabledThenExcludesHeader() + throws Exception { + + Collection xssProtection = Arrays.asList("X-XSS-Protection"); + Map allButXssProtection = remove(defaultHeaders, xssProtection); + + this.spring.configLocations(this.xml("XssProtectionDisabled")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includes(allButXssProtection)) + .andExpect(excludes(xssProtection)); + } + + // --- disable error handling --- + + @Test + public void configureWhenHstsDisabledAndIncludeSubdomainsSpecifiedThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("HstsDisabledSpecifyingIncludeSubdomains")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class) + .hasMessageContaining("include-subdomains"); + } + + @Test + public void configureWhenHstsDisabledAndMaxAgeSpecifiedThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("HstsDisabledSpecifyingMaxAge")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class) + .hasMessageContaining("max-age"); + } + + @Test + public void configureWhenHstsDisabledAndRequestMatcherSpecifiedThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("HstsDisabledSpecifyingRequestMatcher")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class) + .hasMessageContaining("request-matcher-ref"); + } + + @Test + public void configureWhenXssProtectionDisabledAndEnabledThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("XssProtectionDisabledAndEnabled")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class) + .hasMessageContaining("enabled"); + } + + @Test + public void configureWhenXssProtectionDisabledAndBlockSpecifiedThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("XssProtectionDisabledSpecifyingBlock")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class) + .hasMessageContaining("block"); + } + + @Test + public void configureWhenFrameOptionsDisabledAndPolicySpecifiedThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("FrameOptionsDisabledSpecifyingPolicy")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class) + .hasMessageContaining("policy"); + } + + @Test + public void requestWhenContentSecurityPolicyDirectivesConfiguredThenIncludesDirectives() + throws Exception { + + Map includedHeaders = new HashMap<>(defaultHeaders); + includedHeaders.put("Content-Security-Policy", "default-src 'self'"); + + this.spring.configLocations(this.xml("ContentSecurityPolicyWithPolicyDirectives")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includes(includedHeaders)); + } + + @Test + public void requestWhenHeadersDisabledAndContentSecurityPolicyConfiguredThenExcludesHeader() + throws Exception { + + this.spring.configLocations(this.xml("HeadersDisabledWithContentSecurityPolicy")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()) + .andExpect(excludes("Content-Security-Policy")); + } + + @Test + public void requestWhenDefaultsDisabledAndContentSecurityPolicyConfiguredThenIncludesHeader() + throws Exception { + + this.spring.configLocations(this.xml("DefaultsDisabledWithContentSecurityPolicy")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()) + .andExpect(header().string("Content-Security-Policy", "default-src 'self'")); + } + + @Test + public void configureWhenContentSecurityPolicyConfiguredWithEmptyDirectivesThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("ContentSecurityPolicyWithEmptyDirectives")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class); + } + + @Test + public void requestWhenContentSecurityPolicyConfiguredWithReportOnlyThenIncludesReportOnlyHeader() + throws Exception { + + Map includedHeaders = new HashMap<>(defaultHeaders); + includedHeaders.put("Content-Security-Policy-Report-Only", "default-src https:; report-uri https://example.org/"); + + this.spring.configLocations(this.xml("ContentSecurityPolicyWithReportOnly")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includes(includedHeaders)); + } + + @Test + public void requestWhenReferrerPolicyConfiguredThenResponseDefaultsToNoReferrer() + throws Exception { + + this.spring.configLocations(this.xml("DefaultsDisabledWithReferrerPolicy")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()) + .andExpect(header().string("Referrer-Policy", "no-referrer")); + } + + @Test + public void requestWhenReferrerPolicyConfiguredWithSameOriginThenRespondsWithSameOrigin() + throws Exception { + + this.spring.configLocations(this.xml("DefaultsDisabledWithReferrerPolicySameOrigin")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()) + .andExpect(header().string("Referrer-Policy", "same-origin")); + } + + @RestController + public static class SimpleController { + @GetMapping("/") + public String ok() { return "ok"; } + } + + private static ResultMatcher includesDefaults() { + return includes(defaultHeaders); + } + + private static ResultMatcher includes(Map headers) { + return result -> { + for ( Map.Entry header : headers.entrySet() ) { + header().string(header.getKey(), header.getValue()).match(result); + } + }; + } + + private static ResultMatcher excludesDefaults() { + return excludes(defaultHeaders.keySet()); + } + + private static ResultMatcher excludes(Collection headers) { + return result -> { + for ( String name : headers ) { + header().doesNotExist(name).match(result); + } + }; + } + + private static ResultMatcher excludes(String... headers) { + return excludes(Arrays.asList(headers)); + } + + private static Map remove(Map map, Collection keys) { + Map copy = new HashMap<>(map); + + for ( K key : keys ) { + copy.remove(key); + } + + return copy; + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-CacheControlDisabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-CacheControlDisabled.xml new file mode 100644 index 00000000000..8cc3f97354f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-CacheControlDisabled.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentSecurityPolicyWithEmptyDirectives.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentSecurityPolicyWithEmptyDirectives.xml new file mode 100644 index 00000000000..b6c64a228b8 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentSecurityPolicyWithEmptyDirectives.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentSecurityPolicyWithPolicyDirectives.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentSecurityPolicyWithPolicyDirectives.xml new file mode 100644 index 00000000000..ff3eee270ac --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentSecurityPolicyWithPolicyDirectives.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentSecurityPolicyWithReportOnly.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentSecurityPolicyWithReportOnly.xml new file mode 100644 index 00000000000..55ae7559b80 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentSecurityPolicyWithReportOnly.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentTypeOptionsDisabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentTypeOptionsDisabled.xml new file mode 100644 index 00000000000..6e2d415bf63 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentTypeOptionsDisabled.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultConfig.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultConfig.xml new file mode 100644 index 00000000000..2cacad9a183 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultConfig.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCacheControl.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCacheControl.xml new file mode 100644 index 00000000000..1692f68fa0d --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCacheControl.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithContentSecurityPolicy.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithContentSecurityPolicy.xml new file mode 100644 index 00000000000..bcc2a6632c3 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithContentSecurityPolicy.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithContentTypeOptions.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithContentTypeOptions.xml new file mode 100644 index 00000000000..ee6f20a0a1a --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithContentTypeOptions.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCustomHeader.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCustomHeader.xml new file mode 100644 index 00000000000..31c6f8d3059 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCustomHeader.xml @@ -0,0 +1,37 @@ + + + + + + + +
    +
    + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCustomHeaderWriter.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCustomHeaderWriter.xml new file mode 100644 index 00000000000..1991d74152c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCustomHeaderWriter.xml @@ -0,0 +1,41 @@ + + + + + + + +
    + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCustomHstsRequestMatcher.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCustomHstsRequestMatcher.xml new file mode 100644 index 00000000000..8fa625c8aa3 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCustomHstsRequestMatcher.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithEmptyHpkp.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithEmptyHpkp.xml new file mode 100644 index 00000000000..6249be2c170 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithEmptyHpkp.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithEmptyPins.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithEmptyPins.xml new file mode 100644 index 00000000000..a37f9e7221c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithEmptyPins.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptions.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptions.xml new file mode 100644 index 00000000000..9fc1c786632 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptions.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFrom.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFrom.xml new file mode 100644 index 00000000000..481c0bc3797 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFrom.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFromBlankOrigin.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFromBlankOrigin.xml new file mode 100644 index 00000000000..3be6ee362e7 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFromBlankOrigin.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFromNoOrigin.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFromNoOrigin.xml new file mode 100644 index 00000000000..47fd5818907 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFromNoOrigin.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFromWhitelist.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFromWhitelist.xml new file mode 100644 index 00000000000..8678f8cb845 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFromWhitelist.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsDeny.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsDeny.xml new file mode 100644 index 00000000000..3bd66acb256 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsDeny.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsSameOrigin.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsSameOrigin.xml new file mode 100644 index 00000000000..dfac109f130 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsSameOrigin.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkp.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkp.xml new file mode 100644 index 00000000000..b6875c77e30 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkp.xml @@ -0,0 +1,40 @@ + + + + + + + + + + d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM= + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpDefaults.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpDefaults.xml new file mode 100644 index 00000000000..8c5b07c97bc --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpDefaults.xml @@ -0,0 +1,40 @@ + + + + + + + + + + d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM= + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpIncludeSubdomains.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpIncludeSubdomains.xml new file mode 100644 index 00000000000..f9a911fa127 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpIncludeSubdomains.xml @@ -0,0 +1,40 @@ + + + + + + + + + + d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM= + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpMaxAge.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpMaxAge.xml new file mode 100644 index 00000000000..d1ee8f8ea0c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpMaxAge.xml @@ -0,0 +1,40 @@ + + + + + + + + + + d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM= + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpReport.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpReport.xml new file mode 100644 index 00000000000..e5d37074e7f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpReport.xml @@ -0,0 +1,40 @@ + + + + + + + + + + d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM= + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpReportUri.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpReportUri.xml new file mode 100644 index 00000000000..3fa18da5a69 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpReportUri.xml @@ -0,0 +1,40 @@ + + + + + + + + + + d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM= + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHsts.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHsts.xml new file mode 100644 index 00000000000..6e1cebc7896 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHsts.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithNoOverride.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithNoOverride.xml new file mode 100644 index 00000000000..f7619328d44 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithNoOverride.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithOnlyHeaderName.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithOnlyHeaderName.xml new file mode 100644 index 00000000000..7978bd08c3e --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithOnlyHeaderName.xml @@ -0,0 +1,36 @@ + + + + + + + +
    + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithOnlyHeaderValue.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithOnlyHeaderValue.xml new file mode 100644 index 00000000000..fba21c98de1 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithOnlyHeaderValue.xml @@ -0,0 +1,36 @@ + + + + + + + +
    + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithReferrerPolicy.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithReferrerPolicy.xml new file mode 100644 index 00000000000..b9f6b38600b --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithReferrerPolicy.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithReferrerPolicySameOrigin.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithReferrerPolicySameOrigin.xml new file mode 100644 index 00000000000..a7b319576de --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithReferrerPolicySameOrigin.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtection.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtection.xml new file mode 100644 index 00000000000..dfabc869805 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtection.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtectionDisabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtectionDisabled.xml new file mode 100644 index 00000000000..50c01fe7d3e --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtectionDisabled.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtectionDisabledAndBlockSet.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtectionDisabledAndBlockSet.xml new file mode 100644 index 00000000000..b648cde25a1 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtectionDisabledAndBlockSet.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtectionEnabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtectionEnabled.xml new file mode 100644 index 00000000000..cfa732f97b4 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtectionEnabled.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-FrameOptionsDisabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-FrameOptionsDisabled.xml new file mode 100644 index 00000000000..6ab1b5fa066 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-FrameOptionsDisabled.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-FrameOptionsDisabledSpecifyingPolicy.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-FrameOptionsDisabledSpecifyingPolicy.xml new file mode 100644 index 00000000000..73e94a3c87d --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-FrameOptionsDisabledSpecifyingPolicy.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersDisabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersDisabled.xml new file mode 100644 index 00000000000..802f31eaff6 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersDisabled.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersDisabledHavingChildElement.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersDisabledHavingChildElement.xml new file mode 100644 index 00000000000..4946f505e97 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersDisabledHavingChildElement.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersDisabledWithContentSecurityPolicy.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersDisabledWithContentSecurityPolicy.xml new file mode 100644 index 00000000000..64b7287c04c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersDisabledWithContentSecurityPolicy.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersEnabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersEnabled.xml new file mode 100644 index 00000000000..fab128bd329 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersEnabled.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HpkpDisabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HpkpDisabled.xml new file mode 100644 index 00000000000..1b13db83d69 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HpkpDisabled.xml @@ -0,0 +1,40 @@ + + + + + + + + + + d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM= + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabled.xml new file mode 100644 index 00000000000..6bc89f24ca6 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabled.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabledSpecifyingIncludeSubdomains.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabledSpecifyingIncludeSubdomains.xml new file mode 100644 index 00000000000..4fa83aa5d9e --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabledSpecifyingIncludeSubdomains.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabledSpecifyingMaxAge.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabledSpecifyingMaxAge.xml new file mode 100644 index 00000000000..b23dd301a4f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabledSpecifyingMaxAge.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabledSpecifyingRequestMatcher.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabledSpecifyingRequestMatcher.xml new file mode 100644 index 00000000000..903f96c137d --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabledSpecifyingRequestMatcher.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-WithFrameOptions.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-WithFrameOptions.xml new file mode 100644 index 00000000000..cbee408f6f7 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-WithFrameOptions.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-XssProtectionDisabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-XssProtectionDisabled.xml new file mode 100644 index 00000000000..5b2ac369f2f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-XssProtectionDisabled.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-XssProtectionDisabledAndEnabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-XssProtectionDisabledAndEnabled.xml new file mode 100644 index 00000000000..c590b69dea0 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-XssProtectionDisabledAndEnabled.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-XssProtectionDisabledSpecifyingBlock.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-XssProtectionDisabledSpecifyingBlock.xml new file mode 100644 index 00000000000..c183c36d953 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-XssProtectionDisabledSpecifyingBlock.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + From 027dea2a7610461e54b6697e37290ae409850039 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 20 Jun 2018 15:46:58 -0500 Subject: [PATCH 081/226] Update to Spring Boot 2.0.3.RELEASE Fixes: gh-5454 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 42df749d112..84ac09f34e0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ gaeVersion=1.9.64 -springBootVersion=2.0.2.RELEASE +springBootVersion=2.0.3.RELEASE version=5.1.0.BUILD-SNAPSHOT From 5e2a37e621df5b93446a5f15898ff412e6a1c946 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 25 Jun 2018 10:02:30 -0500 Subject: [PATCH 082/226] adding a test --- .../jwt/NimbusJwkReactiveJwtDecoderTests.java | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoderTests.java diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoderTests.java new file mode 100644 index 00000000000..981ad356ae6 --- /dev/null +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoderTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.jwt; + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.security.crypto.keygen.KeyGenerators; + +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import java.util.Map; + +import static org.assertj.core.api.Assertions.*; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class NimbusJwkReactiveJwtDecoderTests { + + String expired = "eyJraWQiOiJrZXktaWQtMSIsImFsZyI6IlJTMjU2In0.eyJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6MTUyOTkzNzYzMX0.Dt5jFOKkB8zAmjciwvlGkj4LNStXWH0HNIfr8YYajIthBIpVgY5Hg_JL8GBmUFzKDgyusT0q60OOg8_Pdi4Lu-VTWyYutLSlNUNayMlyBaVEWfyZJnh2_OwMZr1vRys6HF-o1qZldhwcfvczHg61LwPa1ISoqaAltDTzBu9cGISz2iBUCuR0x71QhbuRNyJdjsyS96NqiM_TspyiOSxmlNch2oAef1MssOQ23CrKilIvEDsz_zk5H94q7rH0giWGdEHCENESsTJS0zvzH6r2xIWjd5WnihFpCPkwznEayxaEhrdvJqT_ceyXCIfY4m3vujPQHNDG0UshpwvDuEbPUg"; + String messageReadToken = "eyJraWQiOiJrZXktaWQtMSIsImFsZyI6IlJTMjU2In0.eyJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6OTIyMzM3MjAwNjA5NjM3NX0.bnQ8IJDXmQbmIXWku0YT1HOyV_3d0iQSA_0W2CmPyELhsxFETzBEEcZ0v0xCBiswDT51rwD83wbX3YXxb84fM64AhpU8wWOxLjha4J6HJX2JnlG47ydaAVD7eWGSYTavyyQ-CwUjQWrfMVcObFZLYG11ydzRYOR9-aiHcK3AobcTcS8jZFeI8EGQV_Cd3IJ018uFCf6VnXLv7eV2kRt08Go2RiPLW47ExvD7Dzzz_wDBKfb4pNem7fDvuzB3UPcp5m9QvLZicnbS_6AvDi6P1y_DFJf-1T5gkGmX5piDH1L1jg2Yl6tjmXbk5B3VhsyjJuXE6gzq1d-xie0Z1NVOxw"; + String noScopes = "eyJraWQiOiJrZXktaWQtMSIsImFsZyI6IlJTMjU2In0.eyJzY29wZSI6IiIsImV4cCI6OTIyMzM3MjAwNjA5NjM3NX0.asF3shV-lLdM4WmsnKd2xjqXu-VJuJjPT-ywkj56lUe4suQDy2tPtkzur7a0uVKj2VDoobzFHOW80F_-67E2aXOJSKBCk9qnqu8GyRiMKdmVekIacEl9EYdZAo6XBvuUJCmcTPNTkJIJifNSQmu33GqJeEw_oJA1CEyg5spIOy_TYCBdQ-jRmuzA5WpdRBmQlr4T-36rccimXwtBLgxK9e7FmUMlP51mkq7UdlOELF6wFn6bh3L4YJbfiKfK-rZAPZjwjio3fr24YTQM4MrqSVTSA5Z0gjHxsz_oTPmrrOzXVY8KVTfkw2OzYuNsPbtlnLJn64cgO2h6AfIc672Aaw"; + private String jwkSet = + "{\n" + + " \"keys\":[\n" + + " {\n" + + " \"kty\":\"RSA\",\n" + + " \"e\":\"AQAB\",\n" + + " \"use\":\"sig\",\n" + + " \"kid\":\"key-id-1\",\n" + + " \"n\":\"qL48v1clgFw-Evm145pmh8nRYiNt72Gupsshn7Qs8dxEydCRp1DPOV_PahPk1y2nvldBNIhfNL13JOAiJ6BTiF-2ICuICAhDArLMnTH61oL1Hepq8W1xpa9gxsnL1P51thvfmiiT4RTW57koy4xIWmIp8ZXXfYgdH2uHJ9R0CQBuYKe7nEOObjxCFWC8S30huOfW2cYtv0iB23h6w5z2fDLjddX6v_FXM7ktcokgpm3_XmvT_-bL6_GGwz9k6kJOyMTubecr-WT__le8ikY66zlplYXRQh6roFfFCL21Pt8xN5zrk-0AMZUnmi8F2S2ztSBmAVJ7H71ELXsURBVZpw\"\n" + + " }\n" + + " ]\n" + + "}"; + + private MockWebServer server; + private NimbusJwkReactiveJwtDecoder decoder; + + @Before + public void setup() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + this.server.enqueue(new MockResponse().setBody(jwkSet)); + this.decoder = new NimbusJwkReactiveJwtDecoder(this.server.url("/certs").toString()); + } + + @After + public void cleanup() throws Exception { + this.server.shutdown(); + } + + @Test + public void decodeWhenMessageReadScopeThenSuccess() { + NimbusJwkReactiveJwtDecoder decoder = new NimbusJwkReactiveJwtDecoder(this.server.url("/certs").toString()); + + Jwt jwt = decoder.decode(this.messageReadToken).block(); + + assertThat(jwt.getClaims().get("scope")).isEqualTo("message:read"); + } + + @Test + public void decodeWhenExpiredThenFail() { + assertThatCode(() -> this.decoder.decode(this.expired).block()) + .isInstanceOf(JwtException.class); + } + + @Test + public void decodeWhenInvalidSignatureThenFail() { + assertThatCode(() -> this.decoder.decode(this.messageReadToken.substring(0, this.messageReadToken.length() - 2)).block()) + .isInstanceOf(JwtException.class); + } + + @Test + public void decodeWhenAlgNoneThenFail() { + assertThatCode(() -> this.decoder.decode("ew0KICAiYWxnIjogIm5vbmUiLA0KICAidHlwIjogIkpXVCINCn0.ew0KICAic3ViIjogIjEyMzQ1Njc4OTAiLA0KICAibmFtZSI6ICJKb2huIERvZSIsDQogICJpYXQiOiAxNTE2MjM5MDIyDQp9.").block()) + .isInstanceOf(JwtException.class) + .hasMessage("Unsupported algorithm of none"); + } +} From 79739dbb4793cc0d43184874597f5d92e1957ca0 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 25 Jun 2018 10:07:43 -0500 Subject: [PATCH 083/226] Fixes in decoder --- .../security/oauth2/jwt/NimbusJwkReactiveJwtDecoder.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoder.java index 1f68843ede8..3ec9e375115 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoder.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoder.java @@ -102,7 +102,7 @@ public Mono decode(String token) throws JwtException { if (jwt instanceof SignedJWT) { return this.decode((SignedJWT) jwt); } - return Mono.empty(); + throw new JwtException("Unsupported algorithm of " + jwt.getHeader().getAlgorithm()); } private JWT parse(String token) { @@ -129,11 +129,8 @@ private JWTClaimsSet createJwkSet(JWT parsedToken, List jwkList) { try { return this.jwtProcessor.process(parsedToken, new JWKContext(jwkList)); } - catch (BadJOSEException e) { - throw new RuntimeException(e); - } - catch (JOSEException e) { - throw new RuntimeException(e); + catch (BadJOSEException | JOSEException e) { + throw new JwtException("Failed to validate the token", e); } } From 91547d256afc30411932fea68f7fa08764ba7508 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 25 Jun 2018 12:08:37 -0500 Subject: [PATCH 084/226] Add NimbusJwkReactiveJwtDecoderTests Issue: gh-5330 --- .../jwt/NimbusJwkReactiveJwtDecoder.java | 5 +- .../jwt/NimbusJwkReactiveJwtDecoderTests.java | 57 ++++++++++++------- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoder.java index 3ec9e375115..7a16daf1882 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoder.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoder.java @@ -119,8 +119,9 @@ private Mono decode(SignedJWT parsedToken) { .createSelector(parsedToken.getHeader()); return this.reactiveJwkSource.get(selector) .map(jwkList -> createJwkSet(parsedToken, jwkList)) - .map(set -> createJwt(parsedToken, set)); - } catch (Exception ex) { + .map(set -> createJwt(parsedToken, set)) + .onErrorMap(e -> new JwtException("An error occurred while attempting to decode the Jwt: ", e)); + } catch (RuntimeException ex) { throw new JwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex); } } diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoderTests.java index 981ad356ae6..ef38f1917f5 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoderTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoderTests.java @@ -16,26 +16,16 @@ package org.springframework.security.oauth2.jwt; -import com.nimbusds.jose.jwk.JWK; -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.RSAKey; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.springframework.security.crypto.keygen.KeyGenerators; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.PrivateKey; -import java.security.interfaces.RSAPrivateKey; -import java.security.spec.PKCS8EncodedKeySpec; -import java.util.Base64; -import java.util.Map; +import java.util.Date; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; /** * @author Rob Winch @@ -43,11 +33,12 @@ */ public class NimbusJwkReactiveJwtDecoderTests { - String expired = "eyJraWQiOiJrZXktaWQtMSIsImFsZyI6IlJTMjU2In0.eyJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6MTUyOTkzNzYzMX0.Dt5jFOKkB8zAmjciwvlGkj4LNStXWH0HNIfr8YYajIthBIpVgY5Hg_JL8GBmUFzKDgyusT0q60OOg8_Pdi4Lu-VTWyYutLSlNUNayMlyBaVEWfyZJnh2_OwMZr1vRys6HF-o1qZldhwcfvczHg61LwPa1ISoqaAltDTzBu9cGISz2iBUCuR0x71QhbuRNyJdjsyS96NqiM_TspyiOSxmlNch2oAef1MssOQ23CrKilIvEDsz_zk5H94q7rH0giWGdEHCENESsTJS0zvzH6r2xIWjd5WnihFpCPkwznEayxaEhrdvJqT_ceyXCIfY4m3vujPQHNDG0UshpwvDuEbPUg"; - String messageReadToken = "eyJraWQiOiJrZXktaWQtMSIsImFsZyI6IlJTMjU2In0.eyJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6OTIyMzM3MjAwNjA5NjM3NX0.bnQ8IJDXmQbmIXWku0YT1HOyV_3d0iQSA_0W2CmPyELhsxFETzBEEcZ0v0xCBiswDT51rwD83wbX3YXxb84fM64AhpU8wWOxLjha4J6HJX2JnlG47ydaAVD7eWGSYTavyyQ-CwUjQWrfMVcObFZLYG11ydzRYOR9-aiHcK3AobcTcS8jZFeI8EGQV_Cd3IJ018uFCf6VnXLv7eV2kRt08Go2RiPLW47ExvD7Dzzz_wDBKfb4pNem7fDvuzB3UPcp5m9QvLZicnbS_6AvDi6P1y_DFJf-1T5gkGmX5piDH1L1jg2Yl6tjmXbk5B3VhsyjJuXE6gzq1d-xie0Z1NVOxw"; - String noScopes = "eyJraWQiOiJrZXktaWQtMSIsImFsZyI6IlJTMjU2In0.eyJzY29wZSI6IiIsImV4cCI6OTIyMzM3MjAwNjA5NjM3NX0.asF3shV-lLdM4WmsnKd2xjqXu-VJuJjPT-ywkj56lUe4suQDy2tPtkzur7a0uVKj2VDoobzFHOW80F_-67E2aXOJSKBCk9qnqu8GyRiMKdmVekIacEl9EYdZAo6XBvuUJCmcTPNTkJIJifNSQmu33GqJeEw_oJA1CEyg5spIOy_TYCBdQ-jRmuzA5WpdRBmQlr4T-36rccimXwtBLgxK9e7FmUMlP51mkq7UdlOELF6wFn6bh3L4YJbfiKfK-rZAPZjwjio3fr24YTQM4MrqSVTSA5Z0gjHxsz_oTPmrrOzXVY8KVTfkw2OzYuNsPbtlnLJn64cgO2h6AfIc672Aaw"; + private String expired = "eyJraWQiOiJrZXktaWQtMSIsImFsZyI6IlJTMjU2In0.eyJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6MTUyOTkzNzYzMX0.Dt5jFOKkB8zAmjciwvlGkj4LNStXWH0HNIfr8YYajIthBIpVgY5Hg_JL8GBmUFzKDgyusT0q60OOg8_Pdi4Lu-VTWyYutLSlNUNayMlyBaVEWfyZJnh2_OwMZr1vRys6HF-o1qZldhwcfvczHg61LwPa1ISoqaAltDTzBu9cGISz2iBUCuR0x71QhbuRNyJdjsyS96NqiM_TspyiOSxmlNch2oAef1MssOQ23CrKilIvEDsz_zk5H94q7rH0giWGdEHCENESsTJS0zvzH6r2xIWjd5WnihFpCPkwznEayxaEhrdvJqT_ceyXCIfY4m3vujPQHNDG0UshpwvDuEbPUg"; + + private String messageReadToken = "eyJraWQiOiJrZXktaWQtMSIsImFsZyI6IlJTMjU2In0.eyJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6OTIyMzM3MjAwNjA5NjM3NX0.bnQ8IJDXmQbmIXWku0YT1HOyV_3d0iQSA_0W2CmPyELhsxFETzBEEcZ0v0xCBiswDT51rwD83wbX3YXxb84fM64AhpU8wWOxLjha4J6HJX2JnlG47ydaAVD7eWGSYTavyyQ-CwUjQWrfMVcObFZLYG11ydzRYOR9-aiHcK3AobcTcS8jZFeI8EGQV_Cd3IJ018uFCf6VnXLv7eV2kRt08Go2RiPLW47ExvD7Dzzz_wDBKfb4pNem7fDvuzB3UPcp5m9QvLZicnbS_6AvDi6P1y_DFJf-1T5gkGmX5piDH1L1jg2Yl6tjmXbk5B3VhsyjJuXE6gzq1d-xie0Z1NVOxw"; + private String jwkSet = - "{\n" + "{\n" + " \"keys\":[\n" + " {\n" + " \"kty\":\"RSA\",\n" @@ -77,19 +68,39 @@ public void cleanup() throws Exception { @Test public void decodeWhenMessageReadScopeThenSuccess() { - NimbusJwkReactiveJwtDecoder decoder = new NimbusJwkReactiveJwtDecoder(this.server.url("/certs").toString()); - - Jwt jwt = decoder.decode(this.messageReadToken).block(); + Jwt jwt = this.decoder.decode(this.messageReadToken).block(); assertThat(jwt.getClaims().get("scope")).isEqualTo("message:read"); } + @Test + public void decodeWhenIssuedAtThenSuccess() { + String withIssuedAt = "eyJraWQiOiJrZXktaWQtMSIsImFsZyI6IlJTMjU2In0.eyJzY29wZSI6IiIsImV4cCI6OTIyMzM3MjAwNjA5NjM3NSwiaWF0IjoxNTI5OTQyNDQ4fQ.LBzAJO-FR-uJDHST61oX4kimuQjz6QMJPW_mvEXRB6A-fMQWpfTQ089eboipAqsb33XnwWth9ELju9HMWLk0FjlWVVzwObh9FcoKelmPNR8mZIlFG-pAYGgSwi8HufyLabXHntFavBiFtqwp_z9clSOFK1RxWvt3lywEbGgtCKve0BXOjfKWiH1qe4QKGixH-NFxidvz8Qd5WbJwyb9tChC6ZKoKPv7Jp-N5KpxkY-O2iUtINvn4xOSactUsvKHgF8ZzZjvJGzG57r606OZXaNtoElQzjAPU5xDGg5liuEJzfBhvqiWCLRmSuZ33qwp3aoBnFgEw0B85gsNe3ggABg"; + + Jwt jwt = this.decoder.decode(withIssuedAt).block(); + + assertThat(jwt.getClaims().get(JwtClaimNames.IAT)).isEqualTo(new Date(1529942448000L)); + } + @Test public void decodeWhenExpiredThenFail() { assertThatCode(() -> this.decoder.decode(this.expired).block()) .isInstanceOf(JwtException.class); } + @Test + public void decodeWhenNoPeriodThenFail() { + assertThatCode(() -> this.decoder.decode("").block()) + .isInstanceOf(JwtException.class); + } + + @Test + public void decodeWhenInvalidJwkSetUrlThenFail() { + this.decoder = new NimbusJwkReactiveJwtDecoder("http://localhost:1280/certs"); + assertThatCode(() -> this.decoder.decode(this.messageReadToken).block()) + .isInstanceOf(JwtException.class); + } + @Test public void decodeWhenInvalidSignatureThenFail() { assertThatCode(() -> this.decoder.decode(this.messageReadToken.substring(0, this.messageReadToken.length() - 2)).block()) @@ -102,4 +113,10 @@ public void decodeWhenAlgNoneThenFail() { .isInstanceOf(JwtException.class) .hasMessage("Unsupported algorithm of none"); } + + @Test + public void decodeWhenInvalidAlgMismatchThenFail() { + assertThatCode(() -> this.decoder.decode("ew0KICAiYWxnIjogIkVTMjU2IiwNCiAgInR5cCI6ICJKV1QiDQp9.ew0KICAic3ViIjogIjEyMzQ1Njc4OTAiLA0KICAibmFtZSI6ICJKb2huIERvZSIsDQogICJpYXQiOiAxNTE2MjM5MDIyDQp9.").block()) + .isInstanceOf(JwtException.class); + } } From dd93d79d98417da81b5065dcad907532fef45c6f Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 25 Jun 2018 11:23:50 -0400 Subject: [PATCH 085/226] OAuth2ClientWebMvcSecurityConfiguration handles multiple OAuth2AuthorizedClientService @Bean Fixes gh-5321 --- .../OAuth2ClientConfiguration.java | 8 +++- .../OAuth2ClientConfigurationTests.java | 40 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java index 86cfdc65f2f..28ef9426096 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java @@ -57,7 +57,6 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { @Configuration static class OAuth2ClientWebMvcSecurityConfiguration implements WebMvcConfigurer { - @Autowired(required = false) private OAuth2AuthorizedClientService authorizedClientService; @Override @@ -68,5 +67,12 @@ public void addArgumentResolvers(List argumentRes argumentResolvers.add(authorizedClientArgumentResolver); } } + + @Autowired(required = false) + public void setAuthorizedClientService(List authorizedClientServices) { + if (authorizedClientServices.size() == 1) { + this.authorizedClientService = authorizedClientServices.get(0); + } + } } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java index e2f84b2d86b..c5482245491 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java @@ -17,6 +17,7 @@ import org.junit.Rule; import org.junit.Test; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -24,12 +25,14 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.test.web.servlet.MockMvc; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; @@ -92,4 +95,41 @@ public OAuth2AuthorizedClientService authorizedClientService() { return AUTHORIZED_CLIENT_SERVICE; } } + + // gh-5321 + @Test + public void loadContextWhenOAuth2AuthorizedClientServiceRegisteredTwiceThenThrowNoUniqueBeanDefinitionException() { + assertThatThrownBy(() -> this.spring.register(OAuth2AuthorizedClientServiceRegisteredTwiceConfig.class).autowire()) + .hasRootCauseInstanceOf(NoUniqueBeanDefinitionException.class) + .hasMessageContaining("Only one matching @Bean of type " + OAuth2AuthorizedClientService.class.getName() + " should be registered."); + } + + @EnableWebMvc + @EnableWebSecurity + static class OAuth2AuthorizedClientServiceRegisteredTwiceConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2Login(); + } + + @Bean + public ClientRegistrationRepository clientRegistrationRepository() { + return mock(ClientRegistrationRepository.class); + } + + @Bean + public OAuth2AuthorizedClientService authorizedClientService1() { + return mock(OAuth2AuthorizedClientService.class); + } + + @Bean + public OAuth2AuthorizedClientService authorizedClientService2() { + return mock(OAuth2AuthorizedClientService.class); + } + } } From a1f869121bc5aa6787ced2b4bd5feea02bbe63e3 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 25 Jun 2018 11:50:04 -0400 Subject: [PATCH 086/226] Improve message for NoUniqueBeanDefinitionException in OAuth2ClientConfigurerUtils --- .../client/OAuth2ClientConfigurerUtils.java | 16 ++++- .../OAuth2ClientConfigurationTests.java | 68 ++++++++++++++++++- 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java index 646c32accfd..bb62ef24221 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java @@ -16,6 +16,7 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.client; import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; @@ -23,6 +24,7 @@ import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.util.StringUtils; import java.util.Map; @@ -47,7 +49,16 @@ static > ClientRegistrationRepository getClient } private static > ClientRegistrationRepository getClientRegistrationRepositoryBean(B builder) { - return builder.getSharedObject(ApplicationContext.class).getBean(ClientRegistrationRepository.class); + Map clientRegistrationRepositoryMap = BeanFactoryUtils.beansOfTypeIncludingAncestors( + builder.getSharedObject(ApplicationContext.class), ClientRegistrationRepository.class); + if (clientRegistrationRepositoryMap.isEmpty()) { + throw new NoSuchBeanDefinitionException(ClientRegistrationRepository.class); + } else if (clientRegistrationRepositoryMap.size() > 1) { + throw new NoUniqueBeanDefinitionException(ClientRegistrationRepository.class, clientRegistrationRepositoryMap.size(), + "Expected single matching bean of type '" + ClientRegistrationRepository.class.getName() + "' but found " + + clientRegistrationRepositoryMap.size() + ": " + StringUtils.collectionToCommaDelimitedString(clientRegistrationRepositoryMap.keySet())); + } + return clientRegistrationRepositoryMap.values().iterator().next(); } static > OAuth2AuthorizedClientService getAuthorizedClientService(B builder) { @@ -67,7 +78,8 @@ private static > OAuth2AuthorizedClientService builder.getSharedObject(ApplicationContext.class), OAuth2AuthorizedClientService.class); if (authorizedClientServiceMap.size() > 1) { throw new NoUniqueBeanDefinitionException(OAuth2AuthorizedClientService.class, authorizedClientServiceMap.size(), - "Only one matching @Bean of type " + OAuth2AuthorizedClientService.class.getName() + " should be registered."); + "Expected single matching bean of type '" + OAuth2AuthorizedClientService.class.getName() + "' but found " + + authorizedClientServiceMap.size() + ": " + StringUtils.collectionToCommaDelimitedString(authorizedClientServiceMap.keySet())); } return (!authorizedClientServiceMap.isEmpty() ? authorizedClientServiceMap.values().iterator().next() : null); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java index c5482245491..86a73476945 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java @@ -17,6 +17,7 @@ import org.junit.Rule; import org.junit.Test; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -101,7 +102,8 @@ public OAuth2AuthorizedClientService authorizedClientService() { public void loadContextWhenOAuth2AuthorizedClientServiceRegisteredTwiceThenThrowNoUniqueBeanDefinitionException() { assertThatThrownBy(() -> this.spring.register(OAuth2AuthorizedClientServiceRegisteredTwiceConfig.class).autowire()) .hasRootCauseInstanceOf(NoUniqueBeanDefinitionException.class) - .hasMessageContaining("Only one matching @Bean of type " + OAuth2AuthorizedClientService.class.getName() + " should be registered."); + .hasMessageContaining("Expected single matching bean of type '" + OAuth2AuthorizedClientService.class.getName() + + "' but found 2: authorizedClientService1,authorizedClientService2"); } @EnableWebMvc @@ -110,11 +112,13 @@ static class OAuth2AuthorizedClientServiceRegisteredTwiceConfig extends WebSecur @Override protected void configure(HttpSecurity http) throws Exception { + // @formatter:off http .authorizeRequests() .anyRequest().authenticated() .and() .oauth2Login(); + // @formatter:on } @Bean @@ -132,4 +136,66 @@ public OAuth2AuthorizedClientService authorizedClientService2() { return mock(OAuth2AuthorizedClientService.class); } } + + @Test + public void loadContextWhenClientRegistrationRepositoryNotRegisteredThenThrowNoSuchBeanDefinitionException() { + assertThatThrownBy(() -> this.spring.register(ClientRegistrationRepositoryNotRegisteredConfig.class).autowire()) + .hasRootCauseInstanceOf(NoSuchBeanDefinitionException.class) + .hasMessageContaining("No qualifying bean of type '" + ClientRegistrationRepository.class.getName() + "' available"); + } + + @EnableWebMvc + @EnableWebSecurity + static class ClientRegistrationRepositoryNotRegisteredConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2Login(); + // @formatter:on + } + } + + @Test + public void loadContextWhenClientRegistrationRepositoryRegisteredTwiceThenThrowNoUniqueBeanDefinitionException() { + assertThatThrownBy(() -> this.spring.register(ClientRegistrationRepositoryRegisteredTwiceConfig.class).autowire()) + .hasRootCauseInstanceOf(NoUniqueBeanDefinitionException.class) + .hasMessageContaining("Expected single matching bean of type '" + ClientRegistrationRepository.class.getName() + + "' but found 2: clientRegistrationRepository1,clientRegistrationRepository2"); + } + + @EnableWebMvc + @EnableWebSecurity + static class ClientRegistrationRepositoryRegisteredTwiceConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2Login(); + // @formatter:on + } + + @Bean + public ClientRegistrationRepository clientRegistrationRepository1() { + return mock(ClientRegistrationRepository.class); + } + + @Bean + public ClientRegistrationRepository clientRegistrationRepository2() { + return mock(ClientRegistrationRepository.class); + } + + @Bean + public OAuth2AuthorizedClientService authorizedClientService() { + return mock(OAuth2AuthorizedClientService.class); + } + } } From 097b04d564ea8c4b3656d8b7703f531d96bf03eb Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 25 Jun 2018 16:44:04 -0400 Subject: [PATCH 087/226] Validate sub claim in UserInfo Response Fixes gh-5447 --- .../client/oidc/userinfo/OidcUserService.java | 17 +++++++--- .../oidc/userinfo/OidcUserServiceTests.java | 31 +++++++++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java index aaae86454a7..ae3c6505a0a 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java @@ -15,10 +15,6 @@ */ package org.springframework.security.oauth2.client.oidc.userinfo; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; @@ -36,6 +32,10 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + /** * An implementation of an {@link OAuth2UserService} that supports OpenID Connect 1.0 Provider's. * @@ -62,7 +62,14 @@ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2Authenticatio userInfo = new OidcUserInfo(oauth2User.getAttributes()); // http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse - // Due to the possibility of token substitution attacks (see Section 16.11), + + // 1) The sub (subject) Claim MUST always be returned in the UserInfo Response + if (userInfo.getSubject() == null) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + + // 2) Due to the possibility of token substitution attacks (see Section 16.11), // the UserInfo Response is not guaranteed to be about the End-User // identified by the sub (subject) element of the ID Token. // The sub Claim in the UserInfo Response MUST be verified to exactly match diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java index 7ac59eb954f..6164022292e 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java @@ -168,6 +168,37 @@ public void loadUserWhenUserInfoSuccessResponseThenReturnUser() throws Exception assertThat(userAuthority.getUserInfo()).isEqualTo(user.getUserInfo()); } + // gh-5447 + @Test + public void loadUserWhenUserInfoSuccessResponseAndUserInfoSubjectIsNullThenThrowOAuth2AuthenticationException() throws Exception { + this.exception.expect(OAuth2AuthenticationException.class); + this.exception.expectMessage(containsString("invalid_user_info_response")); + + MockWebServer server = new MockWebServer(); + + String userInfoResponse = "{\n" + + " \"email\": \"full_name@provider.com\",\n" + + " \"name\": \"full name\"\n" + + "}\n"; + server.enqueue(new MockResponse() + .setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .setBody(userInfoResponse)); + + server.start(); + + String userInfoUri = server.url("/user").toString(); + + when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn(StandardClaimNames.EMAIL); + when(this.accessToken.getTokenValue()).thenReturn("access-token"); + + try { + this.userService.loadUser(new OidcUserRequest(this.clientRegistration, this.accessToken, this.idToken)); + } finally { + server.shutdown(); + } + } + @Test public void loadUserWhenUserInfoSuccessResponseAndUserInfoSubjectNotSameAsIdTokenSubjectThenThrowOAuth2AuthenticationException() throws Exception { this.exception.expect(OAuth2AuthenticationException.class); From 8152fe33344558e75babd3687e093bb1d4968ffc Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 25 Jun 2018 20:42:42 -0500 Subject: [PATCH 088/226] Add NimbusReactiveJwtDecoder RSAPublicKey Support Fixes: gh-5460 --- .../OidcReactiveAuthenticationManager.java | 4 +- ...der.java => NimbusReactiveJwtDecoder.java} | 40 +++++++++++----- .../oauth2/jwt/ReactiveJWKSource.java | 32 +++++++++++++ .../oauth2/jwt/ReactiveJWKSourceAdapter.java | 47 +++++++++++++++++++ .../oauth2/jwt/ReactiveRemoteJWKSource.java | 4 +- ...ava => NimbusReactiveJwtDecoderTests.java} | 24 ++++++++-- 6 files changed, 130 insertions(+), 21 deletions(-) rename oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/{NimbusJwkReactiveJwtDecoder.java => NimbusReactiveJwtDecoder.java} (81%) create mode 100644 oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJWKSource.java create mode 100644 oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJWKSourceAdapter.java rename oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/{NimbusJwkReactiveJwtDecoderTests.java => NimbusReactiveJwtDecoderTests.java} (75%) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManager.java index dfb30ca3c0f..c7cd9f89597 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManager.java @@ -38,7 +38,7 @@ import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.security.oauth2.jwt.NimbusJwkReactiveJwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -220,7 +220,7 @@ public ReactiveJwtDecoder apply(ClientRegistration clientRegistration) { ); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } - jwtDecoder = new NimbusJwkReactiveJwtDecoder(clientRegistration.getProviderDetails().getJwkSetUri()); + jwtDecoder = new NimbusReactiveJwtDecoder(clientRegistration.getProviderDetails().getJwkSetUri()); this.jwtDecoders.put(clientRegistration.getRegistrationId(), jwtDecoder); } return jwtDecoder; diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java similarity index 81% rename from oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoder.java rename to oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java index 7a16daf1882..be428ab0e3a 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoder.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java @@ -19,6 +19,9 @@ import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.BadJOSEException; import com.nimbusds.jose.proc.JWSKeySelector; @@ -33,6 +36,7 @@ import org.springframework.util.Assert; import reactor.core.publisher.Mono; +import java.security.interfaces.RSAPublicKey; import java.time.Instant; import java.util.LinkedHashMap; import java.util.List; @@ -55,32 +59,37 @@ * @see JSON Web Key (JWK) * @see Nimbus JOSE + JWT SDK */ -public final class NimbusJwkReactiveJwtDecoder implements ReactiveJwtDecoder { +public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { private final JWTProcessor jwtProcessor; - private final ReactiveRemoteJWKSource reactiveJwkSource; + private final ReactiveJWKSource reactiveJwkSource; private final JWKSelectorFactory jwkSelectorFactory; - /** - * Constructs a {@code NimbusJwtDecoderJwkSupport} using the provided parameters. - * - * @param jwkSetUrl the JSON Web Key (JWK) Set {@code URL} - */ - public NimbusJwkReactiveJwtDecoder(String jwkSetUrl) { - this(jwkSetUrl, JwsAlgorithms.RS256); + public NimbusReactiveJwtDecoder(RSAPublicKey publicKey) { + JWSAlgorithm algorithm = JWSAlgorithm.parse(JwsAlgorithms.RS256); + + RSAKey rsaKey = rsaKey(publicKey); + JWKSet jwkSet = new JWKSet(rsaKey); + JWKSource jwkSource = new ImmutableJWKSet<>(jwkSet); + JWSKeySelector jwsKeySelector = + new JWSVerificationKeySelector<>(algorithm, jwkSource); + DefaultJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); + jwtProcessor.setJWSKeySelector(jwsKeySelector); + + this.jwtProcessor = jwtProcessor; + this.reactiveJwkSource = new ReactiveJWKSourceAdapter(jwkSource); + this.jwkSelectorFactory = new JWKSelectorFactory(algorithm); } /** * Constructs a {@code NimbusJwtDecoderJwkSupport} using the provided parameters. * * @param jwkSetUrl the JSON Web Key (JWK) Set {@code URL} - * @param jwsAlgorithm the JSON Web Algorithm (JWA) used for verifying the digital signatures */ - public NimbusJwkReactiveJwtDecoder(String jwkSetUrl, String jwsAlgorithm) { + public NimbusReactiveJwtDecoder(String jwkSetUrl) { Assert.hasText(jwkSetUrl, "jwkSetUrl cannot be empty"); - Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty"); - + String jwsAlgorithm = JwsAlgorithms.RS256; JWSAlgorithm algorithm = JWSAlgorithm.parse(jwsAlgorithm); JWKSource jwkSource = new JWKContextJWKSource(); JWSKeySelector jwsKeySelector = @@ -152,4 +161,9 @@ private Jwt createJwt(JWT parsedJwt, JWTClaimsSet jwtClaimsSet) { return new Jwt(parsedJwt.getParsedString(), issuedAt, expiresAt, headers, jwtClaimsSet.getClaims()); } + + private static RSAKey rsaKey(RSAPublicKey publicKey) { + return new RSAKey.Builder(publicKey) + .build(); + } } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJWKSource.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJWKSource.java new file mode 100644 index 00000000000..32b84b88bc9 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJWKSource.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.jwt; + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSelector; +import reactor.core.publisher.Mono; + +import java.util.List; + +/** + * A reactive version of {@link com.nimbusds.jose.jwk.source.JWKSource} + * @author Rob Winch + * @since 5.1 + */ +interface ReactiveJWKSource { + Mono> get(JWKSelector jwkSelector); +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJWKSourceAdapter.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJWKSourceAdapter.java new file mode 100644 index 00000000000..309ae578cd2 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJWKSourceAdapter.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.jwt; + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import reactor.core.publisher.Mono; + +import java.util.List; + +/** + * Adapts a {@link JWKSource} to a {@link ReactiveJWKSource} which must be non-blocking. + * @author Rob Winch + * @since 5.1 + */ +class ReactiveJWKSourceAdapter implements ReactiveJWKSource { + private final JWKSource source; + + /** + * Creates a new instance + * @param source + */ + ReactiveJWKSourceAdapter(JWKSource source) { + this.source = source; + } + + @Override + public Mono> get(JWKSelector jwkSelector) { + return Mono.fromCallable(() -> this.source.get(jwkSelector, null)); + } +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSource.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSource.java index c26a3a9f4b1..0079c8b8634 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSource.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSource.java @@ -34,7 +34,7 @@ * @author Rob Winch * @since 5.1 */ -class ReactiveRemoteJWKSource { +class ReactiveRemoteJWKSource implements ReactiveJWKSource { /** * The cached JWK set. */ @@ -48,7 +48,7 @@ class ReactiveRemoteJWKSource { this.jwkSetURL = jwkSetURL; } - Mono> get(JWKSelector jwkSelector) { + public Mono> get(JWKSelector jwkSelector) { return this.cachedJWKSet.get() .switchIfEmpty(getJWKSet()) .flatMap(jwkSet -> get(jwkSelector, jwkSet)) diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java similarity index 75% rename from oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoderTests.java rename to oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java index ef38f1917f5..3b93935c598 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoderTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java @@ -22,6 +22,10 @@ import org.junit.Before; import org.junit.Test; +import java.security.KeyFactory; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; import java.util.Date; import static org.assertj.core.api.Assertions.assertThat; @@ -31,7 +35,7 @@ * @author Rob Winch * @since 5.1 */ -public class NimbusJwkReactiveJwtDecoderTests { +public class NimbusReactiveJwtDecoderTests { private String expired = "eyJraWQiOiJrZXktaWQtMSIsImFsZyI6IlJTMjU2In0.eyJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6MTUyOTkzNzYzMX0.Dt5jFOKkB8zAmjciwvlGkj4LNStXWH0HNIfr8YYajIthBIpVgY5Hg_JL8GBmUFzKDgyusT0q60OOg8_Pdi4Lu-VTWyYutLSlNUNayMlyBaVEWfyZJnh2_OwMZr1vRys6HF-o1qZldhwcfvczHg61LwPa1ISoqaAltDTzBu9cGISz2iBUCuR0x71QhbuRNyJdjsyS96NqiM_TspyiOSxmlNch2oAef1MssOQ23CrKilIvEDsz_zk5H94q7rH0giWGdEHCENESsTJS0zvzH6r2xIWjd5WnihFpCPkwznEayxaEhrdvJqT_ceyXCIfY4m3vujPQHNDG0UshpwvDuEbPUg"; @@ -51,14 +55,14 @@ public class NimbusJwkReactiveJwtDecoderTests { + "}"; private MockWebServer server; - private NimbusJwkReactiveJwtDecoder decoder; + private NimbusReactiveJwtDecoder decoder; @Before public void setup() throws Exception { this.server = new MockWebServer(); this.server.start(); this.server.enqueue(new MockResponse().setBody(jwkSet)); - this.decoder = new NimbusJwkReactiveJwtDecoder(this.server.url("/certs").toString()); + this.decoder = new NimbusReactiveJwtDecoder(this.server.url("/certs").toString()); } @After @@ -73,6 +77,18 @@ public void decodeWhenMessageReadScopeThenSuccess() { assertThat(jwt.getClaims().get("scope")).isEqualTo("message:read"); } + @Test + public void decodeWhenRSAPublicKeyThenSuccess() throws Exception { + byte[] bytes = Base64.getDecoder().decode("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqL48v1clgFw+Evm145pmh8nRYiNt72Gupsshn7Qs8dxEydCRp1DPOV/PahPk1y2nvldBNIhfNL13JOAiJ6BTiF+2ICuICAhDArLMnTH61oL1Hepq8W1xpa9gxsnL1P51thvfmiiT4RTW57koy4xIWmIp8ZXXfYgdH2uHJ9R0CQBuYKe7nEOObjxCFWC8S30huOfW2cYtv0iB23h6w5z2fDLjddX6v/FXM7ktcokgpm3/XmvT/+bL6/GGwz9k6kJOyMTubecr+WT//le8ikY66zlplYXRQh6roFfFCL21Pt8xN5zrk+0AMZUnmi8F2S2ztSBmAVJ7H71ELXsURBVZpwIDAQAB"); + RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") + .generatePublic(new X509EncodedKeySpec(bytes)); + this.decoder = new NimbusReactiveJwtDecoder(publicKey); + String noKeyId = "eyJhbGciOiJSUzI1NiJ9.eyJzY29wZSI6IiIsImV4cCI6OTIyMzM3MjAwNjA5NjM3NX0.hNVuHSUkxdLZrDfqdmKcOi0ggmNaDuB4ZPxPtJl1gwBiXzIGN6Hwl24O2BfBZiHFKUTQDs4_RvzD71mEG3DvUrcKmdYWqIB1l8KNmxQLUDG-cAPIpJmRJgCh50tf8OhOE_Cb9E1HcsOUb47kT9iz-VayNBcmo6BmyZLdEGhsdGBrc3Mkz2dd_0PF38I2Hf_cuSjn9gBjFGtiPEXJvob3PEjVTSx_zvodT8D9p3An1R3YBZf5JSd1cQisrXgDX2k1Jmf7UKKWzgfyCgnEtRWWbsUdPqo3rSEY9GDC1iSQXsFTTC1FT_JJDkwzGf011fsU5O_Ko28TARibmKTCxAKNRQ"; + + assertThatCode(() -> this.decoder.decode(noKeyId).block()) + .doesNotThrowAnyException(); + } + @Test public void decodeWhenIssuedAtThenSuccess() { String withIssuedAt = "eyJraWQiOiJrZXktaWQtMSIsImFsZyI6IlJTMjU2In0.eyJzY29wZSI6IiIsImV4cCI6OTIyMzM3MjAwNjA5NjM3NSwiaWF0IjoxNTI5OTQyNDQ4fQ.LBzAJO-FR-uJDHST61oX4kimuQjz6QMJPW_mvEXRB6A-fMQWpfTQ089eboipAqsb33XnwWth9ELju9HMWLk0FjlWVVzwObh9FcoKelmPNR8mZIlFG-pAYGgSwi8HufyLabXHntFavBiFtqwp_z9clSOFK1RxWvt3lywEbGgtCKve0BXOjfKWiH1qe4QKGixH-NFxidvz8Qd5WbJwyb9tChC6ZKoKPv7Jp-N5KpxkY-O2iUtINvn4xOSactUsvKHgF8ZzZjvJGzG57r606OZXaNtoElQzjAPU5xDGg5liuEJzfBhvqiWCLRmSuZ33qwp3aoBnFgEw0B85gsNe3ggABg"; @@ -96,7 +112,7 @@ public void decodeWhenNoPeriodThenFail() { @Test public void decodeWhenInvalidJwkSetUrlThenFail() { - this.decoder = new NimbusJwkReactiveJwtDecoder("http://localhost:1280/certs"); + this.decoder = new NimbusReactiveJwtDecoder("http://localhost:1280/certs"); assertThatCode(() -> this.decoder.decode(this.messageReadToken).block()) .isInstanceOf(JwtException.class); } From 7dd721ef1cb4e0dcc05d783691f6e39fac501a98 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 28 Jun 2018 11:31:21 -0600 Subject: [PATCH 089/226] Rename createJwkSet method typo Actually, it is creating a claims set, just a typo. Issue: gh-5330 --- .../security/oauth2/jwt/NimbusReactiveJwtDecoder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java index be428ab0e3a..4b439f57f5f 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java @@ -127,7 +127,7 @@ private Mono decode(SignedJWT parsedToken) { JWKSelector selector = this.jwkSelectorFactory .createSelector(parsedToken.getHeader()); return this.reactiveJwkSource.get(selector) - .map(jwkList -> createJwkSet(parsedToken, jwkList)) + .map(jwkList -> createClaimsSet(parsedToken, jwkList)) .map(set -> createJwt(parsedToken, set)) .onErrorMap(e -> new JwtException("An error occurred while attempting to decode the Jwt: ", e)); } catch (RuntimeException ex) { @@ -135,7 +135,7 @@ private Mono decode(SignedJWT parsedToken) { } } - private JWTClaimsSet createJwkSet(JWT parsedToken, List jwkList) { + private JWTClaimsSet createClaimsSet(JWT parsedToken, List jwkList) { try { return this.jwtProcessor.process(parsedToken, new JWKContext(jwkList)); } From 6f1d1a2382f48f5aeaa7052b7ae8152950ecc9c4 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 2 Jul 2018 10:39:24 -0600 Subject: [PATCH 090/226] InterceptUrlConfigTests groovy->java Issue: gh-4939 --- .../http/InterceptUrlConfigTests.groovy | 400 ------------------ .../config/http/InterceptUrlConfigTests.java | 293 +++++++++++++ ...ptUrlConfigTests-AntMatcherServletPath.xml | 33 ++ ...tUrlConfigTests-CamelCasePathVariables.xml | 36 ++ ...lConfigTests-CiRegexMatcherServletPath.xml | 33 ++ ...lConfigTests-DefaultMatcherServletPath.xml | 33 ++ .../InterceptUrlConfigTests-HasAnyRole.xml | 35 ++ .../InterceptUrlConfigTests-MvcMatchers.xml | 40 ++ ...rlConfigTests-MvcMatchersPathVariables.xml | 41 ++ ...tUrlConfigTests-MvcMatchersServletPath.xml | 40 ++ .../InterceptUrlConfigTests-PatchMethod.xml | 36 ++ .../InterceptUrlConfigTests-PathVariables.xml | 36 ++ ...UrlConfigTests-RegexMatcherServletPath.xml | 33 ++ .../http/InterceptUrlConfigTests-Sec2256.xml | 37 ++ ...onfigTests-TypeConversionPathVariables.xml | 37 ++ .../security/config/http/userservice.xml | 1 + 16 files changed, 764 insertions(+), 400 deletions(-) delete mode 100644 config/src/test/groovy/org/springframework/security/config/http/InterceptUrlConfigTests.groovy create mode 100644 config/src/test/java/org/springframework/security/config/http/InterceptUrlConfigTests.java create mode 100644 config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-AntMatcherServletPath.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-CamelCasePathVariables.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-CiRegexMatcherServletPath.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-DefaultMatcherServletPath.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-HasAnyRole.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchers.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersPathVariables.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersServletPath.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-PatchMethod.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-PathVariables.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-RegexMatcherServletPath.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-Sec2256.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-TypeConversionPathVariables.xml diff --git a/config/src/test/groovy/org/springframework/security/config/http/InterceptUrlConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/InterceptUrlConfigTests.groovy deleted file mode 100644 index c0f3d1fe09b..00000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/InterceptUrlConfigTests.groovy +++ /dev/null @@ -1,400 +0,0 @@ -/* - * Copyright 2002-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.config.http - -import javax.servlet.ServletContext -import javax.servlet.ServletRegistration -import javax.servlet.http.HttpServletResponse - -import org.mockito.invocation.InvocationOnMock -import org.mockito.stubbing.Answer - -import org.springframework.beans.factory.parsing.BeanDefinitionParsingException -import org.springframework.mock.web.MockFilterChain -import org.springframework.mock.web.MockHttpServletRequest -import org.springframework.mock.web.MockHttpServletResponse -import org.springframework.mock.web.MockServletContext -import org.springframework.security.access.SecurityConfig -import org.springframework.security.web.access.intercept.FilterSecurityInterceptor -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController - -import static org.mockito.Mockito.* -/** - * - * @author Rob Winch - */ -class InterceptUrlConfigTests extends AbstractHttpConfigTests { - - def "SEC-2256: intercept-url method is not given priority"() { - when: - httpAutoConfig { - 'intercept-url'(pattern: '/anyurl', access: "ROLE_USER") - 'intercept-url'(pattern: '/anyurl', 'method':'GET',access: 'ROLE_ADMIN') - } - createAppContext() - - def fids = getFilter(FilterSecurityInterceptor).securityMetadataSource - def attrs = fids.getAttributes(createFilterinvocation("/anyurl", "GET")) - def attrsPost = fids.getAttributes(createFilterinvocation("/anyurl", "POST")) - - then: - attrs.size() == 1 - attrs.contains(new SecurityConfig("ROLE_USER")) - attrsPost.size() == 1 - attrsPost.contains(new SecurityConfig("ROLE_USER")) - } - - def "SEC-2355: intercept-url support patch"() { - setup: - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - xml.http('use-expressions':false) { - 'http-basic'() - 'intercept-url'(pattern: '/**', 'method':'PATCH',access: 'ROLE_ADMIN') - csrf(disabled:true) - } - createAppContext() - when: 'Method other than PATCH is used' - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_OK - when: 'Method of PATCH is used' - request = new MockHttpServletRequest(method:'PATCH') - response = new MockHttpServletResponse() - chain = new MockFilterChain() - springSecurityFilterChain.doFilter(request, response, chain) - then: 'The response is unauthorized' - response.status == HttpServletResponse.SC_UNAUTHORIZED - } - - def "intercept-url supports hasAnyRoles"() { - setup: - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - xml.http('use-expressions':true) { - 'http-basic'() - 'intercept-url'(pattern: '/**', access: "hasAnyRole('ROLE_DEVELOPER','ROLE_USER')") - csrf(disabled:true) - } - when: - createAppContext() - then: 'no error' - noExceptionThrown() - when: 'ROLE_USER can access' - login(request, 'user', 'password') - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_OK - when: 'ROLE_A cannot access' - request = new MockHttpServletRequest(method:'GET') - response = new MockHttpServletResponse() - chain = new MockFilterChain() - login(request, 'bob', 'bobspassword') - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is Forbidden' - response.status == HttpServletResponse.SC_FORBIDDEN - } - - def "SEC-2256: intercept-url supports path variables"() { - setup: - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - xml.http('use-expressions':true) { - 'http-basic'() - 'intercept-url'(pattern: '/user/{un}/**', access: "#un == authentication.name") - 'intercept-url'(pattern: '/**', access: "denyAll") - } - createAppContext() - login(request, 'user', 'password') - when: 'user can access' - request.servletPath = '/user/user/abc' - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_OK - when: 'user cannot access otheruser' - request = new MockHttpServletRequest(method:'GET', servletPath : '/user/otheruser/abc') - login(request, 'user', 'password') - response = new MockHttpServletResponse() - chain.reset() - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_FORBIDDEN - when: 'user can access case insensitive URL' - request = new MockHttpServletRequest(method:'GET', servletPath : '/USER/user/abc') - login(request, 'user', 'password') - response = new MockHttpServletResponse() - chain.reset() - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_FORBIDDEN - } - - def "gh-3786 intercept-url supports cammel case path variables"() { - setup: - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - xml.http('use-expressions':true) { - 'http-basic'() - 'intercept-url'(pattern: '/user/{userName}/**', access: "#userName == authentication.name") - 'intercept-url'(pattern: '/**', access: "denyAll") - } - createAppContext() - login(request, 'user', 'password') - when: 'user can access' - request.servletPath = '/user/user/abc' - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_OK - when: 'user cannot access otheruser' - request = new MockHttpServletRequest(method:'GET', servletPath : '/user/otheruser/abc') - login(request, 'user', 'password') - response = new MockHttpServletResponse() - chain.reset() - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_FORBIDDEN - when: 'user can access case insensitive URL' - request = new MockHttpServletRequest(method:'GET', servletPath : '/USER/user/abc') - login(request, 'user', 'password') - response = new MockHttpServletResponse() - chain.reset() - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_FORBIDDEN - } - - def "SEC-2256: intercept-url supports path variable type conversion"() { - setup: - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - xml.http('use-expressions':true) { - 'http-basic'() - 'intercept-url'(pattern: '/user/{un}/**', access: "@id.isOne(#un)") - 'intercept-url'(pattern: '/**', access: "denyAll") - } - bean('id', Id) - createAppContext() - login(request, 'user', 'password') - when: 'can access id == 1' - request.servletPath = '/user/1/abc' - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_OK - when: 'user cannot access 2' - request = new MockHttpServletRequest(method:'GET', servletPath : '/user/2/abc') - login(request, 'user', 'password') - chain.reset() - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_FORBIDDEN - } - - def "intercept-url supports mvc matchers"() { - setup: - MockServletContext servletContext = mockServletContext(); - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - xml.http('request-matcher':'mvc') { - 'http-basic'() - 'intercept-url'(pattern: '/path', access: "denyAll") - } - bean('pathController',PathController) - xml.'mvc:annotation-driven'() - - createWebAppContext(servletContext) - when: - request.servletPath = "/path" - springSecurityFilterChain.doFilter(request, response, chain) - then: - response.status == HttpServletResponse.SC_UNAUTHORIZED - when: - request = new MockHttpServletRequest(method:'GET') - response = new MockHttpServletResponse() - chain = new MockFilterChain() - request.servletPath = "/path.html" - springSecurityFilterChain.doFilter(request, response, chain) - then: - response.status == HttpServletResponse.SC_UNAUTHORIZED - when: - request = new MockHttpServletRequest(method:'GET') - response = new MockHttpServletResponse() - chain = new MockFilterChain() - request.servletPath = "/path/" - springSecurityFilterChain.doFilter(request, response, chain) - then: - response.status == HttpServletResponse.SC_UNAUTHORIZED - } - - def "intercept-url mvc supports path variables"() { - setup: - MockServletContext servletContext = mockServletContext(); - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - xml.http('request-matcher':'mvc') { - 'http-basic'() - 'intercept-url'(pattern: '/user/{un}/**', access: "#un == 'user'") - } - xml.'mvc:annotation-driven'() - createWebAppContext(servletContext) - when: 'user can access' - request.servletPath = '/user/user/abc' - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_OK - when: 'cannot access otheruser' - request = new MockHttpServletRequest(method:'GET', servletPath : '/user/otheruser/abc') - login(request, 'user', 'password') - chain.reset() - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_FORBIDDEN - when: 'user can access case insensitive URL' - request = new MockHttpServletRequest(method:'GET', servletPath : '/USER/user/abc') - login(request, 'user', 'password') - chain.reset() - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_FORBIDDEN - } - - def "intercept-url mvc matchers with servlet path"() { - setup: - MockServletContext servletContext = mockServletContext("/spring"); - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - xml.http('request-matcher':'mvc') { - 'http-basic'() - 'intercept-url'(pattern: '/path', access: "denyAll", 'servlet-path': "/spring") - } - bean('pathController',PathController) - xml.'mvc:annotation-driven'() - createWebAppContext(servletContext) - when: - request.servletPath = "/spring" - request.requestURI = "/spring/path" - springSecurityFilterChain.doFilter(request, response, chain) - then: - response.status == HttpServletResponse.SC_UNAUTHORIZED - when: - request = new MockHttpServletRequest(method:'GET') - response = new MockHttpServletResponse() - chain = new MockFilterChain() - request.servletPath = "/spring" - request.requestURI = "/spring/path.html" - springSecurityFilterChain.doFilter(request, response, chain) - then: - response.status == HttpServletResponse.SC_UNAUTHORIZED - when: - request = new MockHttpServletRequest(method:'GET') - response = new MockHttpServletResponse() - chain = new MockFilterChain() - request.servletPath = "/spring" - request.requestURI = "/spring/path/" - springSecurityFilterChain.doFilter(request, response, chain) - then: - response.status == HttpServletResponse.SC_UNAUTHORIZED - } - - def "intercept-url ant matcher with servlet path fails"() { - when: - xml.http('request-matcher':'ant') { - 'http-basic'() - 'intercept-url'(pattern: '/path', access: "denyAll", 'servlet-path': "/spring") - } - createAppContext() - then: - thrown(BeanDefinitionParsingException) - } - - def "intercept-url regex matcher with servlet path fails"() { - when: - xml.http('request-matcher':'regex') { - 'http-basic'() - 'intercept-url'(pattern: '/path', access: "denyAll", 'servlet-path': "/spring") - } - createAppContext() - then: - thrown(BeanDefinitionParsingException) - } - - def "intercept-url ciRegex matcher with servlet path fails"() { - when: - xml.http('request-matcher':'ciRegex') { - 'http-basic'() - 'intercept-url'(pattern: '/path', access: "denyAll", 'servlet-path': "/spring") - } - createAppContext() - then: - thrown(BeanDefinitionParsingException) - } - - def "intercept-url default matcher with servlet path fails"() { - when: - xml.http() { - 'http-basic'() - 'intercept-url'(pattern: '/path', access: "denyAll", 'servlet-path': "/spring") - } - createAppContext() - then: - thrown(BeanDefinitionParsingException) - } - - public static class Id { - public boolean isOne(int i) { - return i == 1; - } - } - - private ServletContext mockServletContext() { - return mockServletContext("/"); - } - - private ServletContext mockServletContext(String servletPath) { - MockServletContext servletContext = spy(new MockServletContext()); - final ServletRegistration registration = mock(ServletRegistration.class); - when(registration.getMappings()).thenReturn(Collections.singleton(servletPath)); - Answer> answer = new Answer>() { - @Override - public Map answer(InvocationOnMock invocation) throws Throwable { - return Collections.singletonMap("spring", registration); - } - }; - when(servletContext.getServletRegistrations()).thenAnswer(answer); - return servletContext; - } - - def login(MockHttpServletRequest request, String username, String password) { - String toEncode = username + ':' + password - request.addHeader('Authorization','Basic ' + Base64.encoder.encodeToString(toEncode.getBytes('UTF-8'))) - } - - @RestController - static class PathController { - @RequestMapping("/path") - public String path() { - return "path"; - } - } -} diff --git a/config/src/test/java/org/springframework/security/config/http/InterceptUrlConfigTests.java b/config/src/test/java/org/springframework/security/config/http/InterceptUrlConfigTests.java new file mode 100644 index 00000000000..2de3e5912dd --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/InterceptUrlConfigTests.java @@ -0,0 +1,293 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.http; + +import org.junit.Rule; +import org.junit.Test; +import org.mockito.stubbing.Answer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.mock.web.MockServletContext; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.ConfigurableWebApplicationContext; + +import javax.servlet.ServletRegistration; +import java.util.Collections; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * + * @author Rob Winch + * @author Josh Cummings + */ +public class InterceptUrlConfigTests { + + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/InterceptUrlConfigTests"; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + MockMvc mvc; + + /** + * sec-2256 + */ + @Test + public void requestWhenMethodIsSpecifiedThenItIsNotGivenPriority() + throws Exception { + + this.spring.configLocations(this.xml("Sec2256")).autowire(); + + this.mvc.perform(post("/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(get("/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + } + + /** + * sec-2355 + */ + @Test + public void requestWhenUsingPatchThenAuthorizesRequestsAccordingly() + throws Exception { + + this.spring.configLocations(this.xml("PatchMethod")).autowire(); + + this.mvc.perform(get("/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(patch("/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isForbidden()); + + this.mvc.perform(patch("/path") + .with(httpBasic("admin", "password"))) + .andExpect(status().isOk()); + + } + + @Test + public void requestWhenUsingHasAnyRoleThenAuthorizesRequestsAccordingly() + throws Exception { + + this.spring.configLocations(this.xml("HasAnyRole")).autowire(); + + this.mvc.perform(get("/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(get("/path") + .with(httpBasic("admin", "password"))) + .andExpect(status().isForbidden()); + } + + /** + * sec-2059 + */ + @Test + public void requestWhenUsingPathVariablesThenAuthorizesRequestsAccordingly() + throws Exception { + + this.spring.configLocations(this.xml("PathVariables")).autowire(); + + this.mvc.perform(get("/path/user/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(get("/path/otheruser/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isForbidden()); + + this.mvc.perform(get("/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isForbidden()); + } + + /** + * gh-3786 + */ + @Test + public void requestWhenUsingCamelCasePathVariablesThenAuthorizesRequestsAccordingly() + throws Exception { + + this.spring.configLocations(this.xml("CamelCasePathVariables")).autowire(); + + this.mvc.perform(get("/path/user/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(get("/path/otheruser/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isForbidden()); + + this.mvc.perform(get("/PATH/user/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isForbidden()); + } + + /** + * sec-2059 + */ + @Test + public void requestWhenUsingPathVariablesAndTypeConversionThenAuthorizesRequestsAccordingly() + throws Exception { + + this.spring.configLocations(this.xml("TypeConversionPathVariables")).autowire(); + + this.mvc.perform(get("/path/1/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(get("/path/2/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isForbidden()); + + } + + @Test + public void requestWhenUsingMvcMatchersThenAuthorizesRequestsAccordingly() + throws Exception { + + this.spring.configLocations(this.xml("MvcMatchers")).autowire(); + + this.mvc.perform(get("/path")) + .andExpect(status().isUnauthorized()); + + this.mvc.perform(get("/path.html")) + .andExpect(status().isUnauthorized()); + + this.mvc.perform(get("/path/")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void requestWhenUsingMvcMatchersAndPathVariablesThenAuthorizesRequestsAccordingly() + throws Exception { + + this.spring.configLocations(this.xml("MvcMatchersPathVariables")).autowire(); + + this.mvc.perform(get("/path/user/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(get("/path/otheruser/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isForbidden()); + + this.mvc.perform(get("/PATH/user/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isForbidden()); + } + + @Test + public void requestWhenUsingMvcMatchersAndServletPathThenAuthorizesRequestsAccordingly() + throws Exception { + + this.spring.configLocations(this.xml("MvcMatchersServletPath")).autowire(); + + MockServletContext servletContext = mockServletContext("/spring"); + ConfigurableWebApplicationContext context = + (ConfigurableWebApplicationContext) this.spring.getContext(); + context.setServletContext(servletContext); + + this.mvc.perform(get("/spring/path").servletPath("/spring")) + .andExpect(status().isUnauthorized()); + + this.mvc.perform(get("/spring/path.html").servletPath("/spring")) + .andExpect(status().isUnauthorized()); + + this.mvc.perform(get("/spring/path/").servletPath("/spring")) + .andExpect(status().isUnauthorized()); + + } + + @Test + public void configureWhenUsingAntMatcherAndServletPathThenThrowsException() { + assertThatCode(() -> this.spring.configLocations(this.xml("AntMatcherServletPath")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class); + } + + @Test + public void configureWhenUsingRegexMatcherAndServletPathThenThrowsException() { + assertThatCode(() -> this.spring.configLocations(this.xml("RegexMatcherServletPath")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class); + } + + @Test + public void configureWhenUsingCiRegexMatcherAndServletPathThenThrowsException() { + assertThatCode(() -> this.spring.configLocations(this.xml("CiRegexMatcherServletPath")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class); + } + + @Test + public void configureWhenUsingDefaultMatcherAndServletPathThenThrowsException() { + assertThatCode(() -> this.spring.configLocations(this.xml("DefaultMatcherServletPath")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class); + } + + @RestController + static class PathController { + @RequestMapping("/path") + public String path() { + return "path"; + } + + @RequestMapping("/path/{un}/path") + public String path(@PathVariable("un") String name) { + return name; + } + } + + public static class Id { + public boolean isOne(int i) { + return i == 1; + } + } + + private MockServletContext mockServletContext(String servletPath) { + MockServletContext servletContext = spy(new MockServletContext()); + final ServletRegistration registration = mock(ServletRegistration.class); + when(registration.getMappings()).thenReturn(Collections.singleton(servletPath)); + Answer> answer = invocation -> + Collections.singletonMap("spring", registration); + when(servletContext.getServletRegistrations()).thenAnswer(answer); + return servletContext; + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-AntMatcherServletPath.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-AntMatcherServletPath.xml new file mode 100644 index 00000000000..c1ddd45cf74 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-AntMatcherServletPath.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-CamelCasePathVariables.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-CamelCasePathVariables.xml new file mode 100644 index 00000000000..091676e458d --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-CamelCasePathVariables.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-CiRegexMatcherServletPath.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-CiRegexMatcherServletPath.xml new file mode 100644 index 00000000000..cb8f4fd6e30 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-CiRegexMatcherServletPath.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-DefaultMatcherServletPath.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-DefaultMatcherServletPath.xml new file mode 100644 index 00000000000..ddf3ebc630f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-DefaultMatcherServletPath.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-HasAnyRole.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-HasAnyRole.xml new file mode 100644 index 00000000000..68e6fe8ebaa --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-HasAnyRole.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchers.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchers.xml new file mode 100644 index 00000000000..041e070b9f1 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchers.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersPathVariables.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersPathVariables.xml new file mode 100644 index 00000000000..c7ffba5d9b9 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersPathVariables.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersServletPath.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersServletPath.xml new file mode 100644 index 00000000000..365503eda6c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersServletPath.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-PatchMethod.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-PatchMethod.xml new file mode 100644 index 00000000000..4227abd0e89 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-PatchMethod.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-PathVariables.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-PathVariables.xml new file mode 100644 index 00000000000..f0ed68089b8 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-PathVariables.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-RegexMatcherServletPath.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-RegexMatcherServletPath.xml new file mode 100644 index 00000000000..9c242663f61 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-RegexMatcherServletPath.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-Sec2256.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-Sec2256.xml new file mode 100644 index 00000000000..756c38fbbaa --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-Sec2256.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-TypeConversionPathVariables.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-TypeConversionPathVariables.xml new file mode 100644 index 00000000000..8d613919749 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-TypeConversionPathVariables.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/userservice.xml b/config/src/test/resources/org/springframework/security/config/http/userservice.xml index 80314c3acc0..e9eeeceb968 100644 --- a/config/src/test/resources/org/springframework/security/config/http/userservice.xml +++ b/config/src/test/resources/org/springframework/security/config/http/userservice.xml @@ -25,5 +25,6 @@ http://www.springframework.org/schema/beans/spring-beans.xsd"> + From 2bdd799a7fde3a0511bb00c427d3e7de4f006623 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 28 Jun 2018 11:01:28 -0500 Subject: [PATCH 091/226] Add OAuth2AccessTokenResponse.withResponse Add ability to build a new OAuth2AccessTokenResponse from another OAuth2AccessTokenResponse. Fixes: gh-5474 --- .../endpoint/OAuth2AccessTokenResponse.java | 57 ++++++++++++++++--- .../OAuth2AccessTokenResponseTests.java | 51 +++++++++++++++++ 2 files changed, 101 insertions(+), 7 deletions(-) diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java index 83015ee58b2..29af0676318 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java @@ -21,6 +21,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +import java.time.Duration; import java.time.Instant; import java.util.Collections; import java.util.Map; @@ -81,6 +82,15 @@ public static Builder withToken(String tokenValue) { return new Builder(tokenValue); } + /** + * Returns a new {@link Builder}, initialized with the provided response + * @param response the response to intialize the builder with + * @return the {@link Builder} + */ + public static Builder withResponse(OAuth2AccessTokenResponse response) { + return new Builder(response); + } + /** * A builder for {@link OAuth2AccessTokenResponse}. */ @@ -92,6 +102,21 @@ public static class Builder { private String refreshToken; private Map additionalParameters; + private Instant issuedAt; + private Instant expiresAt; + + private Builder(OAuth2AccessTokenResponse response) { + OAuth2AccessToken accessToken = response.getAccessToken(); + this.tokenValue = accessToken.getTokenValue(); + this.tokenType = accessToken.getTokenType(); + this.expiresAt = accessToken.getExpiresAt(); + this.issuedAt = accessToken.getIssuedAt(); + this.scopes = accessToken.getScopes(); + this.refreshToken = response.getRefreshToken() == null ? + null : response.getRefreshToken().getTokenValue(); + this.additionalParameters = response.getAdditionalParameters(); + } + private Builder(String tokenValue) { this.tokenValue = tokenValue; } @@ -157,14 +182,9 @@ public Builder additionalParameters(Map additionalParameters) { * @return a {@link OAuth2AccessTokenResponse} */ public OAuth2AccessTokenResponse build() { - Instant issuedAt = Instant.now(); + Instant issuedAt = getIssuedAt(); - // expires_in is RECOMMENDED, as per spec https://tools.ietf.org/html/rfc6749#section-5.1 - // Therefore, expires_in may not be returned in the Access Token response which would result in the default value of 0. - // For these instances, default the expiresAt to +1 second from issuedAt time. - Instant expiresAt = this.expiresIn > 0 ? - issuedAt.plusSeconds(this.expiresIn) : - issuedAt.plusSeconds(1); + Instant expiresAt = getExpiresAt(); OAuth2AccessTokenResponse accessTokenResponse = new OAuth2AccessTokenResponse(); accessTokenResponse.accessToken = new OAuth2AccessToken( @@ -181,5 +201,28 @@ public OAuth2AccessTokenResponse build() { CollectionUtils.isEmpty(this.additionalParameters) ? Collections.emptyMap() : this.additionalParameters); return accessTokenResponse; } + + private Instant getIssuedAt() { + if (this.issuedAt == null) { + this.issuedAt = Instant.now(); + } + return this.issuedAt; + } + + /** + * expires_in is RECOMMENDED, as per spec https://tools.ietf.org/html/rfc6749#section-5.1 + * Therefore, expires_in may not be returned in the Access Token response which would result in the default value of 0. + * For these instances, default the expiresAt to +1 second from issuedAt time. + * @return + */ + private Instant getExpiresAt() { + if (this.expiresAt == null) { + Instant issuedAt = getIssuedAt(); + this.expiresAt = this.expiresIn > 0 ? + issuedAt.plusSeconds(this.expiresIn) : + issuedAt.plusSeconds(1); + } + return this.expiresAt; + } } } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseTests.java index cfd5a0dedf6..fe2b9483360 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseTests.java @@ -102,4 +102,55 @@ public void buildWhenAllAttributesProvidedThenAllAttributesAreSet() { assertThat(tokenResponse.getRefreshToken().getTokenValue()).isEqualTo(REFRESH_TOKEN_VALUE); assertThat(tokenResponse.getAdditionalParameters()).isEqualTo(additionalParameters); } + + @Test + public void buildWhenResponseThenAllAttributesAreSet() { + Instant expiresAt = Instant.now().plusSeconds(5); + Set scopes = new LinkedHashSet<>(Arrays.asList("scope1", "scope2")); + Map additionalParameters = new HashMap<>(); + additionalParameters.put("param1", "value1"); + additionalParameters.put("param2", "value2"); + + OAuth2AccessTokenResponse tokenResponse = OAuth2AccessTokenResponse + .withToken(TOKEN_VALUE) + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(expiresAt.toEpochMilli()) + .scopes(scopes) + .refreshToken(REFRESH_TOKEN_VALUE) + .additionalParameters(additionalParameters) + .build(); + + OAuth2AccessTokenResponse withResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse) + .build(); + + assertThat(withResponse.getAccessToken().getTokenValue()).isEqualTo(tokenResponse.getAccessToken().getTokenValue()); + assertThat(withResponse.getAccessToken().getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER); + assertThat(withResponse.getAccessToken().getIssuedAt()).isEqualTo(tokenResponse.getAccessToken().getIssuedAt()); + assertThat(withResponse.getAccessToken().getExpiresAt()).isEqualTo(tokenResponse.getAccessToken().getExpiresAt()); + assertThat(withResponse.getAccessToken().getScopes()).isEqualTo(tokenResponse.getAccessToken().getScopes()); + assertThat(withResponse.getRefreshToken().getTokenValue()).isEqualTo(tokenResponse.getRefreshToken().getTokenValue()); + assertThat(withResponse.getAdditionalParameters()).isEqualTo(tokenResponse.getAdditionalParameters()); + } + + @Test + public void buildWhenResponseAndRefreshNullThenRefreshNull() { + Instant expiresAt = Instant.now().plusSeconds(5); + Set scopes = new LinkedHashSet<>(Arrays.asList("scope1", "scope2")); + Map additionalParameters = new HashMap<>(); + additionalParameters.put("param1", "value1"); + additionalParameters.put("param2", "value2"); + + OAuth2AccessTokenResponse tokenResponse = OAuth2AccessTokenResponse + .withToken(TOKEN_VALUE) + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(expiresAt.toEpochMilli()) + .scopes(scopes) + .additionalParameters(additionalParameters) + .build(); + + OAuth2AccessTokenResponse withResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse) + .build(); + + assertThat(withResponse.getRefreshToken()).isNull(); + } } From a0fea11982a2f96b202adba9b7e515aa079aa755 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 28 Jun 2018 11:07:20 -0500 Subject: [PATCH 092/226] Add OAuth2AccessTokenResponseBodyExtractor This externalizes converting a OAuth2AccessTokenResponse from a ReactiveHttpInputMessage. Fixes: gh-5475 --- ...eAuthorizationCodeTokenResponseClient.java | 104 ++------------- .../spring-security-oauth2-core.gradle | 4 + ...Auth2AccessTokenResponseBodyExtractor.java | 113 ++++++++++++++++ .../function/OAuth2BodyExtractors.java | 40 ++++++ .../function/OAuth2BodyExtractorsTests.java | 125 ++++++++++++++++++ 5 files changed, 293 insertions(+), 93 deletions(-) create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2AccessTokenResponseBodyExtractor.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2BodyExtractors.java create mode 100644 oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2BodyExtractorsTests.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClient.java index f92c6cf36f2..1f7833087b6 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClient.java @@ -15,38 +15,21 @@ */ package org.springframework.security.oauth2.client.endpoint; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; - -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; - -import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; -import org.springframework.util.CollectionUtils; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; import org.springframework.web.reactive.function.client.WebClient; - -import com.nimbusds.oauth2.sdk.AccessTokenResponse; -import com.nimbusds.oauth2.sdk.ErrorObject; -import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.TokenErrorResponse; -import com.nimbusds.oauth2.sdk.TokenResponse; -import com.nimbusds.oauth2.sdk.token.AccessToken; - -import net.minidev.json.JSONObject; import reactor.core.publisher.Mono; +import static org.springframework.security.oauth2.core.web.reactive.function.OAuth2BodyExtractors.oauth2AccessTokenResponse; +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; + /** * An implementation of an {@link ReactiveOAuth2AccessTokenResponseClient} that "exchanges" * an authorization code credential for an access token credential @@ -65,8 +48,6 @@ * @see Section 4.1.4 Access Token Response (Authorization Code Grant) */ public class NimbusReactiveAuthorizationCodeTokenResponseClient implements ReactiveOAuth2AccessTokenResponseClient { - private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response"; - private WebClient webClient = WebClient.builder() .filter(ExchangeFilterFunctions.basicAuthentication()) .build(); @@ -87,52 +68,15 @@ public Mono getTokenResponse(OAuth2AuthorizationCodeG .accept(MediaType.APPLICATION_JSON) .attributes(basicAuthenticationCredentials(clientRegistration.getClientId(), clientRegistration.getClientSecret())) .body(body) - .retrieve() - .onStatus(s -> false, response -> { - throw new IllegalStateException("Disabled Status Handlers"); - }) - .bodyToMono(new ParameterizedTypeReference>() {}) - .map(json -> parse(json)) - .flatMap(tokenResponse -> accessTokenResponse(tokenResponse)) - .map(accessTokenResponse -> { - AccessToken accessToken = accessTokenResponse.getTokens().getAccessToken(); - OAuth2AccessToken.TokenType accessTokenType = null; - if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase( - accessToken.getType().getValue())) { - accessTokenType = OAuth2AccessToken.TokenType.BEARER; - } - long expiresIn = accessToken.getLifetime(); - - // As per spec, in section 5.1 Successful Access Token Response - // https://tools.ietf.org/html/rfc6749#section-5.1 - // If AccessTokenResponse.scope is empty, then default to the scope - // originally requested by the client in the Authorization Request - Set scopes; - if (CollectionUtils.isEmpty( - accessToken.getScope())) { - scopes = new LinkedHashSet<>( - authorizationExchange.getAuthorizationRequest().getScopes()); - } - else { - scopes = new LinkedHashSet<>( - accessToken.getScope().toStringList()); - } - - String refreshToken = null; - if (accessTokenResponse.getTokens().getRefreshToken() != null) { - refreshToken = accessTokenResponse.getTokens().getRefreshToken().getValue(); - } - - Map additionalParameters = new LinkedHashMap<>( - accessTokenResponse.getCustomParameters()); - - return OAuth2AccessTokenResponse.withToken(accessToken.getValue()) - .tokenType(accessTokenType) - .expiresIn(expiresIn) - .scopes(scopes) - .refreshToken(refreshToken) - .additionalParameters(additionalParameters) + .exchange() + .flatMap(response -> response.body(oauth2AccessTokenResponse())) + .map(response -> { + if (response.getAccessToken().getScopes().isEmpty()) { + response = OAuth2AccessTokenResponse.withResponse(response) + .scopes(authorizationExchange.getAuthorizationRequest().getScopes()) .build(); + } + return response; }); }); } @@ -148,30 +92,4 @@ private static BodyInserters.FormInserter body(OAuth2AuthorizationExchan } return body; } - - private static Mono accessTokenResponse(TokenResponse tokenResponse) { - if (tokenResponse.indicatesSuccess()) { - return Mono.just(tokenResponse) - .cast(AccessTokenResponse.class); - } - TokenErrorResponse tokenErrorResponse = (TokenErrorResponse) tokenResponse; - ErrorObject errorObject = tokenErrorResponse.getErrorObject(); - OAuth2Error oauth2Error = new OAuth2Error(errorObject.getCode(), - errorObject.getDescription(), (errorObject.getURI() != null ? - errorObject.getURI().toString() : - null)); - - return Mono.error(new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString())); - } - - private static TokenResponse parse(Map json) { - try { - return TokenResponse.parse(new JSONObject(json)); - } - catch (ParseException pe) { - OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE, - "An error occurred parsing the Access Token response: " + pe.getMessage(), null); - throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), pe); - } - } } diff --git a/oauth2/oauth2-core/spring-security-oauth2-core.gradle b/oauth2/oauth2-core/spring-security-oauth2-core.gradle index 0a477bf7c32..bc66851194d 100644 --- a/oauth2/oauth2-core/spring-security-oauth2-core.gradle +++ b/oauth2/oauth2-core/spring-security-oauth2-core.gradle @@ -4,5 +4,9 @@ dependencies { compile project(':spring-security-core') compile springCoreDependency + optional 'com.fasterxml.jackson.core:jackson-databind' + optional 'com.nimbusds:oauth2-oidc-sdk' + optional 'org.springframework:spring-webflux' + testCompile powerMock2Dependencies } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2AccessTokenResponseBodyExtractor.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2AccessTokenResponseBodyExtractor.java new file mode 100644 index 00000000000..a14287eb120 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2AccessTokenResponseBodyExtractor.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.core.web.reactive.function; + +import com.nimbusds.oauth2.sdk.AccessTokenResponse; +import com.nimbusds.oauth2.sdk.ErrorObject; +import com.nimbusds.oauth2.sdk.ParseException; +import com.nimbusds.oauth2.sdk.TokenErrorResponse; +import com.nimbusds.oauth2.sdk.TokenResponse; +import com.nimbusds.oauth2.sdk.token.AccessToken; +import net.minidev.json.JSONObject; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.ReactiveHttpInputMessage; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.web.reactive.function.BodyExtractor; +import org.springframework.web.reactive.function.BodyExtractors; +import reactor.core.publisher.Mono; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * Provides a way to create an {@link OAuth2AccessTokenResponse} from a {@link ReactiveHttpInputMessage} + * @author Rob Winch + * @since 5.1 + */ +class OAuth2AccessTokenResponseBodyExtractor + implements BodyExtractor, ReactiveHttpInputMessage> { + + private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response"; + + OAuth2AccessTokenResponseBodyExtractor() {} + + @Override + public Mono extract(ReactiveHttpInputMessage inputMessage, + Context context) { + ParameterizedTypeReference> type = new ParameterizedTypeReference>() {}; + BodyExtractor>, ReactiveHttpInputMessage> delegate = BodyExtractors.toMono(type); + return delegate.extract(inputMessage, context) + .map(json -> parse(json)) + .flatMap(OAuth2AccessTokenResponseBodyExtractor::oauth2AccessTokenResponse) + .map(OAuth2AccessTokenResponseBodyExtractor::oauth2AccessTokenResponse); + } + + private static TokenResponse parse(Map json) { + try { + return TokenResponse.parse(new JSONObject(json)); + } + catch (ParseException pe) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE, + "An error occurred parsing the Access Token response: " + pe.getMessage(), null); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), pe); + } + } + + private static Mono oauth2AccessTokenResponse(TokenResponse tokenResponse) { + if (tokenResponse.indicatesSuccess()) { + return Mono.just(tokenResponse) + .cast(AccessTokenResponse.class); + } + TokenErrorResponse tokenErrorResponse = (TokenErrorResponse) tokenResponse; + ErrorObject errorObject = tokenErrorResponse.getErrorObject(); + OAuth2Error oauth2Error = new OAuth2Error(errorObject.getCode(), + errorObject.getDescription(), (errorObject.getURI() != null ? + errorObject.getURI().toString() : + null)); + + return Mono.error(new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString())); + } + + private static OAuth2AccessTokenResponse oauth2AccessTokenResponse(AccessTokenResponse accessTokenResponse) { + AccessToken accessToken = accessTokenResponse.getTokens().getAccessToken(); + OAuth2AccessToken.TokenType accessTokenType = null; + if (OAuth2AccessToken.TokenType.BEARER.getValue() + .equalsIgnoreCase(accessToken.getType().getValue())) { + accessTokenType = OAuth2AccessToken.TokenType.BEARER; + } + long expiresIn = accessToken.getLifetime(); + + Set scopes = accessToken.getScope() == null ? + Collections.emptySet() : new LinkedHashSet<>(accessToken.getScope().toStringList()); + + String refreshToken = null; + if (accessTokenResponse.getTokens().getRefreshToken() != null) { + refreshToken = accessTokenResponse.getTokens().getRefreshToken().getValue(); + } + + Map additionalParameters = new LinkedHashMap<>(accessTokenResponse.getCustomParameters()); + + return OAuth2AccessTokenResponse.withToken(accessToken.getValue()).tokenType(accessTokenType).expiresIn(expiresIn).scopes(scopes) + .refreshToken(refreshToken).additionalParameters(additionalParameters).build(); + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2BodyExtractors.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2BodyExtractors.java new file mode 100644 index 00000000000..fbffe082eb7 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2BodyExtractors.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.core.web.reactive.function; + +import org.springframework.http.ReactiveHttpInputMessage; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.web.reactive.function.BodyExtractor; +import reactor.core.publisher.Mono; + +/** + * Static factory methods for OAuth2 {@link BodyExtractor} implementations. + * @author Rob Winch + * @since 5.1 + */ +public abstract class OAuth2BodyExtractors { + + /** + * Extractor to decode an {@link OAuth2AccessTokenResponse} + * @return a BodyExtractor for {@link OAuth2AccessTokenResponse} + */ + public static BodyExtractor, ReactiveHttpInputMessage> oauth2AccessTokenResponse() { + return new OAuth2AccessTokenResponseBodyExtractor(); + } + + private OAuth2BodyExtractors() {} +} diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2BodyExtractorsTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2BodyExtractorsTests.java new file mode 100644 index 00000000000..0e46b5d1dac --- /dev/null +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2BodyExtractorsTests.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.core.web.reactive.function; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.codec.ByteBufferDecoder; +import org.springframework.core.codec.StringDecoder; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ReactiveHttpInputMessage; +import org.springframework.http.codec.DecoderHttpMessageReader; +import org.springframework.http.codec.FormHttpMessageReader; +import org.springframework.http.codec.HttpMessageReader; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.xml.Jaxb2XmlDecoder; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.mock.http.client.reactive.MockClientHttpResponse; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.web.reactive.function.BodyExtractor; +import reactor.core.publisher.Mono; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class OAuth2BodyExtractorsTests { + + private BodyExtractor.Context context; + + private Map hints; + + @Before + public void createContext() { + final List> messageReaders = new ArrayList<>(); + messageReaders.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); + messageReaders.add(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes())); + messageReaders.add(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder())); + messageReaders.add(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder())); + messageReaders.add(new FormHttpMessageReader()); + + this.hints = new HashMap(); + this.context = new BodyExtractor.Context() { + @Override + public List> messageReaders() { + return messageReaders; + } + + @Override + public Optional serverResponse() { + return Optional.empty(); + } + + @Override + public Map hints() { + return OAuth2BodyExtractorsTests.this.hints; + } + }; + } + + @Test + public void oauth2AccessTokenResponseWhenInvalidJsonThenException() { + BodyExtractor, ReactiveHttpInputMessage> extractor = OAuth2BodyExtractors + .oauth2AccessTokenResponse(); + + MockClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK); + response.getHeaders().setContentType(MediaType.APPLICATION_JSON); + response.setBody("{"); + + Mono result = extractor.extract(response, this.context); + + assertThatCode(() -> result.block()) + .isInstanceOf(RuntimeException.class); + } + + @Test + public void oauth2AccessTokenResponseWhenValidThenCreated() throws Exception { + BodyExtractor, ReactiveHttpInputMessage> extractor = OAuth2BodyExtractors + .oauth2AccessTokenResponse(); + + MockClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK); + response.getHeaders().setContentType(MediaType.APPLICATION_JSON); + response.setBody("{\n" + + " \"access_token\":\"2YotnFZFEjr1zCsicMWpAA\",\n" + + " \"token_type\":\"Bearer\",\n" + + " \"expires_in\":3600,\n" + + " \"refresh_token\":\"tGzv3JOkF0XG5Qx2TlKWIA\",\n" + + " \"example_parameter\":\"example_value\"\n" + + " }"); + + Instant now = Instant.now(); + OAuth2AccessTokenResponse result = extractor.extract(response, this.context).block(); + + assertThat(result.getAccessToken().getTokenValue()).isEqualTo("2YotnFZFEjr1zCsicMWpAA"); + assertThat(result.getAccessToken().getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER); + assertThat(result.getAccessToken().getExpiresAt()).isBetween(now.plusSeconds(3600), now.plusSeconds(3600 + 2)); + assertThat(result.getRefreshToken().getTokenValue()).isEqualTo("tGzv3JOkF0XG5Qx2TlKWIA"); + assertThat(result.getAdditionalParameters()).containsEntry("example_parameter", "example_value"); + } +} From 56519705d9c90f5fa1a6dc1cc6f38e21a559b5df Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 2 Jul 2018 10:00:06 -0500 Subject: [PATCH 093/226] MockExchangeFunction Support Multiple Requests Issue: gh-5386 --- .../function/client/MockExchangeFunction.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/MockExchangeFunction.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/MockExchangeFunction.java index a6cf3b4183e..681d672b603 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/MockExchangeFunction.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/MockExchangeFunction.java @@ -24,23 +24,31 @@ import reactor.core.publisher.Mono; +import java.util.ArrayList; +import java.util.List; + /** * @author Rob Winch * @since 5.1 */ public class MockExchangeFunction implements ExchangeFunction { - private ClientRequest request; + private List requests = new ArrayList<>(); private ClientResponse response = mock(ClientResponse.class); public ClientRequest getRequest() { - return this.request; + return this.requests.get(this.requests.size() - 1); + } + + public List getRequests() { + return this.requests; + } } @Override public Mono exchange(ClientRequest request) { return Mono.defer(() -> { - this.request = request; + this.requests.add(request); return Mono.just(this.response); }); } From 144d61db50355a77c6d21cf92b1f907b6d711374 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 2 Jul 2018 10:00:40 -0500 Subject: [PATCH 094/226] Add MockExchangeFunction getResponse This allows setting up the mock Issue: gh-5386 --- .../web/reactive/function/client/MockExchangeFunction.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/MockExchangeFunction.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/MockExchangeFunction.java index 681d672b603..8f1665d4377 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/MockExchangeFunction.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/MockExchangeFunction.java @@ -43,6 +43,9 @@ public ClientRequest getRequest() { public List getRequests() { return this.requests; } + + public ClientResponse getResponse() { + return this.response; } @Override From 1cc5eed73813054dd31d753107ffd68483ac8c75 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 2 Jul 2018 11:29:39 -0500 Subject: [PATCH 095/226] OAuth2AuthorizedClientExchangeFilterFunction Refresh Support --- ...uthorizedClientExchangeFilterFunction.java | 190 ++++++++++++++- ...izedClientExchangeFilterFunctionTests.java | 216 +++++++++++++++++- 2 files changed, 393 insertions(+), 13 deletions(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunction.java index 8df207778aa..45212eb593e 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunction.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunction.java @@ -16,20 +16,40 @@ package org.springframework.security.oauth2.client.web.reactive.function.client; -import static org.springframework.security.web.http.SecurityHeaders.bearerToken; - -import java.util.Map; -import java.util.Optional; -import java.util.function.Consumer; - +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.util.Assert; +import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.ExchangeFunction; - import reactor.core.publisher.Mono; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Base64; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; + +import static org.springframework.security.oauth2.core.web.reactive.function.OAuth2BodyExtractors.oauth2AccessTokenResponse; +import static org.springframework.security.web.http.SecurityHeaders.bearerToken; + /** * Provides an easy mechanism for using an {@link OAuth2AuthorizedClient} to make OAuth2 requests by including the * token as a Bearer Token. @@ -43,12 +63,27 @@ public final class OAuth2AuthorizedClientExchangeFilterFunction implements Excha */ private static final String OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME = OAuth2AuthorizedClient.class.getName(); + private Clock clock = Clock.systemUTC(); + + private Duration accessTokenExpiresSkew = Duration.ofMinutes(1); + + private ReactiveOAuth2AuthorizedClientService authorizedClientService; + + public OAuth2AuthorizedClientExchangeFilterFunction() {} + + public OAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientService authorizedClientService) { + this.authorizedClientService = authorizedClientService; + } + /** * Modifies the {@link ClientRequest#attributes()} to include the {@link OAuth2AuthorizedClient} to be used for * providing the Bearer Token. Example usage: * *
    -	 * Mono response = this.webClient
    +	 * WebClient webClient = WebClient.builder()
    +	 *    .filter(new OAuth2AuthorizedClientExchangeFilterFunction(authorizedClientService))
    +	 *    .build();
    +	 * Mono response = webClient
     	 *    .get()
     	 *    .uri(uri)
     	 *    .attributes(oauth2AuthorizedClient(authorizedClient))
    @@ -57,6 +92,20 @@ public final class OAuth2AuthorizedClientExchangeFilterFunction implements Excha
     	 *    .bodyToMono(String.class);
     	 * 
    * + * An attempt to automatically refresh the token will be made if all of the following + * are true: + * + *
      + *
    • The ReactiveOAuth2AuthorizedClientService on the + * {@link OAuth2AuthorizedClientExchangeFilterFunction} is not null
    • + *
    • A refresh token is present on the OAuth2AuthorizedClient
    • + *
    • The access token will be expired in + * {@link #setAccessTokenExpiresSkew(Duration)}
    • + *
    • The {@link ReactiveSecurityContextHolder} will be used to attempt to save + * the token. If it is empty, then the principal name on the OAuth2AuthorizedClient + * will be used to create an Authentication for saving.
    • + *
    + * * @param authorizedClient the {@link OAuth2AuthorizedClient} to use. * @return the {@link Consumer} to populate the */ @@ -64,14 +113,79 @@ public static Consumer> oauth2AuthorizedClient(OAuth2Authori return attributes -> attributes.put(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME, authorizedClient); } + /** + * An access token will be considered expired by comparing its expiration to now + + * this skewed Duration. The default is 1 minute. + * @param accessTokenExpiresSkew the Duration to use. + */ + public void setAccessTokenExpiresSkew(Duration accessTokenExpiresSkew) { + Assert.notNull(accessTokenExpiresSkew, "accessTokenExpiresSkew cannot be null"); + this.accessTokenExpiresSkew = accessTokenExpiresSkew; + } + @Override public Mono filter(ClientRequest request, ExchangeFunction next) { Optional attribute = request.attribute(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME) .map(OAuth2AuthorizedClient.class::cast); - return attribute + return Mono.justOrEmpty(attribute) + .flatMap(authorizedClient -> authorizedClient(next, authorizedClient)) .map(authorizedClient -> bearer(request, authorizedClient)) - .map(next::exchange) - .orElseGet(() -> next.exchange(request)); + .flatMap(next::exchange) + .switchIfEmpty(next.exchange(request)); + } + + private Mono authorizedClient(ExchangeFunction next, OAuth2AuthorizedClient authorizedClient) { + if (shouldRefresh(authorizedClient)) { + return refreshAuthorizedClient(next, authorizedClient); + } + return Mono.just(authorizedClient); + } + + private Mono refreshAuthorizedClient(ExchangeFunction next, + OAuth2AuthorizedClient authorizedClient) { + ClientRegistration clientRegistration = authorizedClient + .getClientRegistration(); + String tokenUri = clientRegistration + .getProviderDetails().getTokenUri(); + ClientRequest request = ClientRequest.create(HttpMethod.POST, URI.create(tokenUri)) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .headers(httpBasic(clientRegistration.getClientId(), clientRegistration.getClientSecret())) + .body(refreshTokenBody(authorizedClient.getRefreshToken().getTokenValue())) + .build(); + return next.exchange(request) + .flatMap(response -> response.body(oauth2AccessTokenResponse())) + .map(accessTokenResponse -> new OAuth2AuthorizedClient(authorizedClient.getClientRegistration(), authorizedClient.getPrincipalName(), accessTokenResponse.getAccessToken(), accessTokenResponse.getRefreshToken())) + .flatMap(result -> ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .defaultIfEmpty(new PrincipalNameAuthentication(authorizedClient.getPrincipalName())) + .flatMap(principal -> this.authorizedClientService.saveAuthorizedClient(result, principal)) + .thenReturn(result)); + } + + private static Consumer httpBasic(String username, String password) { + return httpHeaders -> { + String credentialsString = username + ":" + password; + byte[] credentialBytes = credentialsString.getBytes(StandardCharsets.ISO_8859_1); + byte[] encodedBytes = Base64.getEncoder().encode(credentialBytes); + String encodedCredentials = new String(encodedBytes, StandardCharsets.ISO_8859_1); + httpHeaders.set(HttpHeaders.AUTHORIZATION, "Basic " + encodedCredentials); + }; + } + + private boolean shouldRefresh(OAuth2AuthorizedClient authorizedClient) { + if (this.authorizedClientService == null) { + return false; + } + OAuth2RefreshToken refreshToken = authorizedClient.getRefreshToken(); + if (refreshToken == null) { + return false; + } + Instant now = this.clock.instant(); + Instant expiresAt = authorizedClient.getAccessToken().getExpiresAt(); + if (now.isAfter(expiresAt.minus(this.accessTokenExpiresSkew))) { + return true; + } + return false; } private ClientRequest bearer(ClientRequest request, OAuth2AuthorizedClient authorizedClient) { @@ -79,4 +193,58 @@ private ClientRequest bearer(ClientRequest request, OAuth2AuthorizedClient autho .headers(bearerToken(authorizedClient.getAccessToken().getTokenValue())) .build(); } + + private static BodyInserters.FormInserter refreshTokenBody(String refreshToken) { + return BodyInserters + .fromFormData("grant_type", AuthorizationGrantType.REFRESH_TOKEN.getValue()) + .with("refresh_token", refreshToken); + } + + private static class PrincipalNameAuthentication implements Authentication { + private final String username; + + private PrincipalNameAuthentication(String username) { + this.username = username; + } + + @Override + public Collection getAuthorities() { + throw unsupported(); + } + + @Override + public Object getCredentials() { + throw unsupported(); + } + + @Override + public Object getDetails() { + throw unsupported(); + } + + @Override + public Object getPrincipal() { + throw unsupported(); + } + + @Override + public boolean isAuthenticated() { + throw unsupported(); + } + + @Override + public void setAuthenticated(boolean isAuthenticated) + throws IllegalArgumentException { + throw unsupported(); + } + + @Override + public String getName() { + return this.username; + } + + private UnsupportedOperationException unsupported() { + return new UnsupportedOperationException("Not Supported"); + } + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunctionTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunctionTests.java index f313e5d42d7..8a5929f8551 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunctionTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunctionTests.java @@ -17,19 +17,51 @@ package org.springframework.security.oauth2.client.web.reactive.function.client; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.core.codec.ByteBufferEncoder; +import org.springframework.core.codec.CharSequenceEncoder; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.codec.EncoderHttpMessageWriter; +import org.springframework.http.codec.FormHttpMessageWriter; +import org.springframework.http.codec.HttpMessageWriter; +import org.springframework.http.codec.ResourceHttpMessageWriter; +import org.springframework.http.codec.ServerSentEventHttpMessageWriter; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.http.codec.multipart.MultipartHttpMessageWriter; +import org.springframework.http.codec.xml.Jaxb2XmlEncoder; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.mock.http.client.reactive.MockClientHttpRequest; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.client.ClientRequest; +import reactor.core.publisher.Mono; import java.net.URI; import java.time.Duration; import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.springframework.http.HttpMethod.GET; import static org.springframework.security.oauth2.client.web.reactive.function.client.OAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient; @@ -37,7 +69,11 @@ * @author Rob Winch * @since 5.1 */ +@RunWith(MockitoJUnitRunner.class) public class OAuth2AuthorizedClientExchangeFilterFunctionTests { + @Mock + private ReactiveOAuth2AuthorizedClientService authorizedClientService; + private OAuth2AuthorizedClientExchangeFilterFunction function = new OAuth2AuthorizedClientExchangeFilterFunction(); private MockExchangeFunction exchange = new MockExchangeFunction(); @@ -57,7 +93,7 @@ public class OAuth2AuthorizedClientExchangeFilterFunctionTests { .build(); private OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, - "token", + "token-0", Instant.now(), Instant.now().plus(Duration.ofDays(1))); @@ -98,4 +134,180 @@ public void filterWhenExistingAuthorizationThenSingleAuthorizationHeader() { HttpHeaders headers = this.exchange.getRequest().headers(); assertThat(headers.get(HttpHeaders.AUTHORIZATION)).containsOnly("Bearer " + this.accessToken.getTokenValue()); } + + @Test + public void filterWhenRefreshRequiredThenRefresh() { + when(this.authorizedClientService.saveAuthorizedClient(any(), any())).thenReturn(Mono.empty()); + OAuth2AccessTokenResponse response = OAuth2AccessTokenResponse.withToken("token-1") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(3600) + .refreshToken("refresh-1") + .build(); + when(this.exchange.getResponse().body(any())).thenReturn(Mono.just(response)); + Instant issuedAt = Instant.now().minus(Duration.ofDays(1)); + Instant accessTokenExpiresAt = issuedAt.plus(Duration.ofHours(1)); + Instant refreshTokenExpiresAt = Instant.now().plus(Duration.ofHours(1)); + + this.accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(), + this.accessToken.getTokenValue(), + issuedAt, + accessTokenExpiresAt); + this.function = new OAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientService); + + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", issuedAt, refreshTokenExpiresAt); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken, refreshToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + TestingAuthenticationToken authentication = new TestingAuthenticationToken("test","this"); + this.function.filter(request, this.exchange) + .subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication)) + .block(); + + verify(this.authorizedClientService).saveAuthorizedClient(any(), eq(authentication)); + + List requests = this.exchange.getRequests(); + assertThat(requests).hasSize(2); + + ClientRequest request0 = requests.get(0); + assertThat(request0.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0"); + assertThat(request0.url().toASCIIString()).isEqualTo("https://github.com/login/oauth/access_token"); + assertThat(request0.method()).isEqualTo(HttpMethod.POST); + assertThat(getBody(request0)).isEqualTo("grant_type=refresh_token&refresh_token=refresh-token"); + + ClientRequest request1 = requests.get(1); + assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-1"); + assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com"); + assertThat(request1.method()).isEqualTo(HttpMethod.GET); + assertThat(getBody(request1)).isEmpty(); + } + + @Test + public void filterWhenRefreshRequiredAndEmptyReactiveSecurityContextThenSaved() { + when(this.authorizedClientService.saveAuthorizedClient(any(), any())).thenReturn(Mono.empty()); + OAuth2AccessTokenResponse response = OAuth2AccessTokenResponse.withToken("token-1") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(3600) + .refreshToken("refresh-1") + .build(); + when(this.exchange.getResponse().body(any())).thenReturn(Mono.just(response)); + Instant issuedAt = Instant.now().minus(Duration.ofDays(1)); + Instant accessTokenExpiresAt = issuedAt.plus(Duration.ofHours(1)); + Instant refreshTokenExpiresAt = Instant.now().plus(Duration.ofHours(1)); + + this.accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(), + this.accessToken.getTokenValue(), + issuedAt, + accessTokenExpiresAt); + this.function = new OAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientService); + + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", issuedAt, refreshTokenExpiresAt); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken, refreshToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + this.function.filter(request, this.exchange) + .block(); + + verify(this.authorizedClientService).saveAuthorizedClient(any(), any()); + + List requests = this.exchange.getRequests(); + assertThat(requests).hasSize(2); + + ClientRequest request0 = requests.get(0); + assertThat(request0.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0"); + assertThat(request0.url().toASCIIString()).isEqualTo("https://github.com/login/oauth/access_token"); + assertThat(request0.method()).isEqualTo(HttpMethod.POST); + assertThat(getBody(request0)).isEqualTo("grant_type=refresh_token&refresh_token=refresh-token"); + + ClientRequest request1 = requests.get(1); + assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-1"); + assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com"); + assertThat(request1.method()).isEqualTo(HttpMethod.GET); + assertThat(getBody(request1)).isEmpty(); + } + + @Test + public void filterWhenRefreshTokenNullThenShouldRefreshFalse() { + this.function = new OAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientService); + + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + this.function.filter(request, this.exchange).block(); + + List requests = this.exchange.getRequests(); + assertThat(requests).hasSize(1); + + ClientRequest request0 = requests.get(0); + assertThat(request0.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-0"); + assertThat(request0.url().toASCIIString()).isEqualTo("https://example.com"); + assertThat(request0.method()).isEqualTo(HttpMethod.GET); + assertThat(getBody(request0)).isEmpty(); + } + + @Test + public void filterWhenNotExpiredThenShouldRefreshFalse() { + this.function = new OAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientService); + + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", this.accessToken.getIssuedAt(), this.accessToken.getExpiresAt()); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken, refreshToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + this.function.filter(request, this.exchange).block(); + + List requests = this.exchange.getRequests(); + assertThat(requests).hasSize(1); + + ClientRequest request0 = requests.get(0); + assertThat(request0.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-0"); + assertThat(request0.url().toASCIIString()).isEqualTo("https://example.com"); + assertThat(request0.method()).isEqualTo(HttpMethod.GET); + assertThat(getBody(request0)).isEmpty(); + } + + private static String getBody(ClientRequest request) { + final List> messageWriters = new ArrayList<>(); + messageWriters.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder())); + messageWriters.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.textPlainOnly())); + messageWriters.add(new ResourceHttpMessageWriter()); + messageWriters.add(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder())); + Jackson2JsonEncoder jsonEncoder = new Jackson2JsonEncoder(); + messageWriters.add(new EncoderHttpMessageWriter<>(jsonEncoder)); + messageWriters.add(new ServerSentEventHttpMessageWriter(jsonEncoder)); + messageWriters.add(new FormHttpMessageWriter()); + messageWriters.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes())); + messageWriters.add(new MultipartHttpMessageWriter(messageWriters)); + + BodyInserter.Context context = new BodyInserter.Context() { + @Override + public List> messageWriters() { + return messageWriters; + } + + @Override + public Optional serverRequest() { + return Optional.empty(); + } + + @Override + public Map hints() { + return new HashMap<>(); + } + }; + + MockClientHttpRequest body = new MockClientHttpRequest(HttpMethod.GET, "/"); + request.body().insert(body, context).block(); + return body.getBodyAsString().block(); + } } From ea18dc4fa37f2a7bfd5a75f35f643186a1ce7d01 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 2 Jul 2018 15:46:33 -0500 Subject: [PATCH 096/226] Fix Imports of OAuth2AccessTokenResponse Issue: gh-5474 --- .../security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java | 1 - 1 file changed, 1 deletion(-) diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java index 29af0676318..38cdbbbc6a5 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java @@ -21,7 +21,6 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; -import java.time.Duration; import java.time.Instant; import java.util.Collections; import java.util.Map; From fa8c1186871fbab86adf2ebf557322b79d1ddbba Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 2 Jul 2018 15:47:24 -0500 Subject: [PATCH 097/226] Fix checkstyle OAuth2AuthorizedClientExchangeFilterFunctionTests Issue: gh-4371 --- .../OAuth2AuthorizedClientExchangeFilterFunctionTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunctionTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunctionTests.java index 8a5929f8551..e6987560462 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunctionTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunctionTests.java @@ -161,7 +161,7 @@ public void filterWhenRefreshRequiredThenRefresh() { .attributes(oauth2AuthorizedClient(authorizedClient)) .build(); - TestingAuthenticationToken authentication = new TestingAuthenticationToken("test","this"); + TestingAuthenticationToken authentication = new TestingAuthenticationToken("test", "this"); this.function.filter(request, this.exchange) .subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication)) .block(); From 6373b596372f1ae32448aa05f623cc4578b227e3 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 2 Jul 2018 16:16:16 -0500 Subject: [PATCH 098/226] Fix OAuth2AuthorizedClientExchangeFilterFunctionTests on JDK9 Issue: gh-4371 --- .../OAuth2AuthorizedClientExchangeFilterFunctionTests.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunctionTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunctionTests.java index e6987560462..d673d845b4b 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunctionTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunctionTests.java @@ -31,7 +31,6 @@ import org.springframework.http.codec.ServerSentEventHttpMessageWriter; import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.multipart.MultipartHttpMessageWriter; -import org.springframework.http.codec.xml.Jaxb2XmlEncoder; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.mock.http.client.reactive.MockClientHttpRequest; import org.springframework.security.authentication.TestingAuthenticationToken; @@ -281,7 +280,6 @@ private static String getBody(ClientRequest request) { messageWriters.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder())); messageWriters.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.textPlainOnly())); messageWriters.add(new ResourceHttpMessageWriter()); - messageWriters.add(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder())); Jackson2JsonEncoder jsonEncoder = new Jackson2JsonEncoder(); messageWriters.add(new EncoderHttpMessageWriter<>(jsonEncoder)); messageWriters.add(new ServerSentEventHttpMessageWriter(jsonEncoder)); From 8bb5e6f14b8131badedb1a9401cff1eae250f94f Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 2 Jul 2018 16:29:07 -0500 Subject: [PATCH 099/226] Fix OAuth2BodyExtractorsTests for JDK9 Issue: gh-5475 --- .../core/web/reactive/function/OAuth2BodyExtractorsTests.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2BodyExtractorsTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2BodyExtractorsTests.java index 0e46b5d1dac..8b9b63f01ee 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2BodyExtractorsTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2BodyExtractorsTests.java @@ -27,7 +27,6 @@ import org.springframework.http.codec.FormHttpMessageReader; import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.json.Jackson2JsonDecoder; -import org.springframework.http.codec.xml.Jaxb2XmlDecoder; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.mock.http.client.reactive.MockClientHttpResponse; import org.springframework.security.oauth2.core.OAuth2AccessToken; @@ -60,7 +59,6 @@ public void createContext() { final List> messageReaders = new ArrayList<>(); messageReaders.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); messageReaders.add(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes())); - messageReaders.add(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder())); messageReaders.add(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder())); messageReaders.add(new FormHttpMessageReader()); From 48972521cbfd1a1412d93b867beb3a378fc9078c Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 25 Jun 2018 16:16:25 -0600 Subject: [PATCH 100/226] Close Nimbus Information Leak This commit captures and remaps the exception that Nimbus throws when a PlainJWT is presented to it. While the surrounding classes are likely only used today by the oauth2Login flow, since they are public, we'll patch them at this point for anyone who may be using them directly. Fixes: gh-5457 --- .../jwt/NimbusJwtDecoderJwkSupport.java | 20 ++++++++++++++-- .../jwt/NimbusJwtDecoderJwkSupportTests.java | 24 ++++++++++++++++--- .../jwt/NimbusReactiveJwtDecoderTests.java | 9 +++++++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java index 6a8f0eb34f8..dc93bf0e05e 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java @@ -26,6 +26,7 @@ import com.nimbusds.jwt.JWT; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.SignedJWT; import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; import com.nimbusds.jwt.proc.DefaultJWTProcessor; import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; @@ -95,11 +96,26 @@ public NimbusJwtDecoderJwkSupport(String jwkSetUrl, String jwsAlgorithm) { @Override public Jwt decode(String token) throws JwtException { - Jwt jwt; + JWT jwt = this.parse(token); + if ( jwt instanceof SignedJWT ) { + return this.createJwt(token, jwt); + } + + throw new JwtException("Unsupported algorithm of " + jwt.getHeader().getAlgorithm()); + } + private JWT parse(String token) { try { - JWT parsedJwt = JWTParser.parse(token); + return JWTParser.parse(token); + } catch (Exception ex) { + throw new JwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex); + } + } + private Jwt createJwt(String token, JWT parsedJwt) { + Jwt jwt; + + try { // Verify the signature JWTClaimsSet jwtClaimsSet = this.jwtProcessor.process(parsedJwt, null); diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java index 177a52f91c4..37ed64f0bee 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java @@ -20,6 +20,7 @@ import com.nimbusds.jwt.JWT; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.SignedJWT; import com.nimbusds.jwt.proc.DefaultJWTProcessor; import org.junit.Test; import org.junit.runner.RunWith; @@ -29,14 +30,19 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; -import static org.powermock.api.mockito.PowerMockito.*; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.when; +import static org.powermock.api.mockito.PowerMockito.whenNew; /** * Tests for {@link NimbusJwtDecoderJwkSupport}. * * @author Joe Grandja + * @author Josh Cummings */ @RunWith(PowerMockRunner.class) @PrepareForTest({NimbusJwtDecoderJwkSupport.class, JWTParser.class}) @@ -44,6 +50,8 @@ public class NimbusJwtDecoderJwkSupportTests { private static final String JWK_SET_URL = "https://provider.com/oauth2/keys"; private static final String JWS_ALGORITHM = JwsAlgorithms.RS256; + private String unsignedToken = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOi0yMDMzMjI0OTcsImp0aSI6IjEyMyIsInR5cCI6IkpXVCJ9."; + @Test public void constructorWhenJwkSetUrlIsNullThenThrowIllegalArgumentException() { assertThatThrownBy(() -> new NimbusJwtDecoderJwkSupport(null)) @@ -72,7 +80,7 @@ public void decodeWhenJwtInvalidThenThrowJwtException() { // gh-5168 @Test public void decodeWhenExpClaimNullThenDoesNotThrowException() throws Exception { - JWT jwt = mock(JWT.class); + SignedJWT jwt = mock(SignedJWT.class); JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.parse(JWS_ALGORITHM)).build(); when(jwt.getHeader()).thenReturn(header); @@ -88,4 +96,14 @@ public void decodeWhenExpClaimNullThenDoesNotThrowException() throws Exception { NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(JWK_SET_URL, JWS_ALGORITHM); assertThatCode(() -> jwtDecoder.decode("encoded-jwt")).doesNotThrowAnyException(); } + + // gh-5457 + @Test + public void decodeWhenPlainJwtThenExceptionDoesNotMentionClass() throws Exception { + NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(JWK_SET_URL, JWS_ALGORITHM); + + assertThatCode(() -> jwtDecoder.decode(this.unsignedToken)) + .isInstanceOf(JwtException.class) + .hasMessageContaining("Unsupported algorithm of none"); + } } diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java index 3b93935c598..d37e1bdde69 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java @@ -41,6 +41,8 @@ public class NimbusReactiveJwtDecoderTests { private String messageReadToken = "eyJraWQiOiJrZXktaWQtMSIsImFsZyI6IlJTMjU2In0.eyJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6OTIyMzM3MjAwNjA5NjM3NX0.bnQ8IJDXmQbmIXWku0YT1HOyV_3d0iQSA_0W2CmPyELhsxFETzBEEcZ0v0xCBiswDT51rwD83wbX3YXxb84fM64AhpU8wWOxLjha4J6HJX2JnlG47ydaAVD7eWGSYTavyyQ-CwUjQWrfMVcObFZLYG11ydzRYOR9-aiHcK3AobcTcS8jZFeI8EGQV_Cd3IJ018uFCf6VnXLv7eV2kRt08Go2RiPLW47ExvD7Dzzz_wDBKfb4pNem7fDvuzB3UPcp5m9QvLZicnbS_6AvDi6P1y_DFJf-1T5gkGmX5piDH1L1jg2Yl6tjmXbk5B3VhsyjJuXE6gzq1d-xie0Z1NVOxw"; + private String unsignedToken = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOi0yMDMzMjI0OTcsImp0aSI6IjEyMyIsInR5cCI6IkpXVCJ9."; + private String jwkSet = "{\n" + " \"keys\":[\n" @@ -135,4 +137,11 @@ public void decodeWhenInvalidAlgMismatchThenFail() { assertThatCode(() -> this.decoder.decode("ew0KICAiYWxnIjogIkVTMjU2IiwNCiAgInR5cCI6ICJKV1QiDQp9.ew0KICAic3ViIjogIjEyMzQ1Njc4OTAiLA0KICAibmFtZSI6ICJKb2huIERvZSIsDQogICJpYXQiOiAxNTE2MjM5MDIyDQp9.").block()) .isInstanceOf(JwtException.class); } + + @Test + public void decodeWhenUnsignedTokenThenMessageDoesNotMentionClass() { + assertThatCode(() -> this.decoder.decode(this.unsignedToken).block()) + .isInstanceOf(JwtException.class) + .hasMessage("Unsupported algorithm of none"); + } } From 95b29e9b60bfed1996c1a46c5b7f046b9eeb646b Mon Sep 17 00:00:00 2001 From: Mahan Hashemizadeh Date: Wed, 4 Jul 2018 16:25:22 +0200 Subject: [PATCH 101/226] HstsSpec methods return this HstsSpec methods maxAge and includeSubdomains use to return void which broke using it as a fluent API. The methods now return HstsSpec which fixes this issue. Fixes: gh-5483 --- .../security/config/web/server/ServerHttpSecurity.java | 6 ++++-- .../security/config/web/server/HeaderSpecTests.java | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 52edbc37c25..fdacf32ce31 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -1439,16 +1439,18 @@ public class HstsSpec { * Configures the max age. Default is one year. * @param maxAge the max age */ - public void maxAge(Duration maxAge) { + public HstsSpec maxAge(Duration maxAge) { HeaderSpec.this.hsts.setMaxAge(maxAge); + return this; } /** * Configures if subdomains should be included. Default is true * @param includeSubDomains if subdomains should be included */ - public void includeSubdomains(boolean includeSubDomains) { + public HstsSpec includeSubdomains(boolean includeSubDomains) { HeaderSpec.this.hsts.setIncludeSubDomains(includeSubDomains); + return this; } /** diff --git a/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java index 562542477bb..e1b0dfe658c 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java @@ -110,8 +110,9 @@ public void headersWhenHstsDisableThenHstsNotWritten() { public void headersWhenHstsCustomThenCustomHstsWritten() { this.expectedHeaders.remove(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY); this.expectedHeaders.add(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=60"); - this.headers.hsts().maxAge(Duration.ofSeconds(60)); - this.headers.hsts().includeSubdomains(false); + this.headers.hsts() + .maxAge(Duration.ofSeconds(60)) + .includeSubdomains(false); assertHeaders(); } From 4c4f95a4da97356e5e9f495f88f180f9e1b366fc Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 6 Jul 2018 16:35:30 -0500 Subject: [PATCH 102/226] NimbusReactiveJwtDecoder propagates errors looking up keys Fixes: gh-5490 --- .../oauth2/jwt/NimbusReactiveJwtDecoder.java | 3 ++- .../oauth2/jwt/NimbusReactiveJwtDecoderTests.java | 13 ++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java index 4b439f57f5f..67cb0f5e54c 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java @@ -127,9 +127,10 @@ private Mono decode(SignedJWT parsedToken) { JWKSelector selector = this.jwkSelectorFactory .createSelector(parsedToken.getHeader()); return this.reactiveJwkSource.get(selector) + .onErrorMap(e -> new IllegalStateException("Could not obtain the keys", e)) .map(jwkList -> createClaimsSet(parsedToken, jwkList)) .map(set -> createJwt(parsedToken, set)) - .onErrorMap(e -> new JwtException("An error occurred while attempting to decode the Jwt: ", e)); + .onErrorMap(e -> !(e instanceof IllegalStateException), e -> new JwtException("An error occurred while attempting to decode the Jwt: ", e)); } catch (RuntimeException ex) { throw new JwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex); } diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java index d37e1bdde69..2b3ca53adf6 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java @@ -22,6 +22,7 @@ import org.junit.Before; import org.junit.Test; +import java.net.UnknownHostException; import java.security.KeyFactory; import java.security.interfaces.RSAPublicKey; import java.security.spec.X509EncodedKeySpec; @@ -72,6 +73,16 @@ public void cleanup() throws Exception { this.server.shutdown(); } + @Test + public void decodeWhenInvalidUrl() { + this.decoder = new NimbusReactiveJwtDecoder("https://s"); + + assertThatCode(() -> this.decoder.decode(this.messageReadToken).block()) + .isInstanceOf(IllegalStateException.class) + .hasCauseInstanceOf(UnknownHostException.class); + + } + @Test public void decodeWhenMessageReadScopeThenSuccess() { Jwt jwt = this.decoder.decode(this.messageReadToken).block(); @@ -116,7 +127,7 @@ public void decodeWhenNoPeriodThenFail() { public void decodeWhenInvalidJwkSetUrlThenFail() { this.decoder = new NimbusReactiveJwtDecoder("http://localhost:1280/certs"); assertThatCode(() -> this.decoder.decode(this.messageReadToken).block()) - .isInstanceOf(JwtException.class); + .isInstanceOf(IllegalStateException.class); } @Test From cb50da4c37110f2b1ecad64a07c391a4783c0e1a Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Wed, 11 Jul 2018 14:47:31 +0900 Subject: [PATCH 103/226] Polish Javadoc in ServerHttpSecurity --- .../config/web/server/ServerHttpSecurity.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index fdacf32ce31..0e88db487d7 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -210,6 +210,7 @@ public class ServerHttpSecurity { * * @param matcher the ServerExchangeMatcher that determines which requests apply to this HttpSecurity instance. * Default is all requests. + * @return the {@link ServerHttpSecurity} to continue configuring */ public ServerHttpSecurity securityMatcher(ServerWebExchangeMatcher matcher) { Assert.notNull(matcher, "matcher cannot be null"); @@ -743,7 +744,7 @@ public ServerHttpSecurity and() { /** * Disables authorization. - * @return the {@link ServerHttpSecurity} to continue configuring + * @return the {@link Access} to continue configuring */ @Override public Access anyExchange() { @@ -1420,7 +1421,7 @@ public HeaderSpec and() { /** * Disables frame options response header - * @return the {@link ServerHttpSecurity} to continue configuring + * @return the {@link HeaderSpec} to continue configuring */ public HeaderSpec disable() { HeaderSpec.this.writers.remove(HeaderSpec.this.frameOptions); @@ -1438,6 +1439,7 @@ public class HstsSpec { /** * Configures the max age. Default is one year. * @param maxAge the max age + * @return the {@link HstsSpec} to continue configuring */ public HstsSpec maxAge(Duration maxAge) { HeaderSpec.this.hsts.setMaxAge(maxAge); @@ -1447,6 +1449,7 @@ public HstsSpec maxAge(Duration maxAge) { /** * Configures if subdomains should be included. Default is true * @param includeSubDomains if subdomains should be included + * @return the {@link HstsSpec} to continue configuring */ public HstsSpec includeSubdomains(boolean includeSubDomains) { HeaderSpec.this.hsts.setIncludeSubDomains(includeSubDomains); @@ -1463,7 +1466,7 @@ public HeaderSpec and() { /** * Disables strict transport security response header - * @return the {@link ServerHttpSecurity} to continue configuring + * @return the {@link HeaderSpec} to continue configuring */ public HeaderSpec disable() { HeaderSpec.this.writers.remove(HeaderSpec.this.hsts); @@ -1480,7 +1483,7 @@ private HstsSpec() {} public class XssProtectionSpec { /** * Disables the x-xss-protection response header - * @return + * @return the {@link HeaderSpec} to continue configuring */ public HeaderSpec disable() { HeaderSpec.this.writers.remove(HeaderSpec.this.xss); @@ -1509,7 +1512,7 @@ public final class LogoutSpec { /** * Configures the logout handler. Default is {@code SecurityContextServerLogoutHandler} * @param logoutHandler - * @return + * @return the {@link LogoutSpec} to configure */ public LogoutSpec logoutHandler(ServerLogoutHandler logoutHandler) { this.logoutWebFilter.setLogoutHandler(logoutHandler); From 87dd02e282f2e36f3da1dbf1408986566fa482aa Mon Sep 17 00:00:00 2001 From: "mhyeon.lee" Date: Fri, 6 Jul 2018 18:35:16 +0900 Subject: [PATCH 104/226] Fix oauth2login loginProcessingUrl NPE for java config Java Config http.oauth2Login().loginProcessingUrl("url"); throws NPE. Override loginProcessingUrl method and cached config url. Then when the config is initialized, it calls the super method to complete the configuration. Fixes gh-5488 --- .../oauth2/client/OAuth2LoginConfigurer.java | 14 +++++-- .../client/OAuth2LoginConfigurerTests.java | 41 +++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java index 972afc1443a..55ca29413e4 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -124,6 +124,7 @@ public final class OAuth2LoginConfigurer> exten private final RedirectionEndpointConfig redirectionEndpointConfig = new RedirectionEndpointConfig(); private final UserInfoEndpointConfig userInfoEndpointConfig = new UserInfoEndpointConfig(); private String loginPage; + private String loginProcessingUrl = OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI; /** * Sets the repository of client registrations. @@ -156,6 +157,13 @@ public OAuth2LoginConfigurer loginPage(String loginPage) { return this; } + @Override + public OAuth2LoginConfigurer loginProcessingUrl(String loginProcessingUrl) { + Assert.hasText(loginProcessingUrl, "loginProcessingUrl cannot be empty"); + this.loginProcessingUrl = loginProcessingUrl; + return this; + } + /** * Returns the {@link AuthorizationEndpointConfig} for configuring the Authorization Server's Authorization Endpoint. * @@ -378,9 +386,9 @@ public void init(B http) throws Exception { new OAuth2LoginAuthenticationFilter( OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()), OAuth2ClientConfigurerUtils.getAuthorizedClientService(this.getBuilder()), - OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI); + this.loginProcessingUrl); this.setAuthenticationFilter(authenticationFilter); - this.loginProcessingUrl(OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI); + super.loginProcessingUrl(this.loginProcessingUrl); if (this.loginPage != null) { super.loginPage(this.loginPage); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java index ba66ece9b48..b0dc626f42a 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java @@ -197,6 +197,34 @@ public void oauth2LoginCustomWithBeanRegistration() throws Exception { assertThat(authentication.getAuthorities()).last().hasToString("ROLE_OAUTH2_USER"); } + // gh-5488 + @Test + public void oauth2LoginConfigLoginProcessingUrl() throws Exception { + // setup application context + loadConfig(OAuth2LoginConfigLoginProcessingUrl.class); + + // setup authorization request + OAuth2AuthorizationRequest authorizationRequest = createOAuth2AuthorizationRequest(); + this.request.setServletPath("/login/oauth2/google"); + this.authorizationRequestRepository.saveAuthorizationRequest( + authorizationRequest, this.request, this.response); + + // setup authentication parameters + this.request.setParameter("code", "code123"); + this.request.setParameter("state", authorizationRequest.getState()); + + // perform test + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + + // assertions + Authentication authentication = this.securityContextRepository + .loadContext(new HttpRequestResponseHolder(this.request, this.response)) + .getAuthentication(); + assertThat(authentication.getAuthorities()).hasSize(1); + assertThat(authentication.getAuthorities()).first() + .isInstanceOf(OAuth2UserAuthority.class).hasToString("ROLE_USER"); + } + @Test public void oidcLogin() throws Exception { // setup application context @@ -365,6 +393,19 @@ GrantedAuthoritiesMapper grantedAuthoritiesMapper() { } } + @EnableWebSecurity + static class OAuth2LoginConfigLoginProcessingUrl extends CommonWebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .oauth2Login() + .clientRegistrationRepository( + new InMemoryClientRegistrationRepository(CLIENT_REGISTRATION)) + .loginProcessingUrl("/login/oauth2/*"); + super.configure(http); + } + } + private static abstract class CommonWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { From 1a0866be3752251312742adc80bc25dbdebd3cae Mon Sep 17 00:00:00 2001 From: "mhyeon.lee" Date: Tue, 10 Jul 2018 17:17:21 +0900 Subject: [PATCH 105/226] Enhance OAuth2AccessToken to be serializable Change the TokenType to Serializable so that the OAuth2AccessToken can be serialized. (org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType) Fixes gh-5492 --- .../oauth2/core/OAuth2AccessToken.java | 7 +++++-- .../oauth2/core/OAuth2AccessTokenTests.java | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java index 20a3014e911..8e18df55a12 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,10 @@ */ package org.springframework.security.oauth2.core; +import org.springframework.security.core.SpringSecurityCoreVersion; import org.springframework.util.Assert; +import java.io.Serializable; import java.time.Instant; import java.util.Collections; import java.util.Set; @@ -90,7 +92,8 @@ public Set getScopes() { * * @see Section 7.1 Access Token Types */ - public static final class TokenType { + public static final class TokenType implements Serializable { + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; public static final TokenType BEARER = new TokenType("Bearer"); private final String value; diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/OAuth2AccessTokenTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/OAuth2AccessTokenTests.java index 7745856fde6..4309cbb6450 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/OAuth2AccessTokenTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/OAuth2AccessTokenTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.core; import org.junit.Test; +import org.springframework.util.SerializationUtils; import java.time.Instant; import java.util.Arrays; @@ -72,4 +73,20 @@ public void constructorWhenAllParametersProvidedAndValidThenCreated() { assertThat(accessToken.getExpiresAt()).isEqualTo(EXPIRES_AT); assertThat(accessToken.getScopes()).isEqualTo(SCOPES); } + + // gh-5492 + @Test + public void constructorWhenCreatedThenIsSerializableAndDeserializable() { + OAuth2AccessToken accessToken = new OAuth2AccessToken( + TOKEN_TYPE, TOKEN_VALUE, ISSUED_AT, EXPIRES_AT, SCOPES); + byte[] serialized = SerializationUtils.serialize(accessToken); + accessToken = (OAuth2AccessToken) SerializationUtils.deserialize(serialized); + + assertThat(serialized).isNotNull(); + assertThat(accessToken.getTokenType()).isEqualTo(TOKEN_TYPE); + assertThat(accessToken.getTokenValue()).isEqualTo(TOKEN_VALUE); + assertThat(accessToken.getIssuedAt()).isEqualTo(ISSUED_AT); + assertThat(accessToken.getExpiresAt()).isEqualTo(EXPIRES_AT); + assertThat(accessToken.getScopes()).isEqualTo(SCOPES); + } } From e852c2a8868a1fd346bd38ecec5b7a499f25cb3f Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 13 Jul 2018 15:57:59 -0500 Subject: [PATCH 106/226] Add JdbcUserDetailsManager(DataSource) constructor Fixes: gh-5512 --- .../security/provisioning/JdbcUserDetailsManager.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/src/main/java/org/springframework/security/provisioning/JdbcUserDetailsManager.java b/core/src/main/java/org/springframework/security/provisioning/JdbcUserDetailsManager.java index 7dbe8b61b4f..3857c1f09fe 100644 --- a/core/src/main/java/org/springframework/security/provisioning/JdbcUserDetailsManager.java +++ b/core/src/main/java/org/springframework/security/provisioning/JdbcUserDetailsManager.java @@ -37,6 +37,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import javax.sql.DataSource; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -122,6 +123,13 @@ public class JdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsMa private UserCache userCache = new NullUserCache(); + public JdbcUserDetailsManager() { + } + + public JdbcUserDetailsManager(DataSource dataSource) { + setDataSource(dataSource); + } + // ~ Methods // ======================================================================================================== From 5e96544cecbc8a746ddd241185aee19c02a9cf8e Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 13 Jul 2018 21:17:23 -0500 Subject: [PATCH 107/226] Jenkinsfile add --refresh-dependencies JDK specific builds --- Jenkinsfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 9ff67dd8f0a..b4a7554c829 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -58,7 +58,7 @@ try { checkout scm try { withEnv(["JAVA_HOME=${ tool 'jdk9' }"]) { - sh "./gradlew clean test --no-daemon --stacktrace" + sh "./gradlew clean test --refresh-dependencies --no-daemon --stacktrace" } } catch(Exception e) { currentBuild.result = 'FAILED: jdk9' @@ -73,7 +73,7 @@ try { checkout scm try { withEnv(["JAVA_HOME=${ tool 'jdk10' }"]) { - sh "./gradlew clean test --no-daemon --stacktrace" + sh "./gradlew clean test --refresh-dependencies --no-daemon --stacktrace" } } catch(Exception e) { currentBuild.result = 'FAILED: jdk10' @@ -88,7 +88,7 @@ try { checkout scm try { withEnv(["JAVA_HOME=${ tool 'jdk11' }"]) { - sh "./gradlew clean test --no-daemon --stacktrace" + sh "./gradlew clean test --refresh-dependencies --no-daemon --stacktrace" } } catch(Exception e) { currentBuild.result = 'FAILED: jdk11' From 8e81fcdc32bfd6dc1b8bb687299f3f086fb34b00 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Sat, 14 Jul 2018 21:05:48 -0500 Subject: [PATCH 108/226] Spring Version null for NullPointerException --- .../security/core/SpringSecurityCoreVersion.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java b/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java index ad007dbe19f..6e5debaac78 100644 --- a/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java +++ b/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java @@ -110,7 +110,7 @@ private static String getSpringVersion() { Properties properties = new Properties(); try { properties.load(SpringSecurityCoreVersion.class.getClassLoader().getResourceAsStream("META-INF/spring-security.versions")); - } catch (IOException e) { + } catch (IOException | NullPointerException e) { return null; } return properties.getProperty("org.springframework:spring-core"); From bd7cd723ca74a27b620c15ff8662b288058585f9 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Sat, 14 Jul 2018 16:58:07 -0500 Subject: [PATCH 109/226] Add scripts/s101.sh --- scripts/s101.sh | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100755 scripts/s101.sh diff --git a/scripts/s101.sh b/scripts/s101.sh new file mode 100755 index 00000000000..78f826d7e3a --- /dev/null +++ b/scripts/s101.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +./gradlew jar +mkdir -p build/s101 && cd $_ +rm *.jar +find ../../ -name '*-SNAPSHOT.jar' | grep -v samples | grep -v itest | xargs -I{} cp {} . From bae14360e62dc7d084e15597b78e840fa084ffcc Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 13 Jul 2018 21:12:13 -0500 Subject: [PATCH 110/226] Add PasswordEncoder.upgradeEncoding Issue: gh-2778 --- .../AuthenticationConfiguration.java | 5 ++++ .../WebSecurityConfigurerAdapter.java | 5 ++++ .../password/DelegatingPasswordEncoder.java | 6 +++++ .../crypto/password/PasswordEncoder.java | 10 ++++++++ .../DelegatingPasswordEncoderTests.java | 25 +++++++++++++++++++ 5 files changed, 51 insertions(+) diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java index 9ac13368577..11dc6c4c7c1 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java @@ -289,6 +289,11 @@ public boolean matches(CharSequence rawPassword, return getPasswordEncoder().matches(rawPassword, encodedPassword); } + @Override + public boolean upgradeEncoding(String encodedPassword) { + return getPasswordEncoder().upgradeEncoding(encodedPassword); + } + private PasswordEncoder getPasswordEncoder() { if (this.passwordEncoder != null) { return this.passwordEncoder; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java index c75b94ccb5d..d8c14ff9ccb 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java @@ -593,6 +593,11 @@ public boolean matches(CharSequence rawPassword, return getPasswordEncoder().matches(rawPassword, encodedPassword); } + @Override + public boolean upgradeEncoding(String encodedPassword) { + return getPasswordEncoder().upgradeEncoding(encodedPassword); + } + private PasswordEncoder getPasswordEncoder() { if (this.passwordEncoder != null) { return this.passwordEncoder; diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java index b6738db848f..8f68bb80fb4 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java @@ -216,6 +216,12 @@ private String extractId(String prefixEncodedPassword) { return prefixEncodedPassword.substring(start + 1, end); } + @Override + public boolean upgradeEncoding(String encodedPassword) { + String id = extractId(encodedPassword); + return !this.idForEncode.equalsIgnoreCase(id); + } + private String extractEncodedPassword(String prefixEncodedPassword) { int start = prefixEncodedPassword.indexOf(SUFFIX); return prefixEncodedPassword.substring(start + 1); diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoder.java index 2b31acd8c42..4e77d3b19b1 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoder.java @@ -42,4 +42,14 @@ public interface PasswordEncoder { */ boolean matches(CharSequence rawPassword, String encodedPassword); + /** + * Returns true if the encoded password should be encoded again for better security, + * else false. The default implementation always returns false. + * @param encodedPassword the encoded password to check + * @return true if the encoded password should be encoded again for better security, + * else false. + */ + default boolean upgradeEncoding(String encodedPassword) { + return false; + } } diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java index d5deea80047..59a88de50f5 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java @@ -198,4 +198,29 @@ public void matchesWhenNullIdThenDelegatesToInvalidId() { public void matchesWhenRawPasswordNotNullAndEncodedPasswordNullThenThrowsIllegalArgumentException() { this.passwordEncoder.matches(this.rawPassword, null); } + + @Test + public void upgradeEncodingWhenEncodedPasswordNullThenTrue() { + assertThat(this.passwordEncoder.upgradeEncoding(null)).isTrue(); + } + + @Test + public void upgradeEncodingWhenNullIdThenTrue() { + assertThat(this.passwordEncoder.upgradeEncoding(this.encodedPassword)).isTrue(); + } + + @Test + public void upgradeEncodingWhenIdInvalidFormatThenTrue() { + assertThat(this.passwordEncoder.upgradeEncoding("{bcrypt"+ this.encodedPassword)).isTrue(); + } + + @Test + public void upgradeEncodingWhenSameIdThenFalse() { + assertThat(this.passwordEncoder.upgradeEncoding(this.bcryptEncodedPassword)).isFalse(); + } + + @Test + public void upgradeEncodingWhenDifferentIdThenTrue() { + assertThat(this.passwordEncoder.upgradeEncoding(this.noopEncodedPassword)).isTrue(); + } } From 66145c04fb41de9b80bba63614ac2fcba5c639d7 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Sat, 14 Jul 2018 16:59:39 -0500 Subject: [PATCH 111/226] UserDetailsPasswordService Issue: gh-2778 --- .../UserDetailsPasswordService.java | 35 ++++++++++++++++ .../InMemoryUserDetailsManager.java | 12 +++++- .../InMemoryUserDetailsManagerTests.java | 40 +++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/org/springframework/security/core/userdetails/UserDetailsPasswordService.java create mode 100644 core/src/test/java/org/springframework/security/provisioning/InMemoryUserDetailsManagerTests.java diff --git a/core/src/main/java/org/springframework/security/core/userdetails/UserDetailsPasswordService.java b/core/src/main/java/org/springframework/security/core/userdetails/UserDetailsPasswordService.java new file mode 100644 index 00000000000..a282db973d4 --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/userdetails/UserDetailsPasswordService.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.core.userdetails; + +/** + * An API for changing a {@link UserDetails} password. + * @author Rob Winch + * @since 5.1 + */ +public interface UserDetailsPasswordService { + + /** + * Modify the specified user's password. This should change the user's password in the + * persistent user repository (datbase, LDAP etc). + * + * @param user the user to modify the password for + * @param newPassword the password to change to + * @return the updated UserDetails with the new password + */ + UserDetails updatePassword(UserDetails user, String newPassword); +} diff --git a/core/src/main/java/org/springframework/security/provisioning/InMemoryUserDetailsManager.java b/core/src/main/java/org/springframework/security/provisioning/InMemoryUserDetailsManager.java index 6592d63b40f..55c462cde41 100644 --- a/core/src/main/java/org/springframework/security/provisioning/InMemoryUserDetailsManager.java +++ b/core/src/main/java/org/springframework/security/provisioning/InMemoryUserDetailsManager.java @@ -30,6 +30,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsPasswordService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.memory.UserAttribute; import org.springframework.security.core.userdetails.memory.UserAttributeEditor; @@ -45,7 +46,8 @@ * @author Luke Taylor * @since 3.1 */ -public class InMemoryUserDetailsManager implements UserDetailsManager { +public class InMemoryUserDetailsManager implements UserDetailsManager, + UserDetailsPasswordService { protected final Log logger = LogFactory.getLog(getClass()); private final Map users = new HashMap<>(); @@ -138,6 +140,14 @@ public void changePassword(String oldPassword, String newPassword) { user.setPassword(newPassword); } + @Override + public UserDetails updatePassword(UserDetails user, String newPassword) { + String username = user.getUsername(); + MutableUserDetails mutableUser = this.users.get(username); + mutableUser.setPassword(newPassword); + return mutableUser; + } + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserDetails user = users.get(username.toLowerCase()); diff --git a/core/src/test/java/org/springframework/security/provisioning/InMemoryUserDetailsManagerTests.java b/core/src/test/java/org/springframework/security/provisioning/InMemoryUserDetailsManagerTests.java new file mode 100644 index 00000000000..9f58cc5385f --- /dev/null +++ b/core/src/test/java/org/springframework/security/provisioning/InMemoryUserDetailsManagerTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.provisioning; + +import org.junit.Test; +import org.springframework.security.core.userdetails.PasswordEncodedUser; +import org.springframework.security.core.userdetails.UserDetails; + +import static org.assertj.core.api.Assertions.*; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class InMemoryUserDetailsManagerTests { + private final UserDetails user = PasswordEncodedUser.user(); + + private InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(this.user); + + @Test + public void changePassword() { + String newPassword = "newPassword"; + this.manager.updatePassword(this.user, newPassword); + assertThat(this.manager.loadUserByUsername(this.user.getUsername()).getPassword()).isEqualTo(newPassword); + } +} From 2099b9735dd90d40126ac5b07eb7b0b2e9274043 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Sat, 14 Jul 2018 17:01:43 -0500 Subject: [PATCH 112/226] DaoAuthenticationProvider supports password upgrades Issue: gh-2778 --- .../dao/DaoAuthenticationProvider.java | 22 +++++ .../dao/DaoAuthenticationProviderTests.java | 80 +++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/core/src/main/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.java b/core/src/main/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.java index 07c34e0a934..0c4e86b574a 100644 --- a/core/src/main/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.java +++ b/core/src/main/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.java @@ -20,12 +20,14 @@ import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.InternalAuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.core.userdetails.UserDetailsPasswordService; import org.springframework.util.Assert; /** @@ -62,6 +64,8 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication private UserDetailsService userDetailsService; + private UserDetailsPasswordService userDetailsPasswordService; + public DaoAuthenticationProvider() { setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()); } @@ -120,6 +124,19 @@ protected final UserDetails retrieveUser(String username, } } + @Override + protected Authentication createSuccessAuthentication(Object principal, + Authentication authentication, UserDetails user) { + boolean upgradeEncoding = this.userDetailsPasswordService != null + && this.passwordEncoder.upgradeEncoding(user.getPassword()); + if (upgradeEncoding) { + String presentedPassword = authentication.getCredentials().toString(); + String newPassword = this.passwordEncoder.encode(presentedPassword); + user = this.userDetailsPasswordService.updatePassword(user, newPassword); + } + return super.createSuccessAuthentication(principal, authentication, user); + } + private void prepareTimingAttackProtection() { if (this.userNotFoundEncodedPassword == null) { this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD); @@ -157,4 +174,9 @@ public void setUserDetailsService(UserDetailsService userDetailsService) { protected UserDetailsService getUserDetailsService() { return userDetailsService; } + + public void setUserDetailsPasswordService( + UserDetailsPasswordService userDetailsPasswordService) { + this.userDetailsPasswordService = userDetailsPasswordService; + } } diff --git a/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java b/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java index 094c79ebee9..5f7c6e0a8d4 100644 --- a/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java +++ b/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java @@ -17,12 +17,16 @@ package org.springframework.security.authentication.dao; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import java.security.SecureRandom; @@ -43,6 +47,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.PasswordEncodedUser; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -53,6 +58,7 @@ import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.core.userdetails.UserDetailsPasswordService; /** * Tests {@link DaoAuthenticationProvider}. @@ -399,6 +405,80 @@ public void testAuthenticatesWithForcePrincipalAsString() { assertThat(castResult.getPrincipal()).isEqualTo("rod"); } + @Test + public void authenticateWhenSuccessAndPasswordManagerThenUpdates() { + String password = "password"; + String encodedPassword = "encoded"; + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + "user", password); + + PasswordEncoder encoder = mock(PasswordEncoder.class); + UserDetailsService userDetailsService = mock(UserDetailsService.class); + UserDetailsPasswordService passwordManager = mock(UserDetailsPasswordService.class); + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setPasswordEncoder(encoder); + provider.setUserDetailsService(userDetailsService); + provider.setUserDetailsPasswordService(passwordManager); + + UserDetails user = PasswordEncodedUser.user(); + when(encoder.matches(any(), any())).thenReturn(true); + when(encoder.upgradeEncoding(any())).thenReturn(true); + when(encoder.encode(any())).thenReturn(encodedPassword); + when(userDetailsService.loadUserByUsername(any())).thenReturn(user); + when(passwordManager.updatePassword(any(), any())).thenReturn(user); + + Authentication result = provider.authenticate(token); + + verify(encoder).encode(password); + verify(passwordManager).updatePassword(eq(user), eq(encodedPassword)); + } + + @Test + public void authenticateWhenBadCredentialsAndPasswordManagerThenNoUpdate() { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + "user", "password"); + + PasswordEncoder encoder = mock(PasswordEncoder.class); + UserDetailsService userDetailsService = mock(UserDetailsService.class); + UserDetailsPasswordService passwordManager = mock(UserDetailsPasswordService.class); + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setPasswordEncoder(encoder); + provider.setUserDetailsService(userDetailsService); + provider.setUserDetailsPasswordService(passwordManager); + + UserDetails user = PasswordEncodedUser.user(); + when(encoder.matches(any(), any())).thenReturn(false); + when(userDetailsService.loadUserByUsername(any())).thenReturn(user); + + assertThatThrownBy(() -> provider.authenticate(token)) + .isInstanceOf(BadCredentialsException.class); + + verifyZeroInteractions(passwordManager); + } + + @Test + public void authenticateWhenNotUpgradeAndPasswordManagerThenNoUpdate() { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + "user", "password"); + + PasswordEncoder encoder = mock(PasswordEncoder.class); + UserDetailsService userDetailsService = mock(UserDetailsService.class); + UserDetailsPasswordService passwordManager = mock(UserDetailsPasswordService.class); + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setPasswordEncoder(encoder); + provider.setUserDetailsService(userDetailsService); + provider.setUserDetailsPasswordService(passwordManager); + + UserDetails user = PasswordEncodedUser.user(); + when(encoder.matches(any(), any())).thenReturn(true); + when(encoder.upgradeEncoding(any())).thenReturn(false); + when(userDetailsService.loadUserByUsername(any())).thenReturn(user); + + Authentication result = provider.authenticate(token); + + verifyZeroInteractions(passwordManager); + } + @Test public void testDetectsNullBeingReturnedFromAuthenticationDao() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( From fece4be7844bce2534425d4c9d565f31f09bf56c Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Sat, 14 Jul 2018 21:13:39 -0500 Subject: [PATCH 113/226] Configuration Support for UserDetailsPasswordManager Issue: gh-2778 --- ...alizeUserDetailsBeanManagerConfigurer.java | 7 ++- .../AbstractDaoAuthenticationConfigurer.java | 9 +++ .../AuthenticationConfigurationTests.java | 32 ++++++++++ .../WebSecurityConfigurerAdapterTests.java | 60 ++++++++++++++++++- .../reactive/EnableWebFluxSecurityTests.java | 17 +++--- 5 files changed, 114 insertions(+), 11 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java index 54e9ac4d0c5..db4d2968dd9 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java @@ -22,6 +22,7 @@ import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.core.userdetails.UserDetailsPasswordService; /** * Lazily initializes the global authentication with a {@link UserDetailsService} if it is @@ -65,12 +66,16 @@ public void configure(AuthenticationManagerBuilder auth) throws Exception { } PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class); + UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(userDetailsService); if (passwordEncoder != null) { provider.setPasswordEncoder(passwordEncoder); } + if (passwordManager != null) { + provider.setUserDetailsPasswordService(passwordManager); + } provider.afterPropertiesSet(); auth.authenticationProvider(provider); @@ -90,4 +95,4 @@ private T getBeanOrNull(Class type) { .getBean(userDetailsBeanNames[0], type); } } -} \ No newline at end of file +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java index 54133e6a304..52cf21c17cf 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java @@ -21,6 +21,7 @@ import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.core.userdetails.UserDetailsPasswordService; /** * Allows configuring a {@link DaoAuthenticationProvider} @@ -46,6 +47,9 @@ abstract class AbstractDaoAuthenticationConfigurer Date: Sat, 14 Jul 2018 23:02:07 -0500 Subject: [PATCH 114/226] ReactiveUserDetailsPasswordService Issue: gh-2778 --- .../MapReactiveUserDetailsService.java | 16 +++++++- .../ReactiveUserDetailsPasswordService.java | 37 +++++++++++++++++++ .../MapReactiveUserDetailsServiceTests.java | 6 +++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/org/springframework/security/core/userdetails/ReactiveUserDetailsPasswordService.java diff --git a/core/src/main/java/org/springframework/security/core/userdetails/MapReactiveUserDetailsService.java b/core/src/main/java/org/springframework/security/core/userdetails/MapReactiveUserDetailsService.java index ce44d886cd0..45923a1e9f0 100644 --- a/core/src/main/java/org/springframework/security/core/userdetails/MapReactiveUserDetailsService.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/MapReactiveUserDetailsService.java @@ -31,7 +31,7 @@ * @author Rob Winch * @since 5.0 */ -public class MapReactiveUserDetailsService implements ReactiveUserDetailsService { +public class MapReactiveUserDetailsService implements ReactiveUserDetailsService, ReactiveUserDetailsPasswordService { private final Map users; /** @@ -66,6 +66,20 @@ public Mono findByUsername(String username) { return result == null ? Mono.empty() : Mono.just(User.withUserDetails(result).build()); } + @Override + public Mono updatePassword(UserDetails user, String newPassword) { + return Mono.just(user) + .map(u -> + User.withUserDetails(u) + .password(newPassword) + .build() + ) + .doOnNext(u -> { + String key = getKey(user.getUsername()); + this.users.put(key, u); + }); + } + private String getKey(String username) { return username.toLowerCase(); } diff --git a/core/src/main/java/org/springframework/security/core/userdetails/ReactiveUserDetailsPasswordService.java b/core/src/main/java/org/springframework/security/core/userdetails/ReactiveUserDetailsPasswordService.java new file mode 100644 index 00000000000..1dcadf719a6 --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/userdetails/ReactiveUserDetailsPasswordService.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.core.userdetails; + +import reactor.core.publisher.Mono; + +/** + * An API for changing a {@link UserDetails} password. + * @author Rob Winch + * @since 5.1 + */ +public interface ReactiveUserDetailsPasswordService { + + /** + * Modify the specified user's password. This should change the user's password in the + * persistent user repository (datbase, LDAP etc). + * + * @param user the user to modify the password for + * @param newPassword the password to change to + * @return the updated UserDetails with the new password + */ + Mono updatePassword(UserDetails user, String newPassword); +} diff --git a/core/src/test/java/org/springframework/security/core/userdetails/MapReactiveUserDetailsServiceTests.java b/core/src/test/java/org/springframework/security/core/userdetails/MapReactiveUserDetailsServiceTests.java index 5c8b701507f..186c522beac 100644 --- a/core/src/test/java/org/springframework/security/core/userdetails/MapReactiveUserDetailsServiceTests.java +++ b/core/src/test/java/org/springframework/security/core/userdetails/MapReactiveUserDetailsServiceTests.java @@ -71,4 +71,10 @@ public void findByUsernameWhenClearCredentialsThenFindByUsernameStillHasCredenti public void findByUsernameWhenNotFoundThenEmpty() { assertThat((users.findByUsername("notfound"))).isEqualTo(Mono.empty()); } + + @Test + public void updatePassword() { + users.updatePassword(USER_DETAILS, "new").block(); + assertThat(users.findByUsername(USER_DETAILS.getUsername()).block().getPassword()).isEqualTo("new"); + } } From dcfae12c8fa2a94aab0cb68ce5f95c3e9ac6971e Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Sat, 14 Jul 2018 23:28:53 -0500 Subject: [PATCH 115/226] UserDetailsRepositoryReactiveAuthenticationManager uses ReactiveUserDetailsPasswordService Issue: gh-2778 --- ...positoryReactiveAuthenticationManager.java | 27 ++++++++- ...oryReactiveAuthenticationManagerTests.java | 59 ++++++++++++++++++- 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java b/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java index c60ea15f5c5..7e67677eb44 100644 --- a/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java +++ b/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java @@ -18,7 +18,9 @@ import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.util.Assert; @@ -38,6 +40,8 @@ public class UserDetailsRepositoryReactiveAuthenticationManager implements React private PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); + private ReactiveUserDetailsPasswordService userDetailsPasswordService; + private Scheduler scheduler = Schedulers.parallel(); public UserDetailsRepositoryReactiveAuthenticationManager(ReactiveUserDetailsService userDetailsService) { @@ -48,11 +52,21 @@ public UserDetailsRepositoryReactiveAuthenticationManager(ReactiveUserDetailsSer @Override public Mono authenticate(Authentication authentication) { final String username = authentication.getName(); + final String presentedPassword = (String) authentication.getCredentials(); return this.userDetailsService.findByUsername(username) .publishOn(this.scheduler) - .filter( u -> this.passwordEncoder.matches((String) authentication.getCredentials(), u.getPassword())) + .filter(u -> this.passwordEncoder.matches(presentedPassword, u.getPassword())) .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials")))) - .map( u -> new UsernamePasswordAuthenticationToken(u, u.getPassword(), u.getAuthorities()) ); + .flatMap(u -> { + boolean upgradeEncoding = this.userDetailsPasswordService != null + && this.passwordEncoder.upgradeEncoding(u.getPassword()); + if (upgradeEncoding) { + String newPassword = this.passwordEncoder.encode(presentedPassword); + return this.userDetailsPasswordService.updatePassword(u, newPassword); + } + return Mono.just(u); + }) + .map(u -> new UsernamePasswordAuthenticationToken(u, u.getPassword(), u.getAuthorities()) ); } /** @@ -80,4 +94,13 @@ public void setScheduler(Scheduler scheduler) { Assert.notNull(scheduler, "scheduler cannot be null"); this.scheduler = scheduler; } + + /** + * Sets the service to use for upgrading passwords on successful authentication. + * @param userDetailsPasswordService the service to use + */ + public void setUserDetailsPasswordService( + ReactiveUserDetailsPasswordService userDetailsPasswordService) { + this.userDetailsPasswordService = userDetailsPasswordService; + } } diff --git a/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java b/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java index 238c075fc1a..4de034dcb5b 100644 --- a/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java @@ -22,6 +22,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; @@ -32,7 +33,9 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; /** @@ -47,6 +50,9 @@ public class UserDetailsRepositoryReactiveAuthenticationManagerTests { @Mock private PasswordEncoder encoder; + @Mock + private ReactiveUserDetailsPasswordService userDetailsPasswordService; + @Mock private Scheduler scheduler; @@ -79,10 +85,61 @@ public void authentiateWhenCustomSchedulerThenUsed() { this.manager.setScheduler(this.scheduler); this.manager.setPasswordEncoder(this.encoder); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( - this.user, this.user.getPassword()); + this.user, this.user.getPassword()); Authentication result = this.manager.authenticate(token).block(); verify(this.scheduler).schedule(any()); } + + @Test + public void authenticateWhenPasswordServiceThenUpdated() { + String encodedPassword = "encoded"; + when(this.userDetailsService.findByUsername(any())).thenReturn(Mono.just(this.user)); + when(this.encoder.matches(any(), any())).thenReturn(true); + when(this.encoder.upgradeEncoding(any())).thenReturn(true); + when(this.encoder.encode(any())).thenReturn(encodedPassword); + when(this.userDetailsPasswordService.updatePassword(any(), any())).thenReturn(Mono.just(this.user)); + this.manager.setPasswordEncoder(this.encoder); + this.manager.setUserDetailsPasswordService(this.userDetailsPasswordService); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + this.user, this.user.getPassword()); + + Authentication result = this.manager.authenticate(token).block(); + + verify(this.encoder).encode(this.user.getPassword()); + verify(this.userDetailsPasswordService).updatePassword(eq(this.user), eq(encodedPassword)); + } + + @Test + public void authenticateWhenPasswordServiceAndBadCredentialsThenNotUpdated() { + when(this.userDetailsService.findByUsername(any())).thenReturn(Mono.just(this.user)); + when(this.encoder.matches(any(), any())).thenReturn(false); + when(this.userDetailsPasswordService.updatePassword(any(), any())).thenReturn(Mono.just(this.user)); + this.manager.setPasswordEncoder(this.encoder); + this.manager.setUserDetailsPasswordService(this.userDetailsPasswordService); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + this.user, this.user.getPassword()); + + assertThatThrownBy(() -> this.manager.authenticate(token).block()) + .isInstanceOf(BadCredentialsException.class); + + verifyZeroInteractions(this.userDetailsPasswordService); + } + + @Test + public void authenticateWhenPasswordServiceAndUpgradeFalseThenNotUpdated() { + when(this.userDetailsService.findByUsername(any())).thenReturn(Mono.just(this.user)); + when(this.encoder.matches(any(), any())).thenReturn(true); + when(this.encoder.upgradeEncoding(any())).thenReturn(false); + when(this.userDetailsPasswordService.updatePassword(any(), any())).thenReturn(Mono.just(this.user)); + this.manager.setPasswordEncoder(this.encoder); + this.manager.setUserDetailsPasswordService(this.userDetailsPasswordService); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + this.user, this.user.getPassword()); + + Authentication result = this.manager.authenticate(token).block(); + + verifyZeroInteractions(this.userDetailsPasswordService); + } } From 89a911a289496ea7fb26ff1d3f4676ad71ce911a Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Sat, 14 Jul 2018 23:38:05 -0500 Subject: [PATCH 116/226] Configuration for ReactiveUserDetailsPasswordService Issue: gh-2778 --- .../ServerHttpSecurityConfiguration.java | 5 ++++ .../reactive/EnableWebFluxSecurityTests.java | 28 +++++++++++++++++++ ...positoryReactiveAuthenticationManager.java | 1 - ...oryReactiveAuthenticationManagerTests.java | 2 -- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java index 8e069900924..2596373e5fe 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java @@ -28,6 +28,7 @@ import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager; import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.reactive.result.method.annotation.AuthenticationPrincipalArgumentResolver; @@ -54,6 +55,9 @@ class ServerHttpSecurityConfiguration implements WebFluxConfigurer { @Autowired(required = false) private PasswordEncoder passwordEncoder; + @Autowired(required = false) + private ReactiveUserDetailsPasswordService userDetailsPasswordService; + @Autowired(required = false) private BeanFactory beanFactory; @@ -92,6 +96,7 @@ private ReactiveAuthenticationManager authenticationManager() { if(this.passwordEncoder != null) { manager.setPasswordEncoder(this.passwordEncoder); } + manager.setUserDetailsPasswordService(this.userDetailsPasswordService); return manager; } return null; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java index 60ce3aaffc3..7d6ee069193 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java @@ -237,6 +237,34 @@ public static PasswordEncoder passwordEncoder() { } } + @Test + public void passwordUpdateManagerUsed() { + this.spring.register(MapReactiveUserDetailsServiceConfig.class).autowire(); + WebTestClient client = WebTestClientBuilder.bindToWebFilters(this.springSecurityFilterChain).build(); + + client + .get() + .uri("/") + .headers(h -> h.setBasicAuth("user", "password")) + .exchange() + .expectStatus().isOk(); + + ReactiveUserDetailsService users = this.spring.getContext().getBean(ReactiveUserDetailsService.class); + assertThat(users.findByUsername("user").block().getPassword()).startsWith("{bcrypt}"); + } + + @EnableWebFluxSecurity + static class MapReactiveUserDetailsServiceConfig { + @Bean + public MapReactiveUserDetailsService userDetailsService() { + return new MapReactiveUserDetailsService(User.withUsername("user") + .password("{noop}password") + .roles("USER") + .build() + ); + } + } + @Test public void formLoginWorks() { this.spring.register(Config.class).autowire(); diff --git a/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java b/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java index 7e67677eb44..0a79e6b2446 100644 --- a/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java +++ b/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java @@ -20,7 +20,6 @@ import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; -import org.springframework.security.core.userdetails.User; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.util.Assert; diff --git a/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java b/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java index 4de034dcb5b..2037bfef6e9 100644 --- a/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java @@ -115,7 +115,6 @@ public void authenticateWhenPasswordServiceThenUpdated() { public void authenticateWhenPasswordServiceAndBadCredentialsThenNotUpdated() { when(this.userDetailsService.findByUsername(any())).thenReturn(Mono.just(this.user)); when(this.encoder.matches(any(), any())).thenReturn(false); - when(this.userDetailsPasswordService.updatePassword(any(), any())).thenReturn(Mono.just(this.user)); this.manager.setPasswordEncoder(this.encoder); this.manager.setUserDetailsPasswordService(this.userDetailsPasswordService); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( @@ -132,7 +131,6 @@ public void authenticateWhenPasswordServiceAndUpgradeFalseThenNotUpdated() { when(this.userDetailsService.findByUsername(any())).thenReturn(Mono.just(this.user)); when(this.encoder.matches(any(), any())).thenReturn(true); when(this.encoder.upgradeEncoding(any())).thenReturn(false); - when(this.userDetailsPasswordService.updatePassword(any(), any())).thenReturn(Mono.just(this.user)); this.manager.setPasswordEncoder(this.encoder); this.manager.setUserDetailsPasswordService(this.userDetailsPasswordService); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( From 783ef536442aeed9583edd42ad7ad446275b6d81 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 9 Jul 2018 12:52:52 -0500 Subject: [PATCH 117/226] Modernize Default Log In Page Fixes: gh-5515 --- .../DefaultLoginPageConfigurerTests.groovy | 231 +++++++++++++----- .../FormLoginBeanDefinitionParserTests.java | 157 ++++++++---- .../samples/OAuth2LoginApplicationTests.java | 8 +- .../security/samples/pages/LoginPage.java | 4 +- .../security/samples/pages/LoginPage.java | 4 +- .../security/samples/pages/LoginPage.java | 4 +- .../security/samples/pages/LoginPage.java | 4 +- .../security/samples/pages/LoginPage.java | 4 +- .../security/samples/pages/LogoutPage.java | 6 +- .../security/samples/pages/LoginPage.java | 4 +- .../security/samples/pages/LogoutPage.java | 6 +- .../ui/DefaultLoginPageGeneratingFilter.java | 139 ++++++----- .../ui/LoginPageGeneratingWebFilter.java | 3 +- 13 files changed, 385 insertions(+), 189 deletions(-) diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy index 25f10dbba79..32b7188b0f3 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy @@ -53,15 +53,33 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { request.requestURI = "/login" springSecurityFilterChain.doFilter(request,response,chain) then: - response.getContentAsString() == """Login Page -

    Login with Username and Password

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

    Your login attempt was not successful, try again.

    Reason: Bad credentials

    Login with Username and Password

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

    You have been logged out

    Login with Username and Password

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

    Login with Username and Password

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

    Login with OpenID Identity

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

    Login with Username and Password

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

    Login with OpenID Identity

    - - - - -
    Identity:
    Remember me on this computer.
    - -
    """ + response.getContentAsString() == """ + + + + + + + Please sign in + + + + +
    + + +""" } @Configuration diff --git a/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java index 86a3290c65a..7bd087725c4 100644 --- a/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java @@ -54,14 +54,32 @@ public void getLoginWhenAutoConfigThenShowsDefaultLoginPage() this.spring.configLocations(this.xml("Simple")).autowire(); String expectedContent = - "Login Page\n" + - "

    Login with Username and Password

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

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

    \n" + + "

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

    \n" + + " \n" + + "
    \n" + + ""; this.mvc.perform(get("/login")).andExpect(content().string(expectedContent)); } @@ -73,14 +91,31 @@ public void getLoginWhenConfiguredWithCustomAttributesThenLoginPageReflects() this.spring.configLocations(this.xml("WithCustomAttributes")).autowire(); String expectedContent = - "Login Page\n" + - "

    Login with Username and Password

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

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

    \n" + + "

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

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

    Login with Username and Password

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

    Login with OpenID Identity

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

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

    \n" + + "

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

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

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

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

    Login with Username and Password

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

    Login with OpenID Identity

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

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

    \n" + + "

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

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

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

    \n" + + " \n" + + "
    \n" + + ""; this.mvc.perform(get("/login")).andExpect(content().string(expectedContent)); } diff --git a/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java b/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java index 1c722da290c..68fb179c61a 100644 --- a/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java +++ b/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java @@ -218,7 +218,7 @@ public void requestAuthorizationCodeGrantWhenNoMatchingAuthorizationRequestThenD page = this.webClient.getPage(new URL(authorizationResponseUri)); assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl); - HtmlElement errorElement = page.getBody().getFirstByXPath("p"); + HtmlElement errorElement = page.getBody().getFirstByXPath("div"); assertThat(errorElement).isNotNull(); assertThat(errorElement.asText()).contains("authorization_request_not_found"); } @@ -248,7 +248,7 @@ public void requestAuthorizationCodeGrantWhenInvalidStateParamThenDisplayLoginPa page = this.webClient.getPage(new URL(authorizationResponseUri)); assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl); - HtmlElement errorElement = page.getBody().getFirstByXPath("p"); + HtmlElement errorElement = page.getBody().getFirstByXPath("div"); assertThat(errorElement).isNotNull(); assertThat(errorElement.asText()).contains("authorization_request_not_found"); } @@ -284,13 +284,13 @@ public void requestAuthorizationCodeGrantWhenInvalidRedirectUriThenDisplayLoginP page = this.webClient.getPage(new URL(authorizationResponseUri)); assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl); - HtmlElement errorElement = page.getBody().getFirstByXPath("p"); + HtmlElement errorElement = page.getBody().getFirstByXPath("div"); assertThat(errorElement).isNotNull(); assertThat(errorElement.asText()).contains("invalid_redirect_uri_parameter"); } private void assertLoginPage(HtmlPage page) throws Exception { - assertThat(page.getTitleText()).isEqualTo("Login Page"); + assertThat(page.getTitleText()).isEqualTo("Please sign in"); int expectedClients = 4; diff --git a/samples/javaconfig/helloworld/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java b/samples/javaconfig/helloworld/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java index 0a118128d2a..6d72ee7c03f 100644 --- a/samples/javaconfig/helloworld/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java +++ b/samples/javaconfig/helloworld/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java @@ -37,7 +37,7 @@ public LoginPage(WebDriver webDriver) { } public LoginPage assertAt() { - assertThat(this.webDriver.getTitle()).isEqualTo("Login Page"); + assertThat(this.webDriver.getTitle()).isEqualTo("Please sign in"); return this; } @@ -49,7 +49,7 @@ public static class LoginForm { private WebDriver webDriver; private WebElement username; private WebElement password; - @FindBy(css = "input[type=submit]") + @FindBy(css = "button[type=submit]") private WebElement submit; public LoginForm(WebDriver webDriver) { diff --git a/samples/javaconfig/jdbc/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java b/samples/javaconfig/jdbc/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java index 0a118128d2a..6d72ee7c03f 100644 --- a/samples/javaconfig/jdbc/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java +++ b/samples/javaconfig/jdbc/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java @@ -37,7 +37,7 @@ public LoginPage(WebDriver webDriver) { } public LoginPage assertAt() { - assertThat(this.webDriver.getTitle()).isEqualTo("Login Page"); + assertThat(this.webDriver.getTitle()).isEqualTo("Please sign in"); return this; } @@ -49,7 +49,7 @@ public static class LoginForm { private WebDriver webDriver; private WebElement username; private WebElement password; - @FindBy(css = "input[type=submit]") + @FindBy(css = "button[type=submit]") private WebElement submit; public LoginForm(WebDriver webDriver) { diff --git a/samples/javaconfig/ldap/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java b/samples/javaconfig/ldap/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java index 0a118128d2a..6d72ee7c03f 100644 --- a/samples/javaconfig/ldap/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java +++ b/samples/javaconfig/ldap/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java @@ -37,7 +37,7 @@ public LoginPage(WebDriver webDriver) { } public LoginPage assertAt() { - assertThat(this.webDriver.getTitle()).isEqualTo("Login Page"); + assertThat(this.webDriver.getTitle()).isEqualTo("Please sign in"); return this; } @@ -49,7 +49,7 @@ public static class LoginForm { private WebDriver webDriver; private WebElement username; private WebElement password; - @FindBy(css = "input[type=submit]") + @FindBy(css = "button[type=submit]") private WebElement submit; public LoginForm(WebDriver webDriver) { diff --git a/samples/xml/helloworld/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java b/samples/xml/helloworld/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java index 0a118128d2a..6d72ee7c03f 100644 --- a/samples/xml/helloworld/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java +++ b/samples/xml/helloworld/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java @@ -37,7 +37,7 @@ public LoginPage(WebDriver webDriver) { } public LoginPage assertAt() { - assertThat(this.webDriver.getTitle()).isEqualTo("Login Page"); + assertThat(this.webDriver.getTitle()).isEqualTo("Please sign in"); return this; } @@ -49,7 +49,7 @@ public static class LoginForm { private WebDriver webDriver; private WebElement username; private WebElement password; - @FindBy(css = "input[type=submit]") + @FindBy(css = "button[type=submit]") private WebElement submit; public LoginForm(WebDriver webDriver) { diff --git a/samples/xml/jaas/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java b/samples/xml/jaas/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java index 6a13e93eca8..1737af5de7e 100644 --- a/samples/xml/jaas/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java +++ b/samples/xml/jaas/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java @@ -39,7 +39,7 @@ public LoginPage(WebDriver webDriver) { } public LoginPage assertAt() { - assertThat(this.webDriver.getTitle()).isEqualTo("Login Page"); + assertThat(this.webDriver.getTitle()).isEqualTo("Please sign in"); return this; } @@ -51,7 +51,7 @@ public static class LoginForm { private WebDriver webDriver; private WebElement username; private WebElement password; - @FindBy(css = "input[type=submit]") + @FindBy(css = "button[type=submit]") private WebElement submit; public LoginForm(WebDriver webDriver) { diff --git a/samples/xml/jaas/src/integration-test/java/org/springframework/security/samples/pages/LogoutPage.java b/samples/xml/jaas/src/integration-test/java/org/springframework/security/samples/pages/LogoutPage.java index abaf6ccdb3f..a4a7e324eec 100644 --- a/samples/xml/jaas/src/integration-test/java/org/springframework/security/samples/pages/LogoutPage.java +++ b/samples/xml/jaas/src/integration-test/java/org/springframework/security/samples/pages/LogoutPage.java @@ -27,8 +27,8 @@ * @author Michael Simons */ public class LogoutPage extends LoginPage { - @FindBy(css = "p") - private WebElement p; + @FindBy(css = "div[role=alert]") + private WebElement alert; public LogoutPage(WebDriver webDriver) { super(webDriver); @@ -38,7 +38,7 @@ public LogoutPage(WebDriver webDriver) { public LogoutPage assertAt() { super.assertAt(); - assertThat(p.getText()).isEqualTo("You have been logged out"); + assertThat(this.alert.getText()).isEqualTo("You have been signed out"); return this; } } diff --git a/samples/xml/ldap/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java b/samples/xml/ldap/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java index 6a13e93eca8..1737af5de7e 100644 --- a/samples/xml/ldap/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java +++ b/samples/xml/ldap/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java @@ -39,7 +39,7 @@ public LoginPage(WebDriver webDriver) { } public LoginPage assertAt() { - assertThat(this.webDriver.getTitle()).isEqualTo("Login Page"); + assertThat(this.webDriver.getTitle()).isEqualTo("Please sign in"); return this; } @@ -51,7 +51,7 @@ public static class LoginForm { private WebDriver webDriver; private WebElement username; private WebElement password; - @FindBy(css = "input[type=submit]") + @FindBy(css = "button[type=submit]") private WebElement submit; public LoginForm(WebDriver webDriver) { diff --git a/samples/xml/ldap/src/integration-test/java/org/springframework/security/samples/pages/LogoutPage.java b/samples/xml/ldap/src/integration-test/java/org/springframework/security/samples/pages/LogoutPage.java index abaf6ccdb3f..a4a7e324eec 100644 --- a/samples/xml/ldap/src/integration-test/java/org/springframework/security/samples/pages/LogoutPage.java +++ b/samples/xml/ldap/src/integration-test/java/org/springframework/security/samples/pages/LogoutPage.java @@ -27,8 +27,8 @@ * @author Michael Simons */ public class LogoutPage extends LoginPage { - @FindBy(css = "p") - private WebElement p; + @FindBy(css = "div[role=alert]") + private WebElement alert; public LogoutPage(WebDriver webDriver) { super(webDriver); @@ -38,7 +38,7 @@ public LogoutPage(WebDriver webDriver) { public LogoutPage assertAt() { super.assertAt(); - assertThat(p.getText()).isEqualTo("You have been logged out"); + assertThat(this.alert.getText()).isEqualTo("You have been signed out"); return this; } } diff --git a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java index 74c0ae67965..783fe6fb019 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java @@ -208,7 +208,7 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) { - String errorMsg = "none"; + String errorMsg = "Invalid credentials"; if (loginError) { HttpSession session = request.getSession(false); @@ -216,82 +216,76 @@ private String generateLoginPageHtml(HttpServletRequest request, boolean loginEr if (session != null) { AuthenticationException ex = (AuthenticationException) session .getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); - errorMsg = ex != null ? ex.getMessage() : "none"; + errorMsg = ex != null ? ex.getMessage() : "Invalid credentials"; } } StringBuilder sb = new StringBuilder(); - sb.append("Login Page"); - - if (formLoginEnabled) { - sb.append("\n"); - } - - if (loginError) { - sb.append("

    Your login attempt was not successful, try again.

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

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

    You have been logged out

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

    Login with Username and Password

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

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

    \n" + + "

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

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

    Login with OpenID Identity

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

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

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

    Login with OAuth 2.0

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

    Remember me on this computer.

    \n"; } private boolean isLogoutSuccess(HttpServletRequest request) { @@ -317,6 +322,14 @@ private boolean isErrorPage(HttpServletRequest request) { return matches(request, failureUrl); } + private static String createError(boolean isError, String message) { + return isError ? "
    " + HtmlUtils.htmlEscape(message) + "
    " : ""; + } + + private static String createLogoutSuccess(boolean isLogoutSuccess) { + return isLogoutSuccess ? "
    You have been signed out
    " : ""; + } + private boolean matches(HttpServletRequest request, String url) { if (!"GET".equals(request.getMethod()) || url == null) { return false; diff --git a/web/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java b/web/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java index 74f63f806ef..7bf0cfaa35b 100644 --- a/web/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java +++ b/web/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java @@ -122,7 +122,8 @@ private String formLogin(MultiValueMap queryParams, String csrfT boolean isLogoutSuccess = queryParams.containsKey("logout"); return "
    \n" + " \n" - + createError(isError) + createLogoutSuccess(isLogoutSuccess) + + createError(isError) + + createLogoutSuccess(isLogoutSuccess) + "

    \n" + " \n" + " \n" From 3ab5b3083b173117f67d6211fb435af20001a5bf Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 13 Jul 2018 14:33:06 -0500 Subject: [PATCH 118/226] Default Log Out Page Fixes: gh-5516 --- .../annotation/web/HttpSecurityBuilder.java | 1 + .../web/builders/FilterComparator.java | 3 + .../DefaultLoginPageConfigurer.java | 17 +- .../http/AuthenticationConfigBuilder.java | 8 + .../security/config/http/SecurityFilters.java | 1 + .../main/resources/META-INF/spring.schemas | 3 +- .../security/config/spring-security-5.1.rnc | 909 ++++++ .../security/config/spring-security-5.1.xsd | 2737 +++++++++++++++++ .../http/AbstractHttpConfigTests.groovy | 2 +- .../config/http/MiscHttpConfigTests.groovy | 6 +- .../http/SessionManagementConfigTests.groovy | 2 +- .../config/doc/XsdDocumentedTests.java | 4 +- .../security/config/http/CsrfConfigTests.java | 3 +- .../FormLoginBeanDefinitionParserTests.java | 13 + ...inBeanDefinitionParserTests-AutoConfig.xml | 31 + .../ui/DefaultLogoutPageGeneratingFilter.java | 103 + ...efaultLogoutPageGeneratingFilterTests.java | 86 + 17 files changed, 3917 insertions(+), 12 deletions(-) create mode 100644 config/src/main/resources/org/springframework/security/config/spring-security-5.1.rnc create mode 100644 config/src/main/resources/org/springframework/security/config/spring-security-5.1.xsd create mode 100644 config/src/test/resources/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests-AutoConfig.xml create mode 100644 web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilter.java create mode 100644 web/src/test/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilterTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java index f772c7d62f3..0e4ff7e6f76 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java @@ -146,6 +146,7 @@ > C removeConfigurer *

  • {@link ConcurrentSessionFilter}
  • *
  • {@link OpenIDAuthenticationFilter}
  • *
  • {@link org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter}
  • + *
  • {@link org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter}
  • *
  • {@link ConcurrentSessionFilter}
  • *
  • {@link DigestAuthenticationFilter}
  • *
  • {@link BasicAuthenticationFilter}
  • diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java index 20f336d6091..ebc1a22715a 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java @@ -33,6 +33,7 @@ import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter; import org.springframework.security.web.authentication.switchuser.SwitchUserFilter; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; +import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.authentication.www.DigestAuthenticationFilter; import org.springframework.security.web.context.SecurityContextPersistenceFilter; @@ -101,6 +102,8 @@ final class FilterComparator implements Comparator, Serializable { order += STEP; put(DefaultLoginPageGeneratingFilter.class, order); order += STEP; + put(DefaultLogoutPageGeneratingFilter.class, order); + order += STEP; put(ConcurrentSessionFilter.class, order); order += STEP; put(DigestAuthenticationFilter.class, order); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java index 5d2fa4930de..84de25722d8 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java @@ -19,9 +19,13 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; +import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter; import org.springframework.security.web.csrf.CsrfToken; +import javax.servlet.http.HttpServletRequest; import java.util.Collections; +import java.util.Map; +import java.util.function.Function; /** * Adds a Filter that will generate a login page if one is not specified otherwise when @@ -66,15 +70,19 @@ public final class DefaultLoginPageConfigurer> private DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = new DefaultLoginPageGeneratingFilter(); + private DefaultLogoutPageGeneratingFilter logoutPageGeneratingFilter = new DefaultLogoutPageGeneratingFilter(); + @Override public void init(H http) throws Exception { - this.loginPageGeneratingFilter.setResolveHiddenInputs( request -> { + Function> hiddenInputs = request -> { CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); - if(token == null) { + if (token == null) { return Collections.emptyMap(); } return Collections.singletonMap(token.getParameterName(), token.getToken()); - }); + }; + this.loginPageGeneratingFilter.setResolveHiddenInputs(hiddenInputs); + this.logoutPageGeneratingFilter.setResolveHiddenInputs(hiddenInputs); http.setSharedObject(DefaultLoginPageGeneratingFilter.class, loginPageGeneratingFilter); } @@ -92,7 +100,8 @@ public void configure(H http) throws Exception { if (loginPageGeneratingFilter.isEnabled() && authenticationEntryPoint == null) { loginPageGeneratingFilter = postProcess(loginPageGeneratingFilter); http.addFilter(loginPageGeneratingFilter); + http.addFilter(this.logoutPageGeneratingFilter); } } -} \ No newline at end of file +} diff --git a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java index 39bcdbf434c..3ad241ccb31 100644 --- a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java @@ -48,6 +48,7 @@ import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor; import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; +import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.csrf.CsrfToken; @@ -123,6 +124,7 @@ final class AuthenticationConfigBuilder { @SuppressWarnings("rawtypes") private ManagedList logoutHandlers; private BeanDefinition loginPageGenerationFilter; + private BeanDefinition logoutPageGenerationFilter; private BeanDefinition etf; private final BeanReference requestCache; private final BeanReference portMapper; @@ -544,6 +546,10 @@ void createLoginPageFilterIfNeeded() { .rootBeanDefinition(DefaultLoginPageGeneratingFilter.class); loginPageFilter.addPropertyValue("resolveHiddenInputs", new CsrfTokenHiddenInputFunction()); + BeanDefinitionBuilder logoutPageFilter = BeanDefinitionBuilder + .rootBeanDefinition(DefaultLogoutPageGeneratingFilter.class); + logoutPageFilter.addPropertyValue("resolveHiddenInputs", new CsrfTokenHiddenInputFunction()); + if (formFilterId != null) { loginPageFilter.addConstructorArgReference(formFilterId); loginPageFilter.addPropertyValue("authenticationUrl", loginProcessingUrl); @@ -556,6 +562,7 @@ void createLoginPageFilterIfNeeded() { } loginPageGenerationFilter = loginPageFilter.getBeanDefinition(); + this.logoutPageGenerationFilter = logoutPageFilter.getBeanDefinition(); } } @@ -798,6 +805,7 @@ List getFilters() { if (loginPageGenerationFilter != null) { filters.add(new OrderDecorator(loginPageGenerationFilter, LOGIN_PAGE_FILTER)); + filters.add(new OrderDecorator(this.logoutPageGenerationFilter, LOGOUT_PAGE_FILTER)); } if (basicFilter != null) { diff --git a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java index 3c01a48f94e..1b84ecace48 100644 --- a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java +++ b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java @@ -40,6 +40,7 @@ enum SecurityFilters { FORM_LOGIN_FILTER, OPENID_FILTER, LOGIN_PAGE_FILTER, + LOGOUT_PAGE_FILTER, DIGEST_AUTH_FILTER, BASIC_AUTH_FILTER, REQUEST_CACHE_FILTER, diff --git a/config/src/main/resources/META-INF/spring.schemas b/config/src/main/resources/META-INF/spring.schemas index 97d4844af25..fe5518e1347 100644 --- a/config/src/main/resources/META-INF/spring.schemas +++ b/config/src/main/resources/META-INF/spring.schemas @@ -1,4 +1,5 @@ -http\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-5.0.xsd +http\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-5.1.xsd +http\://www.springframework.org/schema/security/spring-security-5.1.xsd=org/springframework/security/config/spring-security-5.1.xsd http\://www.springframework.org/schema/security/spring-security-5.0.xsd=org/springframework/security/config/spring-security-5.0.xsd http\://www.springframework.org/schema/security/spring-security-4.2.xsd=org/springframework/security/config/spring-security-4.2.xsd http\://www.springframework.org/schema/security/spring-security-4.1.xsd=org/springframework/security/config/spring-security-4.1.xsd diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.1.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.rnc new file mode 100644 index 00000000000..6f67240121e --- /dev/null +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.rnc @@ -0,0 +1,909 @@ +namespace a = "http://relaxng.org/ns/compatibility/annotations/1.0" +datatypes xsd = "http://www.w3.org/2001/XMLSchema-datatypes" + +default namespace = "http://www.springframework.org/schema/security" + +start = http | ldap-server | authentication-provider | ldap-authentication-provider | any-user-service | ldap-server | ldap-authentication-provider + +hash = + ## Defines the hashing algorithm used on user passwords. Bcrypt is recommended. + attribute hash {"bcrypt"} +base64 = + ## Whether a string should be base64 encoded + attribute base64 {xsd:boolean} +request-matcher = + ## Defines the strategy use for matching incoming requests. Currently the options are 'mvc' (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions and 'ciRegex' for case-insensitive regular expressions. + attribute request-matcher {"mvc" | "ant" | "regex" | "ciRegex"} +port = + ## Specifies an IP port number. Used to configure an embedded LDAP server, for example. + attribute port { xsd:positiveInteger } +url = + ## Specifies a URL. + attribute url { xsd:token } +id = + ## A bean identifier, used for referring to the bean elsewhere in the context. + attribute id {xsd:token} +name = + ## A bean identifier, used for referring to the bean elsewhere in the context. + attribute name {xsd:token} +ref = + ## Defines a reference to a Spring bean Id. + attribute ref {xsd:token} + +cache-ref = + ## Defines a reference to a cache for use with a UserDetailsService. + attribute cache-ref {xsd:token} + +user-service-ref = + ## A reference to a user-service (or UserDetailsService bean) Id + attribute user-service-ref {xsd:token} + +authentication-manager-ref = + ## A reference to an AuthenticationManager bean + attribute authentication-manager-ref {xsd:token} + +data-source-ref = + ## A reference to a DataSource bean + attribute data-source-ref {xsd:token} + + + +debug = + ## Enables Spring Security debugging infrastructure. This will provide human-readable (multi-line) debugging information to monitor requests coming into the security filters. This may include sensitive information, such as request parameters or headers, and should only be used in a development environment. + element debug {empty} + +password-encoder = + ## element which defines a password encoding strategy. Used by an authentication provider to convert submitted passwords to hashed versions, for example. + element password-encoder {password-encoder.attlist} +password-encoder.attlist &= + ref | (hash) + +role-prefix = + ## A non-empty string prefix that will be added to role strings loaded from persistent storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is non-empty. + attribute role-prefix {xsd:token} + +use-expressions = + ## Enables the use of expressions in the 'access' attributes in elements rather than the traditional list of configuration attributes. Defaults to 'true'. If enabled, each attribute should contain a single boolean expression. If the expression evaluates to 'true', access will be granted. + attribute use-expressions {xsd:boolean} + +ldap-server = + ## Defines an LDAP server location or starts an embedded server. The url indicates the location of a remote server. If no url is given, an embedded server will be started, listening on the supplied port number. The port is optional and defaults to 33389. A Spring LDAP ContextSource bean will be registered for the server with the id supplied. + element ldap-server {ldap-server.attlist} +ldap-server.attlist &= id? +ldap-server.attlist &= (url | port)? +ldap-server.attlist &= + ## Username (DN) of the "manager" user identity which will be used to authenticate to a (non-embedded) LDAP server. If omitted, anonymous access will be used. + attribute manager-dn {xsd:string}? +ldap-server.attlist &= + ## The password for the manager DN. This is required if the manager-dn is specified. + attribute manager-password {xsd:string}? +ldap-server.attlist &= + ## Explicitly specifies an ldif file resource to load into an embedded LDAP server. The default is classpath*:*.ldiff + attribute ldif { xsd:string }? +ldap-server.attlist &= + ## Optional root suffix for the embedded LDAP server. Default is "dc=springframework,dc=org" + attribute root { xsd:string }? + +ldap-server-ref-attribute = + ## The optional server to use. If omitted, and a default LDAP server is registered (using with no Id), that server will be used. + attribute server-ref {xsd:token} + + +group-search-filter-attribute = + ## Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN of the user. + attribute group-search-filter {xsd:token} +group-search-base-attribute = + ## Search base for group membership searches. Defaults to "" (searching from the root). + attribute group-search-base {xsd:token} +user-search-filter-attribute = + ## The LDAP filter used to search for users (optional). For example "(uid={0})". The substituted parameter is the user's login name. + attribute user-search-filter {xsd:token} +user-search-base-attribute = + ## Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. + attribute user-search-base {xsd:token} +group-role-attribute-attribute = + ## The LDAP attribute name which contains the role name which will be used within Spring Security. Defaults to "cn". + attribute group-role-attribute {xsd:token} +user-details-class-attribute = + ## Allows the objectClass of the user entry to be specified. If set, the framework will attempt to load standard attributes for the defined class into the returned UserDetails object + attribute user-details-class {"person" | "inetOrgPerson"} +user-context-mapper-attribute = + ## Allows explicit customization of the loaded user object by specifying a UserDetailsContextMapper bean which will be called with the context information from the user's directory entry + attribute user-context-mapper-ref {xsd:token} + + +ldap-user-service = + ## This element configures a LdapUserDetailsService which is a combination of a FilterBasedLdapUserSearch and a DefaultLdapAuthoritiesPopulator. + element ldap-user-service {ldap-us.attlist} +ldap-us.attlist &= id? +ldap-us.attlist &= + ldap-server-ref-attribute? +ldap-us.attlist &= + user-search-filter-attribute? +ldap-us.attlist &= + user-search-base-attribute? +ldap-us.attlist &= + group-search-filter-attribute? +ldap-us.attlist &= + group-search-base-attribute? +ldap-us.attlist &= + group-role-attribute-attribute? +ldap-us.attlist &= + cache-ref? +ldap-us.attlist &= + role-prefix? +ldap-us.attlist &= + (user-details-class-attribute | user-context-mapper-attribute)? + +ldap-authentication-provider = + ## Sets up an ldap authentication provider + element ldap-authentication-provider {ldap-ap.attlist, password-compare-element?} +ldap-ap.attlist &= + ldap-server-ref-attribute? +ldap-ap.attlist &= + user-search-base-attribute? +ldap-ap.attlist &= + user-search-filter-attribute? +ldap-ap.attlist &= + group-search-base-attribute? +ldap-ap.attlist &= + group-search-filter-attribute? +ldap-ap.attlist &= + group-role-attribute-attribute? +ldap-ap.attlist &= + ## A specific pattern used to build the user's DN, for example "uid={0},ou=people". The key "{0}" must be present and will be substituted with the username. + attribute user-dn-pattern {xsd:token}? +ldap-ap.attlist &= + role-prefix? +ldap-ap.attlist &= + (user-details-class-attribute | user-context-mapper-attribute)? + +password-compare-element = + ## Specifies that an LDAP provider should use an LDAP compare operation of the user's password to authenticate the user + element password-compare {password-compare.attlist, password-encoder?} + +password-compare.attlist &= + ## The attribute in the directory which contains the user password. Defaults to "userPassword". + attribute password-attribute {xsd:token}? +password-compare.attlist &= + hash? + +intercept-methods = + ## Can be used inside a bean definition to add a security interceptor to the bean and set up access configuration attributes for the bean's methods + element intercept-methods {intercept-methods.attlist, protect+} +intercept-methods.attlist &= + ## Optional AccessDecisionManager bean ID to be used by the created method security interceptor. + attribute access-decision-manager-ref {xsd:token}? + + +protect = + ## Defines a protected method and the access control configuration attributes that apply to it. We strongly advise you NOT to mix "protect" declarations with any services provided "global-method-security". + element protect {protect.attlist, empty} +protect.attlist &= + ## A method name + attribute method {xsd:token} +protect.attlist &= + ## Access configuration attributes list that applies to the method, e.g. "ROLE_A,ROLE_B". + attribute access {xsd:token} + +method-security-metadata-source = + ## Creates a MethodSecurityMetadataSource instance + element method-security-metadata-source {msmds.attlist, protect+} +msmds.attlist &= id? + +msmds.attlist &= use-expressions? + +global-method-security = + ## Provides method security for all beans registered in the Spring application context. Specifically, beans will be scanned for matches with the ordered list of "protect-pointcut" sub-elements, Spring Security annotations and/or. Where there is a match, the beans will automatically be proxied and security authorization applied to the methods accordingly. If you use and enable all four sources of method security metadata (ie "protect-pointcut" declarations, expression annotations, @Secured and also JSR250 security annotations), the metadata sources will be queried in that order. In practical terms, this enables you to use XML to override method security metadata expressed in annotations. If using annotations, the order of precedence is EL-based (@PreAuthorize etc.), @Secured and finally JSR-250. + element global-method-security {global-method-security.attlist, (pre-post-annotation-handling | expression-handler)?, protect-pointcut*, after-invocation-provider*} +global-method-security.attlist &= + ## Specifies whether the use of Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this application context. Defaults to "disabled". + attribute pre-post-annotations {"disabled" | "enabled" }? +global-method-security.attlist &= + ## Specifies whether the use of Spring Security's @Secured annotations should be enabled for this application context. Defaults to "disabled". + attribute secured-annotations {"disabled" | "enabled" }? +global-method-security.attlist &= + ## Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). This will require the javax.annotation.security classes on the classpath. Defaults to "disabled". + attribute jsr250-annotations {"disabled" | "enabled" }? +global-method-security.attlist &= + ## Optional AccessDecisionManager bean ID to override the default used for method security. + attribute access-decision-manager-ref {xsd:token}? +global-method-security.attlist &= + ## Optional RunAsmanager implementation which will be used by the configured MethodSecurityInterceptor + attribute run-as-manager-ref {xsd:token}? +global-method-security.attlist &= + ## Allows the advice "order" to be set for the method security interceptor. + attribute order {xsd:token}? +global-method-security.attlist &= + ## If true, class based proxying will be used instead of interface based proxying. + attribute proxy-target-class {xsd:boolean}? +global-method-security.attlist &= + ## Can be used to specify that AspectJ should be used instead of the default Spring AOP. If set, secured classes must be woven with the AnnotationSecurityAspect from the spring-security-aspects module. + attribute mode {"aspectj"}? +global-method-security.attlist &= + ## An external MethodSecurityMetadataSource instance can be supplied which will take priority over other sources (such as the default annotations). + attribute metadata-source-ref {xsd:token}? +global-method-security.attlist &= + authentication-manager-ref? + + +after-invocation-provider = + ## Allows addition of extra AfterInvocationProvider beans which should be called by the MethodSecurityInterceptor created by global-method-security. + element after-invocation-provider {ref} + +pre-post-annotation-handling = + ## Allows the default expression-based mechanism for handling Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) to be replace entirely. Only applies if these annotations are enabled. + element pre-post-annotation-handling {invocation-attribute-factory, pre-invocation-advice, post-invocation-advice} + +invocation-attribute-factory = + ## Defines the PrePostInvocationAttributeFactory instance which is used to generate pre and post invocation metadata from the annotated methods. + element invocation-attribute-factory {ref} + +pre-invocation-advice = + ## Customizes the PreInvocationAuthorizationAdviceVoter with the ref as the PreInvocationAuthorizationAdviceVoter for the element. + element pre-invocation-advice {ref} + +post-invocation-advice = + ## Customizes the PostInvocationAdviceProvider with the ref as the PostInvocationAuthorizationAdvice for the element. + element post-invocation-advice {ref} + + +expression-handler = + ## Defines the SecurityExpressionHandler instance which will be used if expression-based access-control is enabled. A default implementation (with no ACL support) will be used if not supplied. + element expression-handler {ref} + +protect-pointcut = + ## Defines a protected pointcut and the access control configuration attributes that apply to it. Every bean registered in the Spring application context that provides a method that matches the pointcut will receive security authorization. + element protect-pointcut {protect-pointcut.attlist, empty} +protect-pointcut.attlist &= + ## An AspectJ expression, including the 'execution' keyword. For example, 'execution(int com.foo.TargetObject.countLength(String))' (without the quotes). + attribute expression {xsd:string} +protect-pointcut.attlist &= + ## Access configuration attributes list that applies to all methods matching the pointcut, e.g. "ROLE_A,ROLE_B" + attribute access {xsd:token} + +websocket-message-broker = + ## Allows securing a Message Broker. There are two modes. If no id is specified: ensures that any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures that the SecurityContextChannelInterceptor is automatically registered for the clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the clientInboundChannel. If the id is specified, creates a ChannelSecurityInterceptor that can be manually registered with the clientInboundChannel. + element websocket-message-broker { websocket-message-broker.attrlist, (intercept-message* & expression-handler?) } + +websocket-message-broker.attrlist &= + ## A bean identifier, used for referring to the bean elsewhere in the context. If specified, explicit configuration within clientInboundChannel is required. If not specified, ensures that any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures that the SecurityContextChannelInterceptor is automatically registered for the clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the clientInboundChannel. + attribute id {xsd:token}? +websocket-message-broker.attrlist &= + ## Disables the requirement for CSRF token to be present in the Stomp headers (default false). Changing the default is useful if it is necessary to allow other origins to make SockJS connections. + attribute same-origin-disabled {xsd:boolean}? + +intercept-message = + ## Creates an authorization rule for a websocket message. + element intercept-message {intercept-message.attrlist} + +intercept-message.attrlist &= + ## The destination ant pattern which will be mapped to the access attribute. For example, /** matches any message with a destination, /admin/** matches any message that has a destination that starts with admin. + attribute pattern {xsd:token}? +intercept-message.attrlist &= + ## The access configuration attributes that apply for the configured message. For example, permitAll grants access to anyone, hasRole('ROLE_ADMIN') requires the user have the role 'ROLE_ADMIN'. + attribute access {xsd:token}? +intercept-message.attrlist &= + ## The type of message to match on. Valid values are defined in SimpMessageType (i.e. CONNECT, CONNECT_ACK, HEARTBEAT, MESSAGE, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT, DISCONNECT_ACK, OTHER). + attribute type {"CONNECT" | "CONNECT_ACK" | "HEARTBEAT" | "MESSAGE" | "SUBSCRIBE"| "UNSUBSCRIBE" | "DISCONNECT" | "DISCONNECT_ACK" | "OTHER"}? + +http-firewall = + ## Allows a custom instance of HttpFirewall to be injected into the FilterChainProxy created by the namespace. + element http-firewall {ref} + +http = + ## Container element for HTTP security configuration. Multiple elements can now be defined, each with a specific pattern to which the enclosed security configuration applies. A pattern can also be configured to bypass Spring Security's filters completely by setting the "security" attribute to "none". + element http {http.attlist, (intercept-url* & access-denied-handler? & form-login? & openid-login? & x509? & jee? & http-basic? & logout? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache? & expression-handler? & headers? & csrf? & cors?) } +http.attlist &= + ## The request URL pattern which will be mapped to the filter chain created by this element. If omitted, the filter chain will match all requests. + attribute pattern {xsd:token}? +http.attlist &= + ## When set to 'none', requests matching the pattern attribute will be ignored by Spring Security. No security filters will be applied and no SecurityContext will be available. If set, the element must be empty, with no children. + attribute security {"none"}? +http.attlist &= + ## Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + attribute request-matcher-ref { xsd:token }? +http.attlist &= + ## A legacy attribute which automatically registers a login form, BASIC authentication and a logout URL and logout services. If unspecified, defaults to "false". We'd recommend you avoid using this and instead explicitly configure the services you require. + attribute auto-config {xsd:boolean}? +http.attlist &= + use-expressions? +http.attlist &= + ## Controls the eagerness with which an HTTP session is created by Spring Security classes. If not set, defaults to "ifRequired". If "stateless" is used, this implies that the application guarantees that it will not create a session. This differs from the use of "never" which means that Spring Security will not create a session, but will make use of one if the application does. + attribute create-session {"ifRequired" | "always" | "never" | "stateless"}? +http.attlist &= + ## A reference to a SecurityContextRepository bean. This can be used to customize how the SecurityContext is stored between requests. + attribute security-context-repository-ref {xsd:token}? +http.attlist &= + request-matcher? +http.attlist &= + ## Provides versions of HttpServletRequest security methods such as isUserInRole() and getPrincipal() which are implemented by accessing the Spring SecurityContext. Defaults to "true". + attribute servlet-api-provision {xsd:boolean}? +http.attlist &= + ## If available, runs the request as the Subject acquired from the JaasAuthenticationToken. Defaults to "false". + attribute jaas-api-provision {xsd:boolean}? +http.attlist &= + ## Optional attribute specifying the ID of the AccessDecisionManager implementation which should be used for authorizing HTTP requests. + attribute access-decision-manager-ref {xsd:token}? +http.attlist &= + ## Optional attribute specifying the realm name that will be used for all authentication features that require a realm name (eg BASIC and Digest authentication). If unspecified, defaults to "Spring Security Application". + attribute realm {xsd:token}? +http.attlist &= + ## Allows a customized AuthenticationEntryPoint to be set on the ExceptionTranslationFilter. + attribute entry-point-ref {xsd:token}? +http.attlist &= + ## Corresponds to the observeOncePerRequest property of FilterSecurityInterceptor. Defaults to "true" + attribute once-per-request {xsd:boolean}? +http.attlist &= + ## Prevents the jsessionid parameter from being added to rendered URLs. Defaults to "true" (rewriting is disabled). + attribute disable-url-rewriting {xsd:boolean}? +http.attlist &= + ## Exposes the list of filters defined by this configuration under this bean name in the application context. + name? +http.attlist &= + authentication-manager-ref? + +access-denied-handler = + ## Defines the access-denied strategy that should be used. An access denied page can be defined or a reference to an AccessDeniedHandler instance. + element access-denied-handler {access-denied-handler.attlist, empty} +access-denied-handler.attlist &= (ref | access-denied-handler-page) + +access-denied-handler-page = + ## The access denied page that an authenticated user will be redirected to if they request a page which they don't have the authority to access. + attribute error-page {xsd:token} + +intercept-url = + ## Specifies the access attributes and/or filter list for a particular set of URLs. + element intercept-url {intercept-url.attlist, empty} +intercept-url.attlist &= + (pattern | request-matcher-ref) +intercept-url.attlist &= + ## The access configuration attributes that apply for the configured path. + attribute access {xsd:token}? +intercept-url.attlist &= + ## The HTTP Method for which the access configuration attributes should apply. If not specified, the attributes will apply to any method. + attribute method {"GET" | "DELETE" | "HEAD" | "OPTIONS" | "POST" | "PUT" | "PATCH" | "TRACE"}? + +intercept-url.attlist &= + ## The filter list for the path. Currently can be set to "none" to remove a path from having any filters applied. The full filter stack (consisting of all filters created by the namespace configuration, and any added using 'custom-filter'), will be applied to any other paths. + attribute filters {"none"}? +intercept-url.attlist &= + ## Used to specify that a URL must be accessed over http or https, or that there is no preference. The value should be "http", "https" or "any", respectively. + attribute requires-channel {xsd:token}? +intercept-url.attlist &= + ## The path to the servlet. This attribute is only applicable when 'request-matcher' is 'mvc'. In addition, the value is only required in the following 2 use cases: 1) There are 2 or more HttpServlet's registered in the ServletContext that have mappings starting with '/' and are different; 2) The pattern starts with the same value of a registered HttpServlet path, excluding the default (root) HttpServlet '/'. + attribute servlet-path {xsd:token}? + +logout = + ## Incorporates a logout processing filter. Most web applications require a logout filter, although you may not require one if you write a controller to provider similar logic. + element logout {logout.attlist, empty} +logout.attlist &= + ## Specifies the URL that will cause a logout. Spring Security will initialize a filter that responds to this particular URL. Defaults to /logout if unspecified. + attribute logout-url {xsd:token}? +logout.attlist &= + ## Specifies the URL to display once the user has logged out. If not specified, defaults to /?logout (i.e. /login?logout). + attribute logout-success-url {xsd:token}? +logout.attlist &= + ## Specifies whether a logout also causes HttpSession invalidation, which is generally desirable. If unspecified, defaults to true. + attribute invalidate-session {xsd:boolean}? +logout.attlist &= + ## A reference to a LogoutSuccessHandler implementation which will be used to determine the destination to which the user is taken after logging out. + attribute success-handler-ref {xsd:token}? +logout.attlist &= + ## A comma-separated list of the names of cookies which should be deleted when the user logs out + attribute delete-cookies {xsd:token}? + +request-cache = + ## Allow the RequestCache used for saving requests during the login process to be set + element request-cache {ref} + +form-login = + ## Sets up a form login configuration for authentication with a username and password + element form-login {form-login.attlist, empty} +form-login.attlist &= + ## The URL that the login form is posted to. If unspecified, it defaults to /login. + attribute login-processing-url {xsd:token}? +form-login.attlist &= + ## The name of the request parameter which contains the username. Defaults to 'username'. + attribute username-parameter {xsd:token}? +form-login.attlist &= + ## The name of the request parameter which contains the password. Defaults to 'password'. + attribute password-parameter {xsd:token}? +form-login.attlist &= + ## The URL that will be redirected to after successful authentication, if the user's previous action could not be resumed. This generally happens if the user visits a login page without having first requested a secured operation that triggers authentication. If unspecified, defaults to the root of the application. + attribute default-target-url {xsd:token}? +form-login.attlist &= + ## Whether the user should always be redirected to the default-target-url after login. + attribute always-use-default-target {xsd:boolean}? +form-login.attlist &= + ## The URL for the login page. If no login URL is specified, Spring Security will automatically create a login URL at GET /login and a corresponding filter to render that login URL when requested. + attribute login-page {xsd:token}? +form-login.attlist &= + ## The URL for the login failure page. If no login failure URL is specified, Spring Security will automatically create a failure login URL at /login?error and a corresponding filter to render that login failure URL when requested. + attribute authentication-failure-url {xsd:token}? +form-login.attlist &= + ## Reference to an AuthenticationSuccessHandler bean which should be used to handle a successful authentication request. Should not be used in combination with default-target-url (or always-use-default-target-url) as the implementation should always deal with navigation to the subsequent destination + attribute authentication-success-handler-ref {xsd:token}? +form-login.attlist &= + ## Reference to an AuthenticationFailureHandler bean which should be used to handle a failed authentication request. Should not be used in combination with authentication-failure-url as the implementation should always deal with navigation to the subsequent destination + attribute authentication-failure-handler-ref {xsd:token}? +form-login.attlist &= + ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter + attribute authentication-details-source-ref {xsd:token}? +form-login.attlist &= + ## The URL for the ForwardAuthenticationFailureHandler + attribute authentication-failure-forward-url {xsd:token}? +form-login.attlist &= + ## The URL for the ForwardAuthenticationSuccessHandler + attribute authentication-success-forward-url {xsd:token}? + + +openid-login = + ## Sets up form login for authentication with an Open ID identity + element openid-login {form-login.attlist, user-service-ref?, attribute-exchange*} + +attribute-exchange = + ## Sets up an attribute exchange configuration to request specified attributes from the OpenID identity provider. When multiple elements are used, each must have an identifier-attribute attribute. Each configuration will be matched in turn against the supplied login identifier until a match is found. + element attribute-exchange {attribute-exchange.attlist, openid-attribute+} + +attribute-exchange.attlist &= + ## A regular expression which will be compared against the claimed identity, when deciding which attribute-exchange configuration to use during authentication. + attribute identifier-match {xsd:token}? + +openid-attribute = + ## Attributes used when making an OpenID AX Fetch Request + element openid-attribute {openid-attribute.attlist} + +openid-attribute.attlist &= + ## Specifies the name of the attribute that you wish to get back. For example, email. + attribute name {xsd:token} +openid-attribute.attlist &= + ## Specifies the attribute type. For example, http://axschema.org/contact/email. See your OP's documentation for valid attribute types. + attribute type {xsd:token} +openid-attribute.attlist &= + ## Specifies if this attribute is required to the OP, but does not error out if the OP does not return the attribute. Default is false. + attribute required {xsd:boolean}? +openid-attribute.attlist &= + ## Specifies the number of attributes that you wish to get back. For example, return 3 emails. The default value is 1. + attribute count {xsd:int}? + + +filter-chain-map = + ## Used to explicitly configure a FilterChainProxy instance with a FilterChainMap + element filter-chain-map {filter-chain-map.attlist, filter-chain+} +filter-chain-map.attlist &= + request-matcher? + +filter-chain = + ## Used within to define a specific URL pattern and the list of filters which apply to the URLs matching that pattern. When multiple filter-chain elements are assembled in a list in order to configure a FilterChainProxy, the most specific patterns must be placed at the top of the list, with most general ones at the bottom. + element filter-chain {filter-chain.attlist, empty} +filter-chain.attlist &= + (pattern | request-matcher-ref) +filter-chain.attlist &= + ## A comma separated list of bean names that implement Filter that should be processed for this FilterChain. If the value is none, then no Filters will be used for this FilterChain. + attribute filters {xsd:token} + +pattern = + ## The request URL pattern which will be mapped to the FilterChain. + attribute pattern {xsd:token} +request-matcher-ref = + ## Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + attribute request-matcher-ref {xsd:token} + +filter-security-metadata-source = + ## Used to explicitly configure a FilterSecurityMetadataSource bean for use with a FilterSecurityInterceptor. Usually only needed if you are configuring a FilterChainProxy explicitly, rather than using the element. The intercept-url elements used should only contain pattern, method and access attributes. Any others will result in a configuration error. + element filter-security-metadata-source {fsmds.attlist, intercept-url+} +fsmds.attlist &= + use-expressions? +fsmds.attlist &= + id? +fsmds.attlist &= + request-matcher? + +http-basic = + ## Adds support for basic authentication + element http-basic {http-basic.attlist, empty} + +http-basic.attlist &= + ## Sets the AuthenticationEntryPoint which is used by the BasicAuthenticationFilter. + attribute entry-point-ref {xsd:token}? +http-basic.attlist &= + ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter + attribute authentication-details-source-ref {xsd:token}? + +session-management = + ## Session-management related functionality is implemented by the addition of a SessionManagementFilter to the filter stack. + element session-management {session-management.attlist, concurrency-control?} + +session-management.attlist &= + ## Indicates how session fixation protection will be applied when a user authenticates. If set to "none", no protection will be applied. "newSession" will create a new empty session, with only Spring Security-related attributes migrated. "migrateSession" will create a new session and copy all session attributes to the new session. In Servlet 3.1 (Java EE 7) and newer containers, specifying "changeSessionId" will keep the existing session and use the container-supplied session fixation protection (HttpServletRequest#changeSessionId()). Defaults to "changeSessionId" in Servlet 3.1 and newer containers, "migrateSession" in older containers. Throws an exception if "changeSessionId" is used in older containers. + attribute session-fixation-protection {"none" | "newSession" | "migrateSession" | "changeSessionId" }? +session-management.attlist &= + ## The URL to which a user will be redirected if they submit an invalid session indentifier. Typically used to detect session timeouts. + attribute invalid-session-url {xsd:token}? +session-management.attlist &= + ## Allows injection of the InvalidSessionStrategy instance used by the SessionManagementFilter + attribute invalid-session-strategy-ref {xsd:token}? +session-management.attlist &= + ## Allows injection of the SessionAuthenticationStrategy instance used by the SessionManagementFilter + attribute session-authentication-strategy-ref {xsd:token}? +session-management.attlist &= + ## Defines the URL of the error page which should be shown when the SessionAuthenticationStrategy raises an exception. If not set, an unauthorized (401) error code will be returned to the client. Note that this attribute doesn't apply if the error occurs during a form-based login, where the URL for authentication failure will take precedence. + attribute session-authentication-error-url {xsd:token}? + + +concurrency-control = + ## Enables concurrent session control, limiting the number of authenticated sessions a user may have at the same time. + element concurrency-control {concurrency-control.attlist, empty} + +concurrency-control.attlist &= + ## The maximum number of sessions a single authenticated user can have open at the same time. Defaults to "1". A negative value denotes unlimited sessions. + attribute max-sessions {xsd:integer}? +concurrency-control.attlist &= + ## The URL a user will be redirected to if they attempt to use a session which has been "expired" because they have logged in again. + attribute expired-url {xsd:token}? +concurrency-control.attlist &= + ## Allows injection of the SessionInformationExpiredStrategy instance used by the ConcurrentSessionFilter + attribute expired-session-strategy-ref {xsd:token}? +concurrency-control.attlist &= + ## Specifies that an unauthorized error should be reported when a user attempts to login when they already have the maximum configured sessions open. The default behaviour is to expire the original session. If the session-authentication-error-url attribute is set on the session-management URL, the user will be redirected to this URL. + attribute error-if-maximum-exceeded {xsd:boolean}? +concurrency-control.attlist &= + ## Allows you to define an alias for the SessionRegistry bean in order to access it in your own configuration. + attribute session-registry-alias {xsd:token}? +concurrency-control.attlist &= + ## Allows you to define an external SessionRegistry bean to be used by the concurrency control setup. + attribute session-registry-ref {xsd:token}? + + +remember-me = + ## Sets up remember-me authentication. If used with the "key" attribute (or no attributes) the cookie-only implementation will be used. Specifying "token-repository-ref" or "remember-me-data-source-ref" will use the more secure, persisten token approach. + element remember-me {remember-me.attlist} +remember-me.attlist &= + ## The "key" used to identify cookies from a specific token-based remember-me application. You should set this to a unique value for your application. If unset, it will default to a random value generated by SecureRandom. + attribute key {xsd:token}? + +remember-me.attlist &= + (token-repository-ref | remember-me-data-source-ref | remember-me-services-ref) + +remember-me.attlist &= + user-service-ref? + +remember-me.attlist &= + ## Exports the internally defined RememberMeServices as a bean alias, allowing it to be used by other beans in the application context. + attribute services-alias {xsd:token}? + +remember-me.attlist &= + ## Determines whether the "secure" flag will be set on the remember-me cookie. If set to true, the cookie will only be submitted over HTTPS (recommended). By default, secure cookies will be used if the request is made on a secure connection. + attribute use-secure-cookie {xsd:boolean}? + +remember-me.attlist &= + ## The period (in seconds) for which the remember-me cookie should be valid. + attribute token-validity-seconds {xsd:string}? + +remember-me.attlist &= + ## Reference to an AuthenticationSuccessHandler bean which should be used to handle a successful remember-me authentication. + attribute authentication-success-handler-ref {xsd:token}? +remember-me.attlist &= + ## The name of the request parameter which toggles remember-me authentication. Defaults to 'remember-me'. + attribute remember-me-parameter {xsd:token}? +remember-me.attlist &= + ## The name of cookie which store the token for remember-me authentication. Defaults to 'remember-me'. + attribute remember-me-cookie {xsd:token}? + +token-repository-ref = + ## Reference to a PersistentTokenRepository bean for use with the persistent token remember-me implementation. + attribute token-repository-ref {xsd:token} +remember-me-services-ref = + ## Allows a custom implementation of RememberMeServices to be used. Note that this implementation should return RememberMeAuthenticationToken instances with the same "key" value as specified in the remember-me element. Alternatively it should register its own AuthenticationProvider. It should also implement the LogoutHandler interface, which will be invoked when a user logs out. Typically the remember-me cookie would be removed on logout. + attribute services-ref {xsd:token}? +remember-me-data-source-ref = + ## DataSource bean for the database that contains the token repository schema. + data-source-ref + +anonymous = + ## Adds support for automatically granting all anonymous web requests a particular principal identity and a corresponding granted authority. + element anonymous {anonymous.attlist} +anonymous.attlist &= + ## The key shared between the provider and filter. This generally does not need to be set. If unset, it will default to a random value generated by SecureRandom. + attribute key {xsd:token}? +anonymous.attlist &= + ## The username that should be assigned to the anonymous request. This allows the principal to be identified, which may be important for logging and auditing. if unset, defaults to "anonymousUser". + attribute username {xsd:token}? +anonymous.attlist &= + ## The granted authority that should be assigned to the anonymous request. Commonly this is used to assign the anonymous request particular roles, which can subsequently be used in authorization decisions. If unset, defaults to "ROLE_ANONYMOUS". + attribute granted-authority {xsd:token}? +anonymous.attlist &= + ## With the default namespace setup, the anonymous "authentication" facility is automatically enabled. You can disable it using this property. + attribute enabled {xsd:boolean}? + + +port-mappings = + ## Defines the list of mappings between http and https ports for use in redirects + element port-mappings {port-mappings.attlist, port-mapping+} + +port-mappings.attlist &= empty + +port-mapping = + ## Provides a method to map http ports to https ports when forcing a redirect. + element port-mapping {http-port, https-port} + +http-port = + ## The http port to use. + attribute http {xsd:token} + +https-port = + ## The https port to use. + attribute https {xsd:token} + + +x509 = + ## Adds support for X.509 client authentication. + element x509 {x509.attlist} +x509.attlist &= + ## The regular expression used to obtain the username from the certificate's subject. Defaults to matching on the common name using the pattern "CN=(.*?),". + attribute subject-principal-regex {xsd:token}? +x509.attlist &= + ## Explicitly specifies which user-service should be used to load user data for X.509 authenticated clients. If ommitted, the default user-service will be used. + user-service-ref? +x509.attlist &= + ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter + attribute authentication-details-source-ref {xsd:token}? + +jee = + ## Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integration with container authentication. + element jee {jee.attlist} +jee.attlist &= + ## A comma-separate list of roles to look for in the incoming HttpServletRequest. + attribute mappable-roles {xsd:token} +jee.attlist &= + ## Explicitly specifies which user-service should be used to load user data for container authenticated clients. If ommitted, the set of mappable-roles will be used to construct the authorities for the user. + user-service-ref? + +authentication-manager = + ## Registers the AuthenticationManager instance and allows its list of AuthenticationProviders to be defined. Also allows you to define an alias to allow you to reference the AuthenticationManager in your own beans. + element authentication-manager {authman.attlist & authentication-provider* & ldap-authentication-provider*} +authman.attlist &= + id? +authman.attlist &= + ## An alias you wish to use for the AuthenticationManager bean (not required it you are using a specific id) + attribute alias {xsd:token}? +authman.attlist &= + ## If set to true, the AuthenticationManger will attempt to clear any credentials data in the returned Authentication object, once the user has been authenticated. + attribute erase-credentials {xsd:boolean}? + +authentication-provider = + ## Indicates that the contained user-service should be used as an authentication source. + element authentication-provider {ap.attlist & any-user-service & password-encoder?} +ap.attlist &= + ## Specifies a reference to a separately configured AuthenticationProvider instance which should be registered within the AuthenticationManager. + ref? +ap.attlist &= + ## Specifies a reference to a separately configured UserDetailsService from which to obtain authentication data. + user-service-ref? + +user-service = + ## Creates an in-memory UserDetailsService from a properties file or a list of "user" child elements. Usernames are converted to lower-case internally to allow for case-insensitive lookups, so this should not be used if case-sensitivity is required. + element user-service {id? & (properties-file | (user*))} +properties-file = + ## The location of a Properties file where each line is in the format of username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] + attribute properties {xsd:token}? + +user = + ## Represents a user in the application. + element user {user.attlist, empty} +user.attlist &= + ## The username assigned to the user. + attribute name {xsd:token} +user.attlist &= + ## The password assigned to the user. This may be hashed if the corresponding authentication provider supports hashing (remember to set the "hash" attribute of the "user-service" element). This attribute be omitted in the case where the data will not be used for authentication, but only for accessing authorities. If omitted, the namespace will generate a random value, preventing its accidental use for authentication. Cannot be empty. + attribute password {xsd:string}? +user.attlist &= + ## One of more authorities granted to the user. Separate authorities with a comma (but no space). For example, "ROLE_USER,ROLE_ADMINISTRATOR" + attribute authorities {xsd:token} +user.attlist &= + ## Can be set to "true" to mark an account as locked and unusable. + attribute locked {xsd:boolean}? +user.attlist &= + ## Can be set to "true" to mark an account as disabled and unusable. + attribute disabled {xsd:boolean}? + +jdbc-user-service = + ## Causes creation of a JDBC-based UserDetailsService. + element jdbc-user-service {id? & jdbc-user-service.attlist} +jdbc-user-service.attlist &= + ## The bean ID of the DataSource which provides the required tables. + attribute data-source-ref {xsd:token} +jdbc-user-service.attlist &= + cache-ref? +jdbc-user-service.attlist &= + ## An SQL statement to query a username, password, and enabled status given a username. Default is "select username,password,enabled from users where username = ?" + attribute users-by-username-query {xsd:token}? +jdbc-user-service.attlist &= + ## An SQL statement to query for a user's granted authorities given a username. The default is "select username, authority from authorities where username = ?" + attribute authorities-by-username-query {xsd:token}? +jdbc-user-service.attlist &= + ## An SQL statement to query user's group authorities given a username. The default is "select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id" + attribute group-authorities-by-username-query {xsd:token}? +jdbc-user-service.attlist &= + role-prefix? + +csrf = +## Element for configuration of the CsrfFilter for protection against CSRF. It also updates the default RequestCache to only replay "GET" requests. + element csrf {csrf-options.attlist} +csrf-options.attlist &= + ## Specifies if csrf protection should be disabled. Default false (i.e. CSRF protection is enabled). + attribute disabled {xsd:boolean}? +csrf-options.attlist &= + ## The RequestMatcher instance to be used to determine if CSRF should be applied. Default is any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS" + attribute request-matcher-ref { xsd:token }? +csrf-options.attlist &= + ## The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository + attribute token-repository-ref { xsd:token }? + +headers = +## Element for configuration of the HeaderWritersFilter. Enables easy setting for the X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. +element headers { headers-options.attlist, (cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & hpkp? & content-security-policy? & referrer-policy? & header*)} +headers-options.attlist &= + ## Specifies if the default headers should be disabled. Default false. + attribute defaults-disabled {xsd:boolean}? +headers-options.attlist &= + ## Specifies if headers should be disabled. Default false. + attribute disabled {xsd:boolean}? +hsts = + ## Adds support for HTTP Strict Transport Security (HSTS) + element hsts {hsts-options.attlist} +hsts-options.attlist &= + ## Specifies if HTTP Strict Transport Security (HSTS) should be disabled. Default false. + attribute disabled {xsd:boolean}? +hsts-options.attlist &= + ## Specifies if subdomains should be included. Default true. + attribute include-subdomains {xsd:boolean}? +hsts-options.attlist &= + ## Specifies the maximum ammount of time the host should be considered a Known HSTS Host. Default one year. + attribute max-age-seconds {xsd:integer}? +hsts-options.attlist &= + ## The RequestMatcher instance to be used to determine if the header should be set. Default is if HttpServletRequest.isSecure() is true. + attribute request-matcher-ref { xsd:token }? + +cors = +## Element for configuration of CorsFilter. If no CorsFilter or CorsConfigurationSource is specified a HandlerMappingIntrospector is used as the CorsConfigurationSource +element cors { cors-options.attlist } +cors-options.attlist &= + ref? +cors-options.attlist &= + ## Specifies a bean id that is a CorsConfigurationSource used to construct the CorsFilter to use + attribute configuration-source-ref {xsd:token}? + +hpkp = + ## Adds support for HTTP Public Key Pinning (HPKP). + element hpkp {hpkp.pins,hpkp.attlist} +hpkp.pins = + ## The list with pins + element pins {hpkp.pin+} +hpkp.pin = + ## A pin is specified using the base64-encoded SPKI fingerprint as value and the cryptographic hash algorithm as attribute + element pin { + ## The cryptographic hash algorithm + attribute algorithm { xsd:string }?, + text + } +hpkp.attlist &= + ## Specifies if HTTP Public Key Pinning (HPKP) should be disabled. Default false. + attribute disabled {xsd:boolean}? +hpkp.attlist &= + ## Specifies if subdomains should be included. Default false. + attribute include-subdomains {xsd:boolean}? +hpkp.attlist &= + ## Sets the value for the max-age directive of the Public-Key-Pins header. Default 60 days. + attribute max-age-seconds {xsd:integer}? +hpkp.attlist &= + ## Specifies if the browser should only report pin validation failures. Default true. + attribute report-only {xsd:boolean}? +hpkp.attlist &= + ## Specifies the URI to which the browser should report pin validation failures. + attribute report-uri {xsd:string}? + +content-security-policy = + ## Adds support for Content Security Policy (CSP) + element content-security-policy {csp-options.attlist} +csp-options.attlist &= + ## The security policy directive(s) for the Content-Security-Policy header or if report-only is set to true, then the Content-Security-Policy-Report-Only header is used. + attribute policy-directives {xsd:token}? +csp-options.attlist &= + ## Set to true, to enable the Content-Security-Policy-Report-Only header for reporting policy violations only. Defaults to false. + attribute report-only {xsd:boolean}? + +referrer-policy = + ## Adds support for Referrer Policy + element referrer-policy {referrer-options.attlist} +referrer-options.attlist &= + ## The policies for the Referrer-Policy header. + attribute policy {"no-referrer","no-referrer-when-downgrade","same-origin","origin","strict-origin","origin-when-cross-origin","strict-origin-when-cross-origin","unsafe-url"}? + +cache-control = + ## Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for every request + element cache-control {cache-control.attlist} +cache-control.attlist &= + ## Specifies if Cache Control should be disabled. Default false. + attribute disabled {xsd:boolean}? + +frame-options = + ## Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options header. + element frame-options {frame-options.attlist,empty} +frame-options.attlist &= + ## If disabled, the X-Frame-Options header will not be included. Default false. + attribute disabled {xsd:boolean}? +frame-options.attlist &= + ## Specify the policy to use for the X-Frame-Options-Header. + attribute policy {"DENY","SAMEORIGIN","ALLOW-FROM"}? +frame-options.attlist &= + ## Specify the strategy to use when ALLOW-FROM is chosen. + attribute strategy {"static","whitelist","regexp"}? +frame-options.attlist &= + ## Specify a reference to the custom AllowFromStrategy to use when ALLOW-FROM is chosen. + ref? +frame-options.attlist &= + ## Specify a value to use for the chosen strategy. + attribute value {xsd:string}? +frame-options.attlist &= + ## Specify the request parameter to use for the origin when using a 'whitelist' or 'regexp' based strategy. Default is 'from'. + attribute from-parameter {xsd:string}? + + +xss-protection = + ## Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the X-XSS-Protection header. + element xss-protection {xss-protection.attlist,empty} +xss-protection.attlist &= + ## disable the X-XSS-Protection header. Default is 'false' meaning it is enabled. + attribute disabled {xsd:boolean}? +xss-protection.attlist &= + ## specify that XSS Protection should be explicitly enabled or disabled. Default is 'true' meaning it is enabled. + attribute enabled {xsd:boolean}? +xss-protection.attlist &= + ## Add mode=block to the header or not, default is on. + attribute block {xsd:boolean}? + +content-type-options = + ## Add a X-Content-Type-Options header to the resopnse. Value is always 'nosniff'. + element content-type-options {content-type-options.attlist, empty} +content-type-options.attlist &= + ## If disabled, the X-Content-Type-Options header will not be included. Default false. + attribute disabled {xsd:boolean}? + +header= + ## Add additional headers to the response. + element header {header.attlist} +header.attlist &= + ## The name of the header to add. + attribute name {xsd:token}? +header.attlist &= + ## The value for the header. + attribute value {xsd:token}? +header.attlist &= + ## Reference to a custom HeaderWriter implementation. + ref? + +any-user-service = user-service | jdbc-user-service | ldap-user-service + +custom-filter = + ## Used to indicate that a filter bean declaration should be incorporated into the security filter chain. + element custom-filter {custom-filter.attlist} + +custom-filter.attlist &= + ref + +custom-filter.attlist &= + (after | before | position) + +after = + ## The filter immediately after which the custom-filter should be placed in the chain. This feature will only be needed by advanced users who wish to mix their own filters into the security filter chain and have some knowledge of the standard Spring Security filters. The filter names map to specific Spring Security implementation filters. + attribute after {named-security-filter} +before = + ## The filter immediately before which the custom-filter should be placed in the chain + attribute before {named-security-filter} +position = + ## The explicit position at which the custom-filter should be placed in the chain. Use if you are replacing a standard filter. + attribute position {named-security-filter} + +named-security-filter = "FIRST" | "CHANNEL_FILTER" | "SECURITY_CONTEXT_FILTER" | "CONCURRENT_SESSION_FILTER" | "WEB_ASYNC_MANAGER_FILTER" | "HEADERS_FILTER" | "CORS_FILTER" | "CSRF_FILTER" | "LOGOUT_FILTER" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_FILTER" | "FORM_LOGIN_FILTER" | "OPENID_FILTER" | "LOGIN_PAGE_FILTER" |"LOGOUT_PAGE_FILTER" | "DIGEST_AUTH_FILTER" | "BASIC_AUTH_FILTER" | "REQUEST_CACHE_FILTER" | "SERVLET_API_SUPPORT_FILTER" | "JAAS_API_SUPPORT_FILTER" | "REMEMBER_ME_FILTER" | "ANONYMOUS_FILTER" | "SESSION_MANAGEMENT_FILTER" | "EXCEPTION_TRANSLATION_FILTER" | "FILTER_SECURITY_INTERCEPTOR" | "SWITCH_USER_FILTER" | "LAST" diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.1.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.xsd new file mode 100644 index 00000000000..acb5e85c84e --- /dev/null +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.xsd @@ -0,0 +1,2737 @@ + + + + + + + + Defines the hashing algorithm used on user passwords. Bcrypt is recommended. + + + + + + + + + + + + + Whether a string should be base64 encoded + + + + + + + + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + + + + + + + + + + + + + + + + Specifies an IP port number. Used to configure an embedded LDAP server, for example. + + + + + + + + Specifies a URL. + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + + + Defines a reference to a cache for use with a UserDetailsService. + + + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + + + A reference to an AuthenticationManager bean + + + + + + + + A reference to a DataSource bean + + + + + + + Enables Spring Security debugging infrastructure. This will provide human-readable + (multi-line) debugging information to monitor requests coming into the security filters. + This may include sensitive information, such as request parameters or headers, and should + only be used in a development environment. + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + Defines the hashing algorithm used on user passwords. Bcrypt is recommended. + + + + + + + + + + + + + A non-empty string prefix that will be added to role strings loaded from persistent + storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is + non-empty. + + + + + + + + Enables the use of expressions in the 'access' attributes in <intercept-url> elements + rather than the traditional list of configuration attributes. Defaults to 'true'. If + enabled, each attribute should contain a single boolean expression. If the expression + evaluates to 'true', access will be granted. + + + + + + + Defines an LDAP server location or starts an embedded server. The url indicates the + location of a remote server. If no url is given, an embedded server will be started, + listening on the supplied port number. The port is optional and defaults to 33389. A + Spring LDAP ContextSource bean will be registered for the server with the id supplied. + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + Specifies a URL. + + + + + + Specifies an IP port number. Used to configure an embedded LDAP server, for example. + + + + + + Username (DN) of the "manager" user identity which will be used to authenticate to a + (non-embedded) LDAP server. If omitted, anonymous access will be used. + + + + + + The password for the manager DN. This is required if the manager-dn is specified. + + + + + + Explicitly specifies an ldif file resource to load into an embedded LDAP server. The + default is classpath*:*.ldiff + + + + + + Optional root suffix for the embedded LDAP server. Default is "dc=springframework,dc=org" + + + + + + + + The optional server to use. If omitted, and a default LDAP server is registered (using + <ldap-server> with no Id), that server will be used. + + + + + + + + Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN + of the user. + + + + + + + + Search base for group membership searches. Defaults to "" (searching from the root). + + + + + + + + The LDAP filter used to search for users (optional). For example "(uid={0})". The + substituted parameter is the user's login name. + + + + + + + + Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. + + + + + + + + The LDAP attribute name which contains the role name which will be used within Spring + Security. Defaults to "cn". + + + + + + + + Allows the objectClass of the user entry to be specified. If set, the framework will + attempt to load standard attributes for the defined class into the returned UserDetails + object + + + + + + + + + + + + + + Allows explicit customization of the loaded user object by specifying a + UserDetailsContextMapper bean which will be called with the context information from the + user's directory entry + + + + + + + This element configures a LdapUserDetailsService which is a combination of a + FilterBasedLdapUserSearch and a DefaultLdapAuthoritiesPopulator. + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + The optional server to use. If omitted, and a default LDAP server is registered (using + <ldap-server> with no Id), that server will be used. + + + + + + The LDAP filter used to search for users (optional). For example "(uid={0})". The + substituted parameter is the user's login name. + + + + + + Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. + + + + + + Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN + of the user. + + + + + + Search base for group membership searches. Defaults to "" (searching from the root). + + + + + + The LDAP attribute name which contains the role name which will be used within Spring + Security. Defaults to "cn". + + + + + + Defines a reference to a cache for use with a UserDetailsService. + + + + + + A non-empty string prefix that will be added to role strings loaded from persistent + storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is + non-empty. + + + + + + Allows the objectClass of the user entry to be specified. If set, the framework will + attempt to load standard attributes for the defined class into the returned UserDetails + object + + + + + + + + + + + + Allows explicit customization of the loaded user object by specifying a + UserDetailsContextMapper bean which will be called with the context information from the + user's directory entry + + + + + + + + + The optional server to use. If omitted, and a default LDAP server is registered (using + <ldap-server> with no Id), that server will be used. + + + + + + Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. + + + + + + The LDAP filter used to search for users (optional). For example "(uid={0})". The + substituted parameter is the user's login name. + + + + + + Search base for group membership searches. Defaults to "" (searching from the root). + + + + + + Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN + of the user. + + + + + + The LDAP attribute name which contains the role name which will be used within Spring + Security. Defaults to "cn". + + + + + + A specific pattern used to build the user's DN, for example "uid={0},ou=people". The key + "{0}" must be present and will be substituted with the username. + + + + + + A non-empty string prefix that will be added to role strings loaded from persistent + storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is + non-empty. + + + + + + Allows the objectClass of the user entry to be specified. If set, the framework will + attempt to load standard attributes for the defined class into the returned UserDetails + object + + + + + + + + + + + + Allows explicit customization of the loaded user object by specifying a + UserDetailsContextMapper bean which will be called with the context information from the + user's directory entry + + + + + + + + + The attribute in the directory which contains the user password. Defaults to + "userPassword". + + + + + + Defines the hashing algorithm used on user passwords. Bcrypt is recommended. + + + + + + + + + + + + Can be used inside a bean definition to add a security interceptor to the bean and set up + access configuration attributes for the bean's methods + + + + + + + Defines a protected method and the access control configuration attributes that apply to + it. We strongly advise you NOT to mix "protect" declarations with any services provided + "global-method-security". + + + + + + + + + + + + + + Optional AccessDecisionManager bean ID to be used by the created method security + interceptor. + + + + + + + + + A method name + + + + + + Access configuration attributes list that applies to the method, e.g. "ROLE_A,ROLE_B". + + + + + + + Creates a MethodSecurityMetadataSource instance + + + + + + + Defines a protected method and the access control configuration attributes that apply to + it. We strongly advise you NOT to mix "protect" declarations with any services provided + "global-method-security". + + + + + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + Enables the use of expressions in the 'access' attributes in <intercept-url> elements + rather than the traditional list of configuration attributes. Defaults to 'true'. If + enabled, each attribute should contain a single boolean expression. If the expression + evaluates to 'true', access will be granted. + + + + + + + Provides method security for all beans registered in the Spring application context. + Specifically, beans will be scanned for matches with the ordered list of + "protect-pointcut" sub-elements, Spring Security annotations and/or. Where there is a + match, the beans will automatically be proxied and security authorization applied to the + methods accordingly. If you use and enable all four sources of method security metadata + (ie "protect-pointcut" declarations, expression annotations, @Secured and also JSR250 + security annotations), the metadata sources will be queried in that order. In practical + terms, this enables you to use XML to override method security metadata expressed in + annotations. If using annotations, the order of precedence is EL-based (@PreAuthorize + etc.), @Secured and finally JSR-250. + + + + + + + + Allows the default expression-based mechanism for handling Spring Security's pre and post + invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) to be + replace entirely. Only applies if these annotations are enabled. + + + + + + + Defines the PrePostInvocationAttributeFactory instance which is used to generate pre and + post invocation metadata from the annotated methods. + + + + + + + + + Customizes the PreInvocationAuthorizationAdviceVoter with the ref as the + PreInvocationAuthorizationAdviceVoter for the <pre-post-annotation-handling> element. + + + + + + + + + Customizes the PostInvocationAdviceProvider with the ref as the + PostInvocationAuthorizationAdvice for the <pre-post-annotation-handling> element. + + + + + + + + + + + + Defines the SecurityExpressionHandler instance which will be used if expression-based + access-control is enabled. A default implementation (with no ACL support) will be used if + not supplied. + + + + + + + + + + Defines a protected pointcut and the access control configuration attributes that apply to + it. Every bean registered in the Spring application context that provides a method that + matches the pointcut will receive security authorization. + + + + + + + + + Allows addition of extra AfterInvocationProvider beans which should be called by the + MethodSecurityInterceptor created by global-method-security. + + + + + + + + + + + + + + Specifies whether the use of Spring Security's pre and post invocation annotations + (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this + application context. Defaults to "disabled". + + + + + + + + + + + + Specifies whether the use of Spring Security's @Secured annotations should be enabled for + this application context. Defaults to "disabled". + + + + + + + + + + + + Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). + This will require the javax.annotation.security classes on the classpath. Defaults to + "disabled". + + + + + + + + + + + + Optional AccessDecisionManager bean ID to override the default used for method security. + + + + + + Optional RunAsmanager implementation which will be used by the configured + MethodSecurityInterceptor + + + + + + Allows the advice "order" to be set for the method security interceptor. + + + + + + If true, class based proxying will be used instead of interface based proxying. + + + + + + Can be used to specify that AspectJ should be used instead of the default Spring AOP. If + set, secured classes must be woven with the AnnotationSecurityAspect from the + spring-security-aspects module. + + + + + + + + + + + An external MethodSecurityMetadataSource instance can be supplied which will take priority + over other sources (such as the default annotations). + + + + + + A reference to an AuthenticationManager bean + + + + + + + + + + + + + + + An AspectJ expression, including the 'execution' keyword. For example, 'execution(int + com.foo.TargetObject.countLength(String))' (without the quotes). + + + + + + Access configuration attributes list that applies to all methods matching the pointcut, + e.g. "ROLE_A,ROLE_B" + + + + + + + Allows securing a Message Broker. There are two modes. If no id is specified: ensures that + any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver + registered as a custom argument resolver; ensures that the + SecurityContextChannelInterceptor is automatically registered for the + clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the + clientInboundChannel. If the id is specified, creates a ChannelSecurityInterceptor that + can be manually registered with the clientInboundChannel. + + + + + + + + Defines the SecurityExpressionHandler instance which will be used if expression-based + access-control is enabled. A default implementation (with no ACL support) will be used if + not supplied. + + + + + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. If specified, + explicit configuration within clientInboundChannel is required. If not specified, ensures + that any SimpAnnotationMethodMessageHandler has the + AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures + that the SecurityContextChannelInterceptor is automatically registered for the + clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the + clientInboundChannel. + + + + + + Disables the requirement for CSRF token to be present in the Stomp headers (default + false). Changing the default is useful if it is necessary to allow other origins to make + SockJS connections. + + + + + + + Creates an authorization rule for a websocket message. + + + + + + + + + + The destination ant pattern which will be mapped to the access attribute. For example, /** + matches any message with a destination, /admin/** matches any message that has a + destination that starts with admin. + + + + + + The access configuration attributes that apply for the configured message. For example, + permitAll grants access to anyone, hasRole('ROLE_ADMIN') requires the user have the role + 'ROLE_ADMIN'. + + + + + + The type of message to match on. Valid values are defined in SimpMessageType (i.e. + CONNECT, CONNECT_ACK, HEARTBEAT, MESSAGE, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT, + DISCONNECT_ACK, OTHER). + + + + + + + + + + + + + + + + + + + + Allows a custom instance of HttpFirewall to be injected into the FilterChainProxy created + by the namespace. + + + + + + + + + Container element for HTTP security configuration. Multiple elements can now be defined, + each with a specific pattern to which the enclosed security configuration applies. A + pattern can also be configured to bypass Spring Security's filters completely by setting + the "security" attribute to "none". + + + + + + + Specifies the access attributes and/or filter list for a particular set of URLs. + + + + + + + + + Defines the access-denied strategy that should be used. An access denied page can be + defined or a reference to an AccessDeniedHandler instance. + + + + + + + + + Sets up a form login configuration for authentication with a username and password + + + + + + + + + Sets up form login for authentication with an Open ID identity + + + + + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + + + Adds support for X.509 client authentication. + + + + + + + + + + Adds support for basic authentication + + + + + + + + + Incorporates a logout processing filter. Most web applications require a logout filter, + although you may not require one if you write a controller to provider similar logic. + + + + + + + + + Session-management related functionality is implemented by the addition of a + SessionManagementFilter to the filter stack. + + + + + + + Enables concurrent session control, limiting the number of authenticated sessions a user + may have at the same time. + + + + + + + + + + + + + Sets up remember-me authentication. If used with the "key" attribute (or no attributes) + the cookie-only implementation will be used. Specifying "token-repository-ref" or + "remember-me-data-source-ref" will use the more secure, persisten token approach. + + + + + + + + + Adds support for automatically granting all anonymous web requests a particular principal + identity and a corresponding granted authority. + + + + + + + + + Defines the list of mappings between http and https ports for use in redirects + + + + + + + Provides a method to map http ports to https ports when forcing a redirect. + + + + + + + + + + + + + + + Defines the SecurityExpressionHandler instance which will be used if expression-based + access-control is enabled. A default implementation (with no ACL support) will be used if + not supplied. + + + + + + + + + + + + + + + + + The request URL pattern which will be mapped to the filter chain created by this <http> + element. If omitted, the filter chain will match all requests. + + + + + + When set to 'none', requests matching the pattern attribute will be ignored by Spring + Security. No security filters will be applied and no SecurityContext will be available. If + set, the <http> element must be empty, with no children. + + + + + + + + + + + Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + + + + + + A legacy attribute which automatically registers a login form, BASIC authentication and a + logout URL and logout services. If unspecified, defaults to "false". We'd recommend you + avoid using this and instead explicitly configure the services you require. + + + + + + Enables the use of expressions in the 'access' attributes in <intercept-url> elements + rather than the traditional list of configuration attributes. Defaults to 'true'. If + enabled, each attribute should contain a single boolean expression. If the expression + evaluates to 'true', access will be granted. + + + + + + Controls the eagerness with which an HTTP session is created by Spring Security classes. + If not set, defaults to "ifRequired". If "stateless" is used, this implies that the + application guarantees that it will not create a session. This differs from the use of + "never" which means that Spring Security will not create a session, but will make use of + one if the application does. + + + + + + + + + + + + + + A reference to a SecurityContextRepository bean. This can be used to customize how the + SecurityContext is stored between requests. + + + + + + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + + + + + + + + + + + + + + Provides versions of HttpServletRequest security methods such as isUserInRole() and + getPrincipal() which are implemented by accessing the Spring SecurityContext. Defaults to + "true". + + + + + + If available, runs the request as the Subject acquired from the JaasAuthenticationToken. + Defaults to "false". + + + + + + Optional attribute specifying the ID of the AccessDecisionManager implementation which + should be used for authorizing HTTP requests. + + + + + + Optional attribute specifying the realm name that will be used for all authentication + features that require a realm name (eg BASIC and Digest authentication). If unspecified, + defaults to "Spring Security Application". + + + + + + Allows a customized AuthenticationEntryPoint to be set on the ExceptionTranslationFilter. + + + + + + Corresponds to the observeOncePerRequest property of FilterSecurityInterceptor. Defaults + to "true" + + + + + + Prevents the jsessionid parameter from being added to rendered URLs. Defaults to "true" + (rewriting is disabled). + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + A reference to an AuthenticationManager bean + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + The access denied page that an authenticated user will be redirected to if they request a + page which they don't have the authority to access. + + + + + + + + The access denied page that an authenticated user will be redirected to if they request a + page which they don't have the authority to access. + + + + + + + + + The request URL pattern which will be mapped to the FilterChain. + + + + + + Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + + + + + + The access configuration attributes that apply for the configured path. + + + + + + The HTTP Method for which the access configuration attributes should apply. If not + specified, the attributes will apply to any method. + + + + + + + + + + + + + + + + + + The filter list for the path. Currently can be set to "none" to remove a path from having + any filters applied. The full filter stack (consisting of all filters created by the + namespace configuration, and any added using 'custom-filter'), will be applied to any + other paths. + + + + + + + + + + + Used to specify that a URL must be accessed over http or https, or that there is no + preference. The value should be "http", "https" or "any", respectively. + + + + + + The path to the servlet. This attribute is only applicable when 'request-matcher' is + 'mvc'. In addition, the value is only required in the following 2 use cases: 1) There are + 2 or more HttpServlet's registered in the ServletContext that have mappings starting with + '/' and are different; 2) The pattern starts with the same value of a registered + HttpServlet path, excluding the default (root) HttpServlet '/'. + + + + + + + + + Specifies the URL that will cause a logout. Spring Security will initialize a filter that + responds to this particular URL. Defaults to /logout if unspecified. + + + + + + Specifies the URL to display once the user has logged out. If not specified, defaults to + <form-login-login-page>/?logout (i.e. /login?logout). + + + + + + Specifies whether a logout also causes HttpSession invalidation, which is generally + desirable. If unspecified, defaults to true. + + + + + + A reference to a LogoutSuccessHandler implementation which will be used to determine the + destination to which the user is taken after logging out. + + + + + + A comma-separated list of the names of cookies which should be deleted when the user logs + out + + + + + + + Allow the RequestCache used for saving requests during the login process to be set + + + + + + + + + + + The URL that the login form is posted to. If unspecified, it defaults to /login. + + + + + + The name of the request parameter which contains the username. Defaults to 'username'. + + + + + + The name of the request parameter which contains the password. Defaults to 'password'. + + + + + + The URL that will be redirected to after successful authentication, if the user's previous + action could not be resumed. This generally happens if the user visits a login page + without having first requested a secured operation that triggers authentication. If + unspecified, defaults to the root of the application. + + + + + + Whether the user should always be redirected to the default-target-url after login. + + + + + + The URL for the login page. If no login URL is specified, Spring Security will + automatically create a login URL at GET /login and a corresponding filter to render that + login URL when requested. + + + + + + The URL for the login failure page. If no login failure URL is specified, Spring Security + will automatically create a failure login URL at /login?error and a corresponding filter + to render that login failure URL when requested. + + + + + + Reference to an AuthenticationSuccessHandler bean which should be used to handle a + successful authentication request. Should not be used in combination with + default-target-url (or always-use-default-target-url) as the implementation should always + deal with navigation to the subsequent destination + + + + + + Reference to an AuthenticationFailureHandler bean which should be used to handle a failed + authentication request. Should not be used in combination with authentication-failure-url + as the implementation should always deal with navigation to the subsequent destination + + + + + + Reference to an AuthenticationDetailsSource which will be used by the authentication + filter + + + + + + The URL for the ForwardAuthenticationFailureHandler + + + + + + The URL for the ForwardAuthenticationSuccessHandler + + + + + + + + Sets up an attribute exchange configuration to request specified attributes from the + OpenID identity provider. When multiple elements are used, each must have an + identifier-attribute attribute. Each configuration will be matched in turn against the + supplied login identifier until a match is found. + + + + + + + + + + + + + A regular expression which will be compared against the claimed identity, when deciding + which attribute-exchange configuration to use during authentication. + + + + + + + Attributes used when making an OpenID AX Fetch Request + + + + + + + + + + Specifies the name of the attribute that you wish to get back. For example, email. + + + + + + Specifies the attribute type. For example, http://axschema.org/contact/email. See your + OP's documentation for valid attribute types. + + + + + + Specifies if this attribute is required to the OP, but does not error out if the OP does + not return the attribute. Default is false. + + + + + + Specifies the number of attributes that you wish to get back. For example, return 3 + emails. The default value is 1. + + + + + + + Used to explicitly configure a FilterChainProxy instance with a FilterChainMap + + + + + + + + + + + + + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + + + + + + + + + + + + + + + Used within to define a specific URL pattern and the list of filters which apply to the + URLs matching that pattern. When multiple filter-chain elements are assembled in a list in + order to configure a FilterChainProxy, the most specific patterns must be placed at the + top of the list, with most general ones at the bottom. + + + + + + + + + + The request URL pattern which will be mapped to the FilterChain. + + + + + + Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + + + + + + A comma separated list of bean names that implement Filter that should be processed for + this FilterChain. If the value is none, then no Filters will be used for this FilterChain. + + + + + + + + The request URL pattern which will be mapped to the FilterChain. + + + + + + + + Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + + + + + + + Used to explicitly configure a FilterSecurityMetadataSource bean for use with a + FilterSecurityInterceptor. Usually only needed if you are configuring a FilterChainProxy + explicitly, rather than using the <http> element. The intercept-url elements used should + only contain pattern, method and access attributes. Any others will result in a + configuration error. + + + + + + + Specifies the access attributes and/or filter list for a particular set of URLs. + + + + + + + + + + + + + + Enables the use of expressions in the 'access' attributes in <intercept-url> elements + rather than the traditional list of configuration attributes. Defaults to 'true'. If + enabled, each attribute should contain a single boolean expression. If the expression + evaluates to 'true', access will be granted. + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + + + + + + + + + + + + + + + + + Sets the AuthenticationEntryPoint which is used by the BasicAuthenticationFilter. + + + + + + Reference to an AuthenticationDetailsSource which will be used by the authentication + filter + + + + + + + + + Indicates how session fixation protection will be applied when a user authenticates. If + set to "none", no protection will be applied. "newSession" will create a new empty + session, with only Spring Security-related attributes migrated. "migrateSession" will + create a new session and copy all session attributes to the new session. In Servlet 3.1 + (Java EE 7) and newer containers, specifying "changeSessionId" will keep the existing + session and use the container-supplied session fixation protection + (HttpServletRequest#changeSessionId()). Defaults to "changeSessionId" in Servlet 3.1 and + newer containers, "migrateSession" in older containers. Throws an exception if + "changeSessionId" is used in older containers. + + + + + + + + + + + + + + The URL to which a user will be redirected if they submit an invalid session indentifier. + Typically used to detect session timeouts. + + + + + + Allows injection of the InvalidSessionStrategy instance used by the + SessionManagementFilter + + + + + + Allows injection of the SessionAuthenticationStrategy instance used by the + SessionManagementFilter + + + + + + Defines the URL of the error page which should be shown when the + SessionAuthenticationStrategy raises an exception. If not set, an unauthorized (401) error + code will be returned to the client. Note that this attribute doesn't apply if the error + occurs during a form-based login, where the URL for authentication failure will take + precedence. + + + + + + + + + The maximum number of sessions a single authenticated user can have open at the same time. + Defaults to "1". A negative value denotes unlimited sessions. + + + + + + The URL a user will be redirected to if they attempt to use a session which has been + "expired" because they have logged in again. + + + + + + Allows injection of the SessionInformationExpiredStrategy instance used by the + ConcurrentSessionFilter + + + + + + Specifies that an unauthorized error should be reported when a user attempts to login when + they already have the maximum configured sessions open. The default behaviour is to expire + the original session. If the session-authentication-error-url attribute is set on the + session-management URL, the user will be redirected to this URL. + + + + + + Allows you to define an alias for the SessionRegistry bean in order to access it in your + own configuration. + + + + + + Allows you to define an external SessionRegistry bean to be used by the concurrency + control setup. + + + + + + + + + The "key" used to identify cookies from a specific token-based remember-me application. + You should set this to a unique value for your application. If unset, it will default to a + random value generated by SecureRandom. + + + + + + Reference to a PersistentTokenRepository bean for use with the persistent token + remember-me implementation. + + + + + + A reference to a DataSource bean + + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + Exports the internally defined RememberMeServices as a bean alias, allowing it to be used + by other beans in the application context. + + + + + + Determines whether the "secure" flag will be set on the remember-me cookie. If set to + true, the cookie will only be submitted over HTTPS (recommended). By default, secure + cookies will be used if the request is made on a secure connection. + + + + + + The period (in seconds) for which the remember-me cookie should be valid. + + + + + + Reference to an AuthenticationSuccessHandler bean which should be used to handle a + successful remember-me authentication. + + + + + + The name of the request parameter which toggles remember-me authentication. Defaults to + 'remember-me'. + + + + + + The name of cookie which store the token for remember-me authentication. Defaults to + 'remember-me'. + + + + + + + + Reference to a PersistentTokenRepository bean for use with the persistent token + remember-me implementation. + + + + + + + + Allows a custom implementation of RememberMeServices to be used. Note that this + implementation should return RememberMeAuthenticationToken instances with the same "key" + value as specified in the remember-me element. Alternatively it should register its own + AuthenticationProvider. It should also implement the LogoutHandler interface, which will + be invoked when a user logs out. Typically the remember-me cookie would be removed on + logout. + + + + + + + + + + + + The key shared between the provider and filter. This generally does not need to be set. If + unset, it will default to a random value generated by SecureRandom. + + + + + + The username that should be assigned to the anonymous request. This allows the principal + to be identified, which may be important for logging and auditing. if unset, defaults to + "anonymousUser". + + + + + + The granted authority that should be assigned to the anonymous request. Commonly this is + used to assign the anonymous request particular roles, which can subsequently be used in + authorization decisions. If unset, defaults to "ROLE_ANONYMOUS". + + + + + + With the default namespace setup, the anonymous "authentication" facility is automatically + enabled. You can disable it using this property. + + + + + + + + + + The http port to use. + + + + + + + + The https port to use. + + + + + + + + + The regular expression used to obtain the username from the certificate's subject. + Defaults to matching on the common name using the pattern "CN=(.*?),". + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + Reference to an AuthenticationDetailsSource which will be used by the authentication + filter + + + + + + + Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integration + with container authentication. + + + + + + + + + + A comma-separate list of roles to look for in the incoming HttpServletRequest. + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + + Registers the AuthenticationManager instance and allows its list of + AuthenticationProviders to be defined. Also allows you to define an alias to allow you to + reference the AuthenticationManager in your own beans. + + + + + + + Indicates that the contained user-service should be used as an authentication source. + + + + + + + + element which defines a password encoding strategy. Used by an authentication provider to + convert submitted passwords to hashed versions, for example. + + + + + + + + + + + + + Sets up an ldap authentication provider + + + + + + + Specifies that an LDAP provider should use an LDAP compare operation of the user's + password to authenticate the user + + + + + + + element which defines a password encoding strategy. Used by an authentication provider to + convert submitted passwords to hashed versions, for example. + + + + + + + + + + + + + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + An alias you wish to use for the AuthenticationManager bean (not required it you are using + a specific id) + + + + + + If set to true, the AuthenticationManger will attempt to clear any credentials data in the + returned Authentication object, once the user has been authenticated. + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + + Creates an in-memory UserDetailsService from a properties file or a list of "user" child + elements. Usernames are converted to lower-case internally to allow for case-insensitive + lookups, so this should not be used if case-sensitivity is required. + + + + + + + Represents a user in the application. + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + + + + + The location of a Properties file where each line is in the format of + username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] + + + + + + + + + The username assigned to the user. + + + + + + The password assigned to the user. This may be hashed if the corresponding authentication + provider supports hashing (remember to set the "hash" attribute of the "user-service" + element). This attribute be omitted in the case where the data will not be used for + authentication, but only for accessing authorities. If omitted, the namespace will + generate a random value, preventing its accidental use for authentication. Cannot be + empty. + + + + + + One of more authorities granted to the user. Separate authorities with a comma (but no + space). For example, "ROLE_USER,ROLE_ADMINISTRATOR" + + + + + + Can be set to "true" to mark an account as locked and unusable. + + + + + + Can be set to "true" to mark an account as disabled and unusable. + + + + + + + Causes creation of a JDBC-based UserDetailsService. + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + + + + + The bean ID of the DataSource which provides the required tables. + + + + + + Defines a reference to a cache for use with a UserDetailsService. + + + + + + An SQL statement to query a username, password, and enabled status given a username. + Default is "select username,password,enabled from users where username = ?" + + + + + + An SQL statement to query for a user's granted authorities given a username. The default + is "select username, authority from authorities where username = ?" + + + + + + An SQL statement to query user's group authorities given a username. The default is + "select g.id, g.group_name, ga.authority from groups g, group_members gm, + group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id" + + + + + + A non-empty string prefix that will be added to role strings loaded from persistent + storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is + non-empty. + + + + + + + Element for configuration of the CsrfFilter for protection against CSRF. It also updates + the default RequestCache to only replay "GET" requests. + + + + + + + + + + Specifies if csrf protection should be disabled. Default false (i.e. CSRF protection is + enabled). + + + + + + The RequestMatcher instance to be used to determine if CSRF should be applied. Default is + any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS" + + + + + + The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository + + + + + + + Element for configuration of the HeaderWritersFilter. Enables easy setting for the + X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. + + + + + + + + + + + + + + + + + + + + + Specifies if the default headers should be disabled. Default false. + + + + + + Specifies if headers should be disabled. Default false. + + + + + + + Adds support for HTTP Strict Transport Security (HSTS) + + + + + + + + + + Specifies if HTTP Strict Transport Security (HSTS) should be disabled. Default false. + + + + + + Specifies if subdomains should be included. Default true. + + + + + + Specifies the maximum ammount of time the host should be considered a Known HSTS Host. + Default one year. + + + + + + The RequestMatcher instance to be used to determine if the header should be set. Default + is if HttpServletRequest.isSecure() is true. + + + + + + + Element for configuration of CorsFilter. If no CorsFilter or CorsConfigurationSource is + specified a HandlerMappingIntrospector is used as the CorsConfigurationSource + + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + Specifies a bean id that is a CorsConfigurationSource used to construct the CorsFilter to + use + + + + + + + Adds support for HTTP Public Key Pinning (HPKP). + + + + + + + + + + + + + + + + + + The list with pins + + + + + + + + + + + A pin is specified using the base64-encoded SPKI fingerprint as value and the + cryptographic hash algorithm as attribute + + + + + + The cryptographic hash algorithm + + + + + + + + + Specifies if HTTP Public Key Pinning (HPKP) should be disabled. Default false. + + + + + + Specifies if subdomains should be included. Default false. + + + + + + Sets the value for the max-age directive of the Public-Key-Pins header. Default 60 days. + + + + + + Specifies if the browser should only report pin validation failures. Default true. + + + + + + Specifies the URI to which the browser should report pin validation failures. + + + + + + + Adds support for Content Security Policy (CSP) + + + + + + + + + + The security policy directive(s) for the Content-Security-Policy header or if report-only + is set to true, then the Content-Security-Policy-Report-Only header is used. + + + + + + Set to true, to enable the Content-Security-Policy-Report-Only header for reporting policy + violations only. Defaults to false. + + + + + + + Adds support for Referrer Policy + + + + + + + + + + The policies for the Referrer-Policy header. + + + + + + + + + + + + + + + + + + + Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for + every request + + + + + + + + + + Specifies if Cache Control should be disabled. Default false. + + + + + + + Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options + header. + + + + + + + + + + If disabled, the X-Frame-Options header will not be included. Default false. + + + + + + Specify the policy to use for the X-Frame-Options-Header. + + + + + + + + + + + + + Specify the strategy to use when ALLOW-FROM is chosen. + + + + + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + Specify a value to use for the chosen strategy. + + + + + + Specify the request parameter to use for the origin when using a 'whitelist' or 'regexp' + based strategy. Default is 'from'. + + + + + + + Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the + X-XSS-Protection header. + + + + + + + + + + disable the X-XSS-Protection header. Default is 'false' meaning it is enabled. + + + + + + specify that XSS Protection should be explicitly enabled or disabled. Default is 'true' + meaning it is enabled. + + + + + + Add mode=block to the header or not, default is on. + + + + + + + Add a X-Content-Type-Options header to the resopnse. Value is always 'nosniff'. + + + + + + + + + + If disabled, the X-Content-Type-Options header will not be included. Default false. + + + + + + + Add additional headers to the response. + + + + + + + + + + The name of the header to add. + + + + + + The value for the header. + + + + + + Defines a reference to a Spring bean Id. + + + + + + + + Used to indicate that a filter bean declaration should be incorporated into the security + filter chain. + + + + + + + + + + + The filter immediately after which the custom-filter should be placed in the chain. This + feature will only be needed by advanced users who wish to mix their own filters into the + security filter chain and have some knowledge of the standard Spring Security filters. The + filter names map to specific Spring Security implementation filters. + + + + + + The filter immediately before which the custom-filter should be placed in the chain + + + + + + The explicit position at which the custom-filter should be placed in the chain. Use if you + are replacing a standard filter. + + + + + + + + The filter immediately after which the custom-filter should be placed in the chain. This + feature will only be needed by advanced users who wish to mix their own filters into the + security filter chain and have some knowledge of the standard Spring Security filters. The + filter names map to specific Spring Security implementation filters. + + + + + + + + The filter immediately before which the custom-filter should be placed in the chain + + + + + + + + The explicit position at which the custom-filter should be placed in the chain. Use if you + are replacing a standard filter. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy index 53ea917d451..6a2ae1c8fcf 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy @@ -28,7 +28,7 @@ import javax.servlet.http.HttpServletRequest * */ abstract class AbstractHttpConfigTests extends AbstractXmlConfigTests { - final int AUTO_CONFIG_FILTERS = 14; + final int AUTO_CONFIG_FILTERS = 15; def httpAutoConfig(Closure c) { xml.http(['auto-config': 'true', 'use-expressions':false], c) diff --git a/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy index f84677d3062..eaab25d203b 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy @@ -15,6 +15,7 @@ */ package org.springframework.security.config.http +import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter import org.springframework.security.web.csrf.CsrfFilter import org.springframework.security.web.header.HeaderWriterFilter @@ -113,6 +114,7 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests { Object authProcFilter = filters.next(); assert authProcFilter instanceof UsernamePasswordAuthenticationFilter assert filters.next() instanceof DefaultLoginPageGeneratingFilter + assert filters.next() instanceof DefaultLogoutPageGeneratingFilter assert filters.next() instanceof BasicAuthenticationFilter assert filters.next() instanceof RequestCacheAwareFilter assert filters.next() instanceof SecurityContextHolderAwareRequestFilter @@ -189,7 +191,7 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests { createAppContext() expect: - getFilters("/anything")[8] instanceof AnonymousAuthenticationFilter + getFilters("/anything")[9] instanceof AnonymousAuthenticationFilter } def anonymousFilterIsRemovedIfDisabledFlagSet() { @@ -200,7 +202,7 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests { createAppContext() expect: - !(getFilters("/anything").get(5) instanceof AnonymousAuthenticationFilter) + !(getFilters("/anything").get(9) instanceof AnonymousAuthenticationFilter) } def anonymousCustomAttributesAreSetCorrectly() { diff --git a/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy index 4b7908e5e71..15cf40a8319 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy @@ -420,7 +420,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests { csrf(disabled:true) } createAppContext() - def filter = getFilters("/someurl")[10] + def filter = getFilters("/someurl")[11] expect: filter instanceof SessionManagementFilter diff --git a/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java b/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java index 11a36966e6f..768bdfabac5 100644 --- a/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java +++ b/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java @@ -50,7 +50,7 @@ public class XsdDocumentedTests { String referenceLocation = "../docs/manual/src/docs/asciidoc/_includes/appendix/namespace.adoc"; String schema31xDocumentLocation = "org/springframework/security/config/spring-security-3.1.xsd"; - String schemaDocumentLocation = "org/springframework/security/config/spring-security-5.0.xsd"; + String schemaDocumentLocation = "org/springframework/security/config/spring-security-5.1.xsd"; XmlSupport xml = new XmlSupport(); @@ -142,7 +142,7 @@ public void sizeWhenReadingFilesystemThenIsCorrectNumberOfSchemaFiles() String[] schemas = resource.getFile().getParentFile().list((dir, name) -> name.endsWith(".xsd")); - assertThat(schemas.length).isEqualTo(12) + assertThat(schemas.length).isEqualTo(13) .withFailMessage("the count is equal to 12, if not then schemaDocument needs updating"); } diff --git a/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java b/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java index 31d166252e0..755a14769fa 100644 --- a/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java @@ -528,7 +528,8 @@ public void logoutWhenDefaultConfigurationThenDisabled() this.xml("CsrfEnabled") ).autowire(); - this.mvc.perform(get("/logout")).andExpect(status().isNotFound()); + this.mvc.perform(get("/logout")) + .andExpect(status().isOk()); // renders form to log out but does not perform a redirect // still logged in this.mvc.perform(get("/authenticated")).andExpect(status().isOk()); diff --git a/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java index 7bd087725c4..16ea49c54e2 100644 --- a/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java @@ -22,6 +22,7 @@ import org.springframework.security.web.WebAttributes; import org.springframework.test.web.servlet.MockMvc; +import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.core.IsNot.not; import static org.hamcrest.core.IsNull.nullValue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -84,6 +85,16 @@ public void getLoginWhenAutoConfigThenShowsDefaultLoginPage() this.mvc.perform(get("/login")).andExpect(content().string(expectedContent)); } + @Test + public void getLogoutWhenAutoConfigThenShowsDefaultLogoutPage() + throws Exception { + + this.spring.configLocations(this.xml("AutoConfig")).autowire(); + + this.mvc.perform(get("/logout")) + .andExpect(content().string(containsString("action=\"/logout\""))); + } + @Test public void getLoginWhenConfiguredWithCustomAttributesThenLoginPageReflects() throws Exception { @@ -118,6 +129,8 @@ public void getLoginWhenConfiguredWithCustomAttributesThenLoginPageReflects() + ""; this.mvc.perform(get("/login")).andExpect(content().string(expectedContent)); + + this.mvc.perform(get("/logout")).andExpect(status().is3xxRedirection()); } @Test diff --git a/config/src/test/resources/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests-AutoConfig.xml b/config/src/test/resources/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests-AutoConfig.xml new file mode 100644 index 00000000000..13372dd9441 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests-AutoConfig.xml @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilter.java b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilter.java new file mode 100644 index 00000000000..70186605b07 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilter.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.authentication.ui; + +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.function.Function; + +/** + * Generates a default log out page. + * + * @author Rob Winch + * @since 5.1 + */ +public class DefaultLogoutPageGeneratingFilter extends OncePerRequestFilter { + private RequestMatcher matcher = new AntPathRequestMatcher("/logout", "GET"); + + private Function> resolveHiddenInputs = request -> Collections + .emptyMap(); + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + if (this.matcher.matches(request)) { + renderLogout(request, response); + } else { + filterChain.doFilter(request, response); + } + } + + private void renderLogout(HttpServletRequest request, HttpServletResponse response) + throws IOException { + String page = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " Confirm Log Out?\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
    \n" + + " \n" + + " \n" + + renderHiddenInputs(request) + + " \n" + + " \n" + + "
    \n" + + " \n" + + ""; + + response.setContentType("text/html;charset=UTF-8"); + response.getWriter().write(page); + } + + /** + * Sets a Function used to resolve a Map of the hidden inputs where the key is the + * name of the input and the value is the value of the input. Typically this is used + * to resolve the CSRF token. + * @param resolveHiddenInputs the function to resolve the inputs + */ + public void setResolveHiddenInputs( + Function> resolveHiddenInputs) { + Assert.notNull(resolveHiddenInputs, "resolveHiddenInputs cannot be null"); + this.resolveHiddenInputs = resolveHiddenInputs; + } + + private String renderHiddenInputs(HttpServletRequest request) { + StringBuilder sb = new StringBuilder(); + for(Map.Entry input : this.resolveHiddenInputs.apply(request).entrySet()) { + sb.append("\n"); + } + return sb.toString(); + } +} diff --git a/web/src/test/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilterTests.java new file mode 100644 index 00000000000..bfb17149d6b --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilterTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.authentication.ui; + +import org.junit.Test; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.Collections; + +import static org.hamcrest.core.StringContains.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class DefaultLogoutPageGeneratingFilterTests { + private DefaultLogoutPageGeneratingFilter filter = new DefaultLogoutPageGeneratingFilter(); + + @Test + public void doFilterWhenNoHiddenInputsThenPageRendered() throws Exception { + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new Object()) + .addFilter(this.filter) + .build(); + + mockMvc.perform(get("/logout")) + .andExpect(content().string("\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " Confirm Log Out?\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
    \n" + + "
    \n" + + " \n" + + " \n" + + "
    \n" + + "
    \n" + + " \n" + + "")) + .andExpect(content().contentType("text/html;charset=UTF-8")); + } + + @Test + public void doFilterWhenHiddenInputsSetThenHiddenInputsRendered() throws Exception { + this.filter.setResolveHiddenInputs(r -> Collections.singletonMap("_csrf", "csrf-token-1")); + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new Object()) + .addFilters(this.filter) + .build(); + + mockMvc.perform(get("/logout")) + .andExpect(content().string(containsString(""))); + } + + @Test + public void doFilterWhenRequestContextThenActionContainsRequestContext() throws Exception { + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new Object()) + .addFilters(this.filter) + .build(); + + mockMvc.perform(get("/context/logout").contextPath("/context")) + .andExpect(content().string(containsString("action=\"/context/logout\""))); + } +} From 5b0f37fb6b46f89913baa292104fab65d3e2a361 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Sat, 30 Jun 2018 20:21:02 -0400 Subject: [PATCH 119/226] Add support for custom authorization request parameters Fixes gh-4911 --- .../client/OAuth2ClientConfigurerTests.java | 4 +- ...ultOAuth2AuthorizationRequestResolver.java | 169 +++++++++++ ...th2AuthorizationRequestRedirectFilter.java | 161 +++------- ...AuthorizationRequestRedirectWebFilter.java | 5 +- .../OAuth2AuthorizationRequestResolver.java | 43 +++ .../OAuth2AuthorizationRequestUriBuilder.java | 53 ---- ...uth2AuthorizationRequestResolverTests.java | 242 +++++++++++++++ ...thorizationRequestRedirectFilterTests.java | 175 +++++------ ...h2AuthorizationRequestUriBuilderTests.java | 58 ---- .../endpoint/OAuth2AuthorizationRequest.java | 93 +++++- .../OAuth2AuthorizationRequestTests.java | 285 ++++++++++++------ ...uthorizationCodeGrantApplicationTests.java | 2 +- 12 files changed, 867 insertions(+), 423 deletions(-) create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestResolver.java delete mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestUriBuilder.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java delete mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestUriBuilderTests.java diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java index dfc5a5f9f43..919305eed0d 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java @@ -120,7 +120,7 @@ public void configureWhenAuthorizationCodeRequestThenRedirectForAuthorization() MvcResult mvcResult = this.mockMvc.perform(get("/oauth2/authorization/registration-1")) .andExpect(status().is3xxRedirection()) .andReturn(); - assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/client-1"); + assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fclient-1"); } @Test @@ -168,7 +168,7 @@ public void configureWhenRequestCacheProvidedAndClientAuthorizationRequiredExcep MvcResult mvcResult = this.mockMvc.perform(get("/resource1").with(user("user1"))) .andExpect(status().is3xxRedirection()) .andReturn(); - assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/client-1"); + assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fclient-1"); verify(requestCache).saveRequest(any(HttpServletRequest.class), any(HttpServletResponse.class)); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java new file mode 100644 index 00000000000..1cdf283d1fc --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java @@ -0,0 +1,169 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.client.web; + +import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; +import org.springframework.security.crypto.keygen.StringKeyGenerator; +import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.web.util.UrlUtils; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.util.Assert; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.servlet.http.HttpServletRequest; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import static org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME; + +/** + * An implementation of an {@link OAuth2AuthorizationRequestResolver} that attempts to + * resolve an {@link OAuth2AuthorizationRequest} from the provided {@code HttpServletRequest} + * using the default request {@code URI} pattern {@code /oauth2/authorization/{registrationId}}. + * + *

    + * NOTE: The default base {@code URI} {@code /oauth2/authorization} may be overridden + * via it's constructor {@link #DefaultOAuth2AuthorizationRequestResolver(ClientRegistrationRepository, String)}. + * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AuthorizationRequestResolver + * @see OAuth2AuthorizationRequestRedirectFilter + */ +public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { + private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId"; + private final ClientRegistrationRepository clientRegistrationRepository; + private final AntPathRequestMatcher authorizationRequestMatcher; + private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder()); + + /** + * Constructs a {@code DefaultOAuth2AuthorizationRequestResolver} using the provided parameters. + * + * @param clientRegistrationRepository the repository of client registrations + * @param authorizationRequestBaseUri the base {@code URI} used for resolving authorization requests + */ + public DefaultOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository, + String authorizationRequestBaseUri) { + Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); + Assert.hasText(authorizationRequestBaseUri, "authorizationRequestBaseUri cannot be empty"); + this.clientRegistrationRepository = clientRegistrationRepository; + this.authorizationRequestMatcher = new AntPathRequestMatcher( + authorizationRequestBaseUri + "/{" + REGISTRATION_ID_URI_VARIABLE_NAME + "}"); + } + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { + String registrationId = this.resolveRegistrationId(request); + if (registrationId == null) { + return null; + } + + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); + if (clientRegistration == null) { + throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId); + } + + OAuth2AuthorizationRequest.Builder builder; + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { + builder = OAuth2AuthorizationRequest.authorizationCode(); + } else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) { + builder = OAuth2AuthorizationRequest.implicit(); + } else { + throw new IllegalArgumentException("Invalid Authorization Grant Type (" + + clientRegistration.getAuthorizationGrantType().getValue() + + ") for Client Registration with Id: " + clientRegistration.getRegistrationId()); + } + + String redirectUriAction = this.resolveRedirectUriAction(request, clientRegistration); + String redirectUriStr = this.expandRedirectUri(request, clientRegistration, redirectUriAction); + + Map additionalParameters = new HashMap<>(); + additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()); + + OAuth2AuthorizationRequest authorizationRequest = builder + .clientId(clientRegistration.getClientId()) + .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()) + .redirectUri(redirectUriStr) + .scopes(clientRegistration.getScopes()) + .state(this.stateGenerator.generateKey()) + .additionalParameters(additionalParameters) + .build(); + + return authorizationRequest; + } + + private String resolveRegistrationId(HttpServletRequest request) { + // Check for ClientAuthorizationRequiredException which may have been set + // in the request by OAuth2AuthorizationRequestRedirectFilter + ClientAuthorizationRequiredException authzEx = + (ClientAuthorizationRequiredException) request.getAttribute(AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME); + if (authzEx != null) { + return authzEx.getClientRegistrationId(); + } + if (this.authorizationRequestMatcher.matches(request)) { + return this.authorizationRequestMatcher + .extractUriTemplateVariables(request).get(REGISTRATION_ID_URI_VARIABLE_NAME); + } + return null; + } + + private String resolveRedirectUriAction(HttpServletRequest request, ClientRegistration clientRegistration) { + String action = null; + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { + String loginAction = "login"; + String authorizeAction = "authorize"; + String actionParameter = request.getParameter("action"); + if (request.getAttribute(AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME) != null) { + // Check for ClientAuthorizationRequiredException which may have been set + // in the request by OAuth2AuthorizationRequestRedirectFilter + action = authorizeAction; + } else if (actionParameter == null) { + action = loginAction; // Default + } else { + if (actionParameter.equalsIgnoreCase(loginAction)) { + action = loginAction; + } else { + action = authorizeAction; + } + } + } + return action; + } + + private String expandRedirectUri(HttpServletRequest request, ClientRegistration clientRegistration, String action) { + // Supported URI variables -> baseUrl, action, registrationId + // Used in -> CommonOAuth2Provider.DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}" + Map uriVariables = new HashMap<>(); + uriVariables.put("registrationId", clientRegistration.getRegistrationId()); + String baseUrl = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) + .replacePath(request.getContextPath()) + .build() + .toUriString(); + uriVariables.put("baseUrl", baseUrl); + if (action != null) { + uriVariables.put("action", action); + } + return UriComponentsBuilder.fromUriString(clientRegistration.getRedirectUriTemplate()) + .buildAndExpand(uriVariables) + .toUriString(); + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilter.java index 44fe7270da7..ef6c12975d8 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilter.java @@ -16,34 +16,24 @@ package org.springframework.security.oauth2.client.web; import org.springframework.http.HttpStatus; -import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; -import org.springframework.security.crypto.keygen.StringKeyGenerator; import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; -import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.util.ThrowableAnalyzer; -import org.springframework.security.web.util.UrlUtils; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.util.Assert; import org.springframework.web.filter.OncePerRequestFilter; -import org.springframework.web.util.UriComponentsBuilder; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.net.URI; -import java.util.Base64; -import java.util.HashMap; -import java.util.Map; /** * This {@code Filter} initiates the authorization code grant or implicit grant flow @@ -58,19 +48,24 @@ * *

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

    - * NOTE: The default base {@code URI} {@code /oauth2/authorization} may be overridden - * via it's constructor {@link #OAuth2AuthorizationRequestRedirectFilter(ClientRegistrationRepository, String)}. + * The default base {@code URI} {@code /oauth2/authorization} may be overridden + * via the constructor {@link #OAuth2AuthorizationRequestRedirectFilter(ClientRegistrationRepository, String)}, + * or alternatively, an {@code OAuth2AuthorizationRequestResolver} may be provided to the constructor + * {@link #OAuth2AuthorizationRequestRedirectFilter(OAuth2AuthorizationRequestResolver)} + * to override the resolving of authorization requests. * @author Joe Grandja * @author Rob Winch * @since 5.0 * @see OAuth2AuthorizationRequest + * @see OAuth2AuthorizationRequestResolver * @see AuthorizationRequestRepository * @see ClientRegistration * @see ClientRegistrationRepository @@ -84,18 +79,14 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt * The default base {@code URI} used for authorization requests. */ public static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization"; - private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId"; - private static final String AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME = + static final String AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME = ClientAuthorizationRequiredException.class.getName() + ".AUTHORIZATION_REQUIRED_EXCEPTION"; - private final AntPathRequestMatcher authorizationRequestMatcher; - private final ClientRegistrationRepository clientRegistrationRepository; - private final OAuth2AuthorizationRequestUriBuilder authorizationRequestUriBuilder = new OAuth2AuthorizationRequestUriBuilder(); + private final ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer(); private final RedirectStrategy authorizationRedirectStrategy = new DefaultRedirectStrategy(); - private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder()); + private OAuth2AuthorizationRequestResolver authorizationRequestResolver; private AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository(); private RequestCache requestCache = new HttpSessionRequestCache(); - private final ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer(); /** * Constructs an {@code OAuth2AuthorizationRequestRedirectFilter} using the provided parameters. @@ -112,14 +103,23 @@ public OAuth2AuthorizationRequestRedirectFilter(ClientRegistrationRepository cli * @param clientRegistrationRepository the repository of client registrations * @param authorizationRequestBaseUri the base {@code URI} used for authorization requests */ - public OAuth2AuthorizationRequestRedirectFilter( - ClientRegistrationRepository clientRegistrationRepository, String authorizationRequestBaseUri) { - - Assert.hasText(authorizationRequestBaseUri, "authorizationRequestBaseUri cannot be empty"); + public OAuth2AuthorizationRequestRedirectFilter(ClientRegistrationRepository clientRegistrationRepository, + String authorizationRequestBaseUri) { Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); - this.authorizationRequestMatcher = new AntPathRequestMatcher( - authorizationRequestBaseUri + "/{" + REGISTRATION_ID_URI_VARIABLE_NAME + "}"); - this.clientRegistrationRepository = clientRegistrationRepository; + Assert.hasText(authorizationRequestBaseUri, "authorizationRequestBaseUri cannot be empty"); + this.authorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( + clientRegistrationRepository, authorizationRequestBaseUri); + } + + /** + * Constructs an {@code OAuth2AuthorizationRequestRedirectFilter} using the provided parameters. + * + * @since 5.1 + * @param authorizationRequestResolver the resolver used for resolving authorization requests + */ + public OAuth2AuthorizationRequestRedirectFilter(OAuth2AuthorizationRequestResolver authorizationRequestResolver) { + Assert.notNull(authorizationRequestResolver, "authorizationRequestResolver cannot be null"); + this.authorizationRequestResolver = authorizationRequestResolver; } /** @@ -147,12 +147,14 @@ public final void setRequestCache(RequestCache requestCache) { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - if (this.shouldRequestAuthorization(request, response)) { - try { - this.sendRedirectForAuthorization(request, response); - } catch (Exception failed) { - this.unsuccessfulRedirectForAuthorization(request, response, failed); + try { + OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request); + if (authorizationRequest != null) { + this.sendRedirectForAuthorization(request, response, authorizationRequest); + return; } + } catch (Exception failed) { + this.unsuccessfulRedirectForAuthorization(request, response, failed); return; } @@ -168,7 +170,11 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse if (authzEx != null) { try { request.setAttribute(AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME, authzEx); - this.sendRedirectForAuthorization(request, response, authzEx.getClientRegistrationId()); + OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request); + if (authorizationRequest == null) { + throw authzEx; + } + this.sendRedirectForAuthorization(request, response, authorizationRequest); this.requestCache.saveRequest(request, response); } catch (Exception failed) { this.unsuccessfulRedirectForAuthorization(request, response, failed); @@ -188,61 +194,13 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } } - private boolean shouldRequestAuthorization(HttpServletRequest request, HttpServletResponse response) { - return this.authorizationRequestMatcher.matches(request); - } - - private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response) - throws IOException, ServletException { - - String registrationId = this.authorizationRequestMatcher - .extractUriTemplateVariables(request).get(REGISTRATION_ID_URI_VARIABLE_NAME); - this.sendRedirectForAuthorization(request, response, registrationId); - } - private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response, - String registrationId) throws IOException, ServletException { - - ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); - if (clientRegistration == null) { - throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId); - } - this.sendRedirectForAuthorization(request, response, clientRegistration); - } - - private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response, - ClientRegistration clientRegistration) throws IOException, ServletException { - - String redirectUriStr = this.expandRedirectUri(request, clientRegistration); - - Map additionalParameters = new HashMap<>(); - additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()); - - OAuth2AuthorizationRequest.Builder builder; - if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { - builder = OAuth2AuthorizationRequest.authorizationCode(); - } else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) { - builder = OAuth2AuthorizationRequest.implicit(); - } else { - throw new IllegalArgumentException("Invalid Authorization Grant Type (" + - clientRegistration.getAuthorizationGrantType().getValue() + - ") for Client Registration with Id: " + clientRegistration.getRegistrationId()); - } - OAuth2AuthorizationRequest authorizationRequest = builder - .clientId(clientRegistration.getClientId()) - .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()) - .redirectUri(redirectUriStr) - .scopes(clientRegistration.getScopes()) - .state(this.stateGenerator.generateKey()) - .additionalParameters(additionalParameters) - .build(); + OAuth2AuthorizationRequest authorizationRequest) throws IOException, ServletException { if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationRequest.getGrantType())) { this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response); } - - URI redirectUri = this.authorizationRequestUriBuilder.build(authorizationRequest); - this.authorizationRedirectStrategy.sendRedirect(request, response, redirectUri.toString()); + this.authorizationRedirectStrategy.sendRedirect(request, response, authorizationRequest.getAuthorizationRequestUri()); } private void unsuccessfulRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response, @@ -254,43 +212,6 @@ private void unsuccessfulRedirectForAuthorization(HttpServletRequest request, Ht response.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value(), HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()); } - private String expandRedirectUri(HttpServletRequest request, ClientRegistration clientRegistration) { - // Supported URI variables -> baseUrl, action, registrationId - // Used in -> CommonOAuth2Provider.DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}" - Map uriVariables = new HashMap<>(); - uriVariables.put("registrationId", clientRegistration.getRegistrationId()); - - String baseUrl = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) - .replacePath(request.getContextPath()) - .build() - .toUriString(); - uriVariables.put("baseUrl", baseUrl); - - if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { - String loginAction = "login"; - String authorizeAction = "authorize"; - String actionParameter = "action"; - String action; - if (request.getAttribute(AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME) != null) { - action = authorizeAction; - } else if (request.getParameter(actionParameter) == null) { - action = loginAction; - } else { - String actionValue = request.getParameter(actionParameter); - if (loginAction.equalsIgnoreCase(actionValue)) { - action = loginAction; - } else { - action = authorizeAction; - } - } - uriVariables.put("action", action); - } - - return UriComponentsBuilder.fromUriString(clientRegistration.getRedirectUriTemplate()) - .buildAndExpand(uriVariables) - .toUriString(); - } - private static final class DefaultThrowableAnalyzer extends ThrowableAnalyzer { protected void initExtractorMap() { super.initExtractorMap(); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilter.java index d6fcad1fea7..b350d5ed581 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilter.java @@ -87,7 +87,6 @@ public class OAuth2AuthorizationRequestRedirectWebFilter implements WebFilter { ClientAuthorizationRequiredException.class.getName() + ".AUTHORIZATION_REQUIRED_EXCEPTION"; private final ServerWebExchangeMatcher authorizationRequestMatcher; private final ReactiveClientRegistrationRepository clientRegistrationRepository; - private final OAuth2AuthorizationRequestUriBuilder authorizationRequestUriBuilder = new OAuth2AuthorizationRequestUriBuilder(); private final ServerRedirectStrategy authorizationRedirectStrategy = new DefaultServerRedirectStrategy(); private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder()); private ReactiveAuthorizationRequestRepository authorizationRequestRepository = @@ -184,7 +183,9 @@ else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizat .saveAuthorizationRequest(authorizationRequest, exchange); } - URI redirectUri = this.authorizationRequestUriBuilder.build(authorizationRequest); + URI redirectUri = UriComponentsBuilder + .fromUriString(authorizationRequest.getAuthorizationRequestUri()) + .build(true).toUri(); return saveAuthorizationRequest .then(this.authorizationRedirectStrategy.sendRedirect(exchange, redirectUri)); }); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestResolver.java new file mode 100644 index 00000000000..1e6a9c03e60 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestResolver.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.client.web; + +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; + +import javax.servlet.http.HttpServletRequest; + +/** + * Implementations of this interface are capable of resolving + * an {@link OAuth2AuthorizationRequest} from the provided {@code HttpServletRequest}. + * Used by the {@link OAuth2AuthorizationRequestRedirectFilter} for resolving Authorization Requests. + * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AuthorizationRequest + * @see OAuth2AuthorizationRequestRedirectFilter + */ +public interface OAuth2AuthorizationRequestResolver { + + /** + * Returns the {@link OAuth2AuthorizationRequest} resolved from + * the provided {@code HttpServletRequest} or {@code null} if not available. + * + * @param request the {@code HttpServletRequest} + * @return the resolved {@link OAuth2AuthorizationRequest} or {@code null} if not available + */ + OAuth2AuthorizationRequest resolve(HttpServletRequest request); + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestUriBuilder.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestUriBuilder.java deleted file mode 100644 index 3a9635f9e9c..00000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestUriBuilder.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2002-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.oauth2.client.web; - -import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; -import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; -import org.springframework.web.util.UriComponentsBuilder; - -import java.net.URI; -import java.util.Set; - -/** - * A {@code URI} builder for an OAuth 2.0 Authorization Request. - * - * @author Joe Grandja - * @since 5.0 - * @see OAuth2AuthorizationRequest - * @see Section 4.1.1 Authorization Code Grant Request - * @see Section 4.2.1 Implicit Grant Request - */ -class OAuth2AuthorizationRequestUriBuilder { - - URI build(OAuth2AuthorizationRequest authorizationRequest) { - Assert.notNull(authorizationRequest, "authorizationRequest cannot be null"); - Set scopes = authorizationRequest.getScopes(); - UriComponentsBuilder uriBuilder = UriComponentsBuilder - .fromUriString(authorizationRequest.getAuthorizationUri()) - .queryParam(OAuth2ParameterNames.RESPONSE_TYPE, authorizationRequest.getResponseType().getValue()) - .queryParam(OAuth2ParameterNames.CLIENT_ID, authorizationRequest.getClientId()) - .queryParam(OAuth2ParameterNames.SCOPE, StringUtils.collectionToDelimitedString(scopes, " ")) - .queryParam(OAuth2ParameterNames.STATE, authorizationRequest.getState()); - if (authorizationRequest.getRedirectUri() != null) { - uriBuilder.queryParam(OAuth2ParameterNames.REDIRECT_URI, authorizationRequest.getRedirectUri()); - } - - return uriBuilder.build().encode().toUri(); - } -} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java new file mode 100644 index 00000000000..2ff722aa024 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java @@ -0,0 +1,242 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.client.web; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; + +import static org.assertj.core.api.Assertions.*; + +/** + * Tests for {@link DefaultOAuth2AuthorizationRequestResolver}. + * + * @author Joe Grandja + */ +public class DefaultOAuth2AuthorizationRequestResolverTests { + private ClientRegistration registration1; + private ClientRegistration registration2; + private ClientRegistrationRepository clientRegistrationRepository; + private String authorizationRequestBaseUri = "/oauth2/authorization"; + private DefaultOAuth2AuthorizationRequestResolver resolver; + + @Before + public void setUp() { + this.registration1 = ClientRegistration.withRegistrationId("registration-1") + .clientId("client-1") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}") + .scope("user") + .authorizationUri("https://provider.com/oauth2/authorize") + .tokenUri("https://provider.com/oauth2/token") + .userInfoUri("https://provider.com/oauth2/user") + .userNameAttributeName("id") + .clientName("client-1") + .build(); + this.registration2 = ClientRegistration.withRegistrationId("registration-2") + .clientId("client-2") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}") + .scope("openid", "profile", "email") + .authorizationUri("https://provider.com/oauth2/authorize") + .tokenUri("https://provider.com/oauth2/token") + .userInfoUri("https://provider.com/oauth2/userinfo") + .jwkSetUri("https://provider.com/oauth2/keys") + .clientName("client-2") + .build(); + this.clientRegistrationRepository = new InMemoryClientRegistrationRepository( + this.registration1, this.registration2); + this.resolver = new DefaultOAuth2AuthorizationRequestResolver( + this.clientRegistrationRepository, this.authorizationRequestBaseUri); + } + + @Test + public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new DefaultOAuth2AuthorizationRequestResolver(null, this.authorizationRequestBaseUri)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenAuthorizationRequestBaseUriIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new DefaultOAuth2AuthorizationRequestResolver(this.clientRegistrationRepository, null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void resolveWhenNotAuthorizationRequestThenDoesNotResolve() { + String requestUri = "/path"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest).isNull(); + } + + @Test + public void resolveWhenAuthorizationRequestWithInvalidClientThenThrowIllegalArgumentException() { + ClientRegistration clientRegistration = this.registration1; + String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId() + "-invalid"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + + assertThatThrownBy(() -> this.resolver.resolve(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid Client Registration with Id: " + clientRegistration.getRegistrationId() + "-invalid"); + } + + @Test + public void resolveWhenAuthorizationRequestWithValidClientThenResolves() { + ClientRegistration clientRegistration = this.registration1; + String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest).isNotNull(); + assertThat(authorizationRequest.getAuthorizationUri()).isEqualTo( + clientRegistration.getProviderDetails().getAuthorizationUri()); + assertThat(authorizationRequest.getGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + assertThat(authorizationRequest.getResponseType()).isEqualTo(OAuth2AuthorizationResponseType.CODE); + assertThat(authorizationRequest.getClientId()).isEqualTo(clientRegistration.getClientId()); + assertThat(authorizationRequest.getRedirectUri()) + .isEqualTo("http://localhost/login/oauth2/code/" + clientRegistration.getRegistrationId()); + assertThat(authorizationRequest.getScopes()).isEqualTo(clientRegistration.getScopes()); + assertThat(authorizationRequest.getState()).isNotNull(); + assertThat(authorizationRequest.getAdditionalParameters()) + .containsExactly(entry(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId())); + assertThat(authorizationRequest.getAuthorizationRequestUri()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fregistration-1"); + } + + @Test + public void resolveWhenClientAuthorizationRequiredExceptionAvailableThenResolves() { + ClientRegistration clientRegistration = this.registration2; + String requestUri = "/path"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + request.setAttribute( + OAuth2AuthorizationRequestRedirectFilter.AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME, + new ClientAuthorizationRequiredException(clientRegistration.getRegistrationId())); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest).isNotNull(); + assertThat(authorizationRequest.getAdditionalParameters()) + .containsExactly(entry(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId())); + } + + @Test + public void resolveWhenAuthorizationRequestRedirectUriTemplatedThenRedirectUriExpanded() { + ClientRegistration clientRegistration = this.registration2; + String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest.getRedirectUri()).isNotEqualTo( + clientRegistration.getRedirectUriTemplate()); + assertThat(authorizationRequest.getRedirectUri()).isEqualTo( + "http://localhost/login/oauth2/code/" + clientRegistration.getRegistrationId()); + } + + @Test + public void resolveWhenAuthorizationRequestIncludesPort80ThenExpandedRedirectUriExcludesPort() { + ClientRegistration clientRegistration = this.registration1; + String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setScheme("http"); + request.setServerName("example.com"); + request.setServerPort(80); + request.setServletPath(requestUri); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest.getAuthorizationRequestUri()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Fexample.com%2Flogin%2Foauth2%2Fcode%2Fregistration-1"); + } + + @Test + public void resolveWhenAuthorizationRequestIncludesPort443ThenExpandedRedirectUriExcludesPort() { + ClientRegistration clientRegistration = this.registration1; + String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setScheme("https"); + request.setServerName("example.com"); + request.setServerPort(443); + request.setServletPath(requestUri); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest.getAuthorizationRequestUri()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=https%3A%2F%2Fexample.com%2Flogin%2Foauth2%2Fcode%2Fregistration-1"); + } + + @Test + public void resolveWhenClientAuthorizationRequiredExceptionAvailableThenRedirectUriIsAuthorize() { + ClientRegistration clientRegistration = this.registration1; + String requestUri = "/path"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + request.setAttribute( + OAuth2AuthorizationRequestRedirectFilter.AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME, + new ClientAuthorizationRequiredException(clientRegistration.getRegistrationId())); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest.getAuthorizationRequestUri()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fauthorize%2Foauth2%2Fcode%2Fregistration-1"); + } + + @Test + public void resolveWhenAuthorizationRequestOAuth2LoginThenRedirectUriIsLogin() { + ClientRegistration clientRegistration = this.registration2; + String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest.getAuthorizationRequestUri()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-2&scope=openid\\+profile\\+email&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fregistration-2"); + } + + @Test + public void resolveWhenAuthorizationRequestHasActionParameterAuthorizeThenRedirectUriIsAuthorize() { + ClientRegistration clientRegistration = this.registration1; + String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.addParameter("action", "authorize"); + request.setServletPath(requestUri); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest.getAuthorizationRequestUri()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fauthorize%2Foauth2%2Fcode%2Fregistration-1"); + } + + @Test + public void resolveWhenAuthorizationRequestHasActionParameterLoginThenRedirectUriIsLogin() { + ClientRegistration clientRegistration = this.registration2; + String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.addParameter("action", "login"); + request.setServletPath(requestUri); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest.getAuthorizationRequestUri()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-2&scope=openid\\+profile\\+email&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fregistration-2"); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilterTests.java index 3412c852d10..b8dadbaf728 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ import org.junit.Before; import org.junit.Test; -import org.mockito.ArgumentCaptor; import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -29,16 +28,22 @@ import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.web.savedrequest.RequestCache; +import org.springframework.util.ClassUtils; +import org.springframework.web.util.UriComponentsBuilder; import javax.servlet.FilterChain; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.lang.reflect.Constructor; +import java.util.HashMap; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.*; +import static org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME; /** * Tests for {@link OAuth2AuthorizationRequestRedirectFilter}. @@ -100,7 +105,9 @@ public void setUp() { @Test public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2AuthorizationRequestRedirectFilter(null)) + Constructor constructor = ClassUtils.getConstructorIfAvailable( + OAuth2AuthorizationRequestRedirectFilter.class, ClientRegistrationRepository.class); + assertThatThrownBy(() -> constructor.newInstance(null)) .isInstanceOf(IllegalArgumentException.class); } @@ -110,6 +117,14 @@ public void constructorWhenAuthorizationRequestBaseUriIsNullThenThrowIllegalArgu .isInstanceOf(IllegalArgumentException.class); } + @Test + public void constructorWhenAuthorizationRequestResolverIsNullThenThrowIllegalArgumentException() { + Constructor constructor = ClassUtils.getConstructorIfAvailable( + OAuth2AuthorizationRequestRedirectFilter.class, OAuth2AuthorizationRequestResolver.class); + assertThatThrownBy(() -> constructor.newInstance(null)) + .isInstanceOf(IllegalArgumentException.class); + } + @Test public void setAuthorizationRequestRepositoryWhenAuthorizationRequestRepositoryIsNullThenThrowIllegalArgumentException() { assertThatThrownBy(() -> this.filter.setAuthorizationRequestRepository(null)) @@ -165,7 +180,7 @@ public void doFilterWhenAuthorizationRequestOAuth2LoginThenRedirectForAuthorizat verifyZeroInteractions(filterChain); - assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/login/oauth2/code/registration-1"); + assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fregistration-1"); } @Test @@ -201,7 +216,7 @@ public void doFilterWhenAuthorizationRequestImplicitGrantThenRedirectForAuthoriz verifyZeroInteractions(filterChain); - assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=token&client_id=client-3&scope=openid%20profile%20email&state=.{15,}&redirect_uri=http://localhost/authorize/oauth2/implicit/registration-3"); + assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=token&client_id=client-3&scope=openid\\+profile\\+email&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fauthorize%2Foauth2%2Fimplicit%2Fregistration-3"); } @Test @@ -239,75 +254,7 @@ public void doFilterWhenCustomAuthorizationRequestBaseUriThenRedirectForAuthoriz verifyZeroInteractions(filterChain); - assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/login/oauth2/code/registration-1"); - } - - @Test - public void doFilterWhenAuthorizationRequestRedirectUriTemplatedThenRedirectUriExpanded() throws Exception { - String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + - "/" + this.registration2.getRegistrationId(); - MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); - request.setServletPath(requestUri); - MockHttpServletResponse response = new MockHttpServletResponse(); - FilterChain filterChain = mock(FilterChain.class); - - AuthorizationRequestRepository authorizationRequestRepository = - mock(AuthorizationRequestRepository.class); - this.filter.setAuthorizationRequestRepository(authorizationRequestRepository); - - this.filter.doFilter(request, response, filterChain); - - ArgumentCaptor authorizationRequestArgCaptor = - ArgumentCaptor.forClass(OAuth2AuthorizationRequest.class); - - verifyZeroInteractions(filterChain); - verify(authorizationRequestRepository).saveAuthorizationRequest( - authorizationRequestArgCaptor.capture(), any(HttpServletRequest.class), any(HttpServletResponse.class)); - - OAuth2AuthorizationRequest authorizationRequest = authorizationRequestArgCaptor.getValue(); - - assertThat(authorizationRequest.getRedirectUri()).isNotEqualTo( - this.registration2.getRedirectUriTemplate()); - assertThat(authorizationRequest.getRedirectUri()).isEqualTo( - "http://localhost/login/oauth2/code/registration-2"); - } - - @Test - public void doFilterWhenAuthorizationRequestIncludesPort80ThenExpandedRedirectUriExcludesPort() throws Exception { - String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + - "/" + this.registration1.getRegistrationId(); - MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); - request.setScheme("http"); - request.setServerName("example.com"); - request.setServerPort(80); - request.setServletPath(requestUri); - MockHttpServletResponse response = new MockHttpServletResponse(); - FilterChain filterChain = mock(FilterChain.class); - - this.filter.doFilter(request, response, filterChain); - - verifyZeroInteractions(filterChain); - - assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://example.com/login/oauth2/code/registration-1"); - } - - @Test - public void doFilterWhenAuthorizationRequestIncludesPort443ThenExpandedRedirectUriExcludesPort() throws Exception { - String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + - "/" + this.registration1.getRegistrationId(); - MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); - request.setScheme("https"); - request.setServerName("example.com"); - request.setServerPort(443); - request.setServletPath(requestUri); - MockHttpServletResponse response = new MockHttpServletResponse(); - FilterChain filterChain = mock(FilterChain.class); - - this.filter.doFilter(request, response, filterChain); - - verifyZeroInteractions(filterChain); - - assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=https://example.com/login/oauth2/code/registration-1"); + assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fregistration-1"); } @Test @@ -325,13 +272,13 @@ public void doFilterWhenNotAuthorizationRequestAndClientAuthorizationRequiredExc verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); - assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/authorize/oauth2/code/registration-1"); - + assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fauthorize%2Foauth2%2Fcode%2Fregistration-1"); verify(this.requestCache).saveRequest(any(HttpServletRequest.class), any(HttpServletResponse.class)); + assertThat(request.getAttribute(AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME)).isNull(); } @Test - public void doFilterWhenNotAuthorizationRequestAndClientAuthorizationRequiredExceptionThrownThenRedirectUriIsAuthorize() throws Exception { + public void doFilterWhenNotAuthorizationRequestAndClientAuthorizationRequiredExceptionThrownButAuthorizationRequestNotResolvedThenStatusInternalServerError() throws Exception { String requestUri = "/path"; MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); request.setServletPath(requestUri); @@ -341,60 +288,84 @@ public void doFilterWhenNotAuthorizationRequestAndClientAuthorizationRequiredExc doThrow(new ClientAuthorizationRequiredException(this.registration1.getRegistrationId())) .when(filterChain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); - this.filter.doFilter(request, response, filterChain); + OAuth2AuthorizationRequestResolver resolver = req -> null; + OAuth2AuthorizationRequestRedirectFilter filter = new OAuth2AuthorizationRequestRedirectFilter(resolver); - verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + filter.doFilter(request, response, filterChain); - assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/authorize/oauth2/code/registration-1"); - } - - @Test - public void doFilterWhenAuthorizationRequestOAuth2LoginThenRedirectUriIsLogin() throws Exception { - String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + - "/" + this.registration2.getRegistrationId(); - MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); - request.setServletPath(requestUri); - MockHttpServletResponse response = new MockHttpServletResponse(); - FilterChain filterChain = mock(FilterChain.class); - - this.filter.doFilter(request, response, filterChain); + verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); verifyZeroInteractions(filterChain); - assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-2&scope=openid%20profile%20email&state=.{15,}&redirect_uri=http://localhost/login/oauth2/code/registration-2"); + assertThat(response.getStatus()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value()); + assertThat(response.getErrorMessage()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()); } + // gh-4911 @Test - public void doFilterWhenAuthorizationRequestHasActionParameterAuthorizeThenRedirectUriIsAuthorize() throws Exception { + public void doFilterWhenAuthorizationRequestAndAdditionalParametersProvidedThenAuthorizationRequestIncludesAdditionalParameters() throws Exception { String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/" + this.registration1.getRegistrationId(); MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); - request.addParameter("action", "authorize"); request.setServletPath(requestUri); + request.addParameter("idp", "https://other.provider.com"); MockHttpServletResponse response = new MockHttpServletResponse(); FilterChain filterChain = mock(FilterChain.class); - this.filter.doFilter(request, response, filterChain); + OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( + this.clientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI); + + OAuth2AuthorizationRequestResolver resolver = req -> { + OAuth2AuthorizationRequest defaultAuthorizationRequest = defaultAuthorizationRequestResolver.resolve(req); + Map additionalParameters = new HashMap<>(defaultAuthorizationRequest.getAdditionalParameters()); + additionalParameters.put("idp", req.getParameter("idp")); + return OAuth2AuthorizationRequest.from(defaultAuthorizationRequest) + .additionalParameters(additionalParameters) + .build(); + }; + OAuth2AuthorizationRequestRedirectFilter filter = new OAuth2AuthorizationRequestRedirectFilter(resolver); + + filter.doFilter(request, response, filterChain); verifyZeroInteractions(filterChain); - assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/authorize/oauth2/code/registration-1"); + assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fregistration-1&idp=https%3A%2F%2Fother.provider.com"); } + // gh-4911, gh-5244 @Test - public void doFilterWhenAuthorizationRequestHasActionParameterLoginThenRedirectUriIsLogin() throws Exception { + public void doFilterWhenAuthorizationRequestAndCustomAuthorizationRequestUriSetThenCustomAuthorizationRequestUriUsed() throws Exception { String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + - "/" + this.registration2.getRegistrationId(); + "/" + this.registration1.getRegistrationId(); MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); - request.addParameter("action", "login"); request.setServletPath(requestUri); + String loginHintParamName = "login_hint"; + request.addParameter(loginHintParamName, "user@provider.com"); MockHttpServletResponse response = new MockHttpServletResponse(); FilterChain filterChain = mock(FilterChain.class); - this.filter.doFilter(request, response, filterChain); + OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( + this.clientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI); + + OAuth2AuthorizationRequestResolver resolver = req -> { + OAuth2AuthorizationRequest defaultAuthorizationRequest = defaultAuthorizationRequestResolver.resolve(req); + Map additionalParameters = new HashMap<>(defaultAuthorizationRequest.getAdditionalParameters()); + additionalParameters.put(loginHintParamName, req.getParameter(loginHintParamName)); + String customAuthorizationRequestUri = UriComponentsBuilder + .fromUriString(defaultAuthorizationRequest.getAuthorizationRequestUri()) + .queryParam(loginHintParamName, additionalParameters.get(loginHintParamName)) + .build(true).toUriString(); + return OAuth2AuthorizationRequest.from(defaultAuthorizationRequest) + .additionalParameters(additionalParameters) + .authorizationRequestUri(customAuthorizationRequestUri) + .build(); + }; + OAuth2AuthorizationRequestRedirectFilter filter = new OAuth2AuthorizationRequestRedirectFilter(resolver); + + filter.doFilter(request, response, filterChain); verifyZeroInteractions(filterChain); - assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-2&scope=openid%20profile%20email&state=.{15,}&redirect_uri=http://localhost/login/oauth2/code/registration-2"); + assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fregistration-1&login_hint=user@provider\\.com"); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestUriBuilderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestUriBuilderTests.java deleted file mode 100644 index 0fde656c870..00000000000 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestUriBuilderTests.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2002-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.oauth2.client.web; - -import org.junit.Test; -import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; - -import java.net.URI; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link OAuth2AuthorizationRequestUriBuilder}. - * - * @author Rob Winch - * @since 5.0 - */ -public class OAuth2AuthorizationRequestUriBuilderTests { - private OAuth2AuthorizationRequestUriBuilder builder = new OAuth2AuthorizationRequestUriBuilder(); - - @Test(expected = IllegalArgumentException.class) - public void buildWhenAuthorizationRequestIsNullThenThrowIllegalArgumentException() { - this.builder.build(null); - } - - @Test - public void buildWhenScopeMultiThenSeparatedByEncodedSpace() { - OAuth2AuthorizationRequest request = OAuth2AuthorizationRequest.implicit() - .additionalParameters(Collections.singletonMap("foo", "bar")) - .authorizationUri("https://idp.example.com/oauth2/v2/auth") - .clientId("client-id") - .state("thestate") - .redirectUri("https://client.example.com/login/oauth2") - .scopes(new HashSet<>(Arrays.asList("openid", "user"))) - .build(); - - URI result = this.builder.build(request); - - assertThat(result.toASCIIString()).isEqualTo("https://idp.example.com/oauth2/v2/auth?response_type=token&client_id=client-id&scope=openid%20user&state=thestate&redirect_uri=https://client.example.com/login/oauth2"); - } -} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequest.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequest.java index 78eaac5598c..3db5f3541d7 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequest.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,14 +19,18 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; +import java.util.StringJoiner; import java.util.stream.Collectors; /** @@ -50,6 +54,7 @@ public final class OAuth2AuthorizationRequest implements Serializable { private Set scopes; private String state; private Map additionalParameters; + private String authorizationRequestUri; private OAuth2AuthorizationRequest() { } @@ -126,6 +131,20 @@ public Map getAdditionalParameters() { return this.additionalParameters; } + /** + * Returns the {@code URI} string representation of the OAuth 2.0 Authorization Request. + * + *

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

    + * NOTE: The {@code URI} string is required to be encoded in the + * {@code application/x-www-form-urlencoded} MIME format. + * + * @since 5.1 + * @param authorizationRequestUri the {@code URI} string representation of the OAuth 2.0 Authorization Request + * @return the {@link Builder} + */ + public Builder authorizationRequestUri(String authorizationRequestUri) { + this.authorizationRequestUri = authorizationRequestUri; + return this; + } + /** * Builds a new {@link OAuth2AuthorizationRequest}. * @@ -272,7 +328,42 @@ public OAuth2AuthorizationRequest build() { authorizationRequest.additionalParameters = Collections.unmodifiableMap( CollectionUtils.isEmpty(this.additionalParameters) ? Collections.emptyMap() : new LinkedHashMap<>(this.additionalParameters)); + authorizationRequest.authorizationRequestUri = + StringUtils.hasText(this.authorizationRequestUri) ? + this.authorizationRequestUri : this.buildAuthorizationRequestUri(); + return authorizationRequest; } + + private String buildAuthorizationRequestUri() { + Map parameters = new LinkedHashMap<>(); + parameters.put(OAuth2ParameterNames.RESPONSE_TYPE, this.responseType.getValue()); + parameters.put(OAuth2ParameterNames.CLIENT_ID, this.clientId); + if (!CollectionUtils.isEmpty(this.scopes)) { + parameters.put(OAuth2ParameterNames.SCOPE, + StringUtils.collectionToDelimitedString(this.scopes, " ")); + } + if (this.state != null) { + parameters.put(OAuth2ParameterNames.STATE, this.state); + } + if (this.redirectUri != null) { + parameters.put(OAuth2ParameterNames.REDIRECT_URI, this.redirectUri); + } + if (!CollectionUtils.isEmpty(this.additionalParameters)) { + this.additionalParameters.entrySet().stream() + .filter(e -> !e.getKey().equals(OAuth2ParameterNames.REGISTRATION_ID)) + .forEach(e -> parameters.put(e.getKey(), e.getValue().toString())); + } + + try { + StringJoiner queryParams = new StringJoiner("&"); + for (String paramName : parameters.keySet()) { + queryParams.add(paramName + "=" + URLEncoder.encode(parameters.get(paramName), "UTF-8")); + } + return this.authorizationUri + "?" + queryParams.toString(); + } catch (UnsupportedEncodingException ex) { + throw new IllegalArgumentException("Unable to build authorization request uri: " + ex.getMessage(), ex); + } + } } } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequestTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequestTests.java index 70ccd4caf15..d61516785a1 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequestTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ package org.springframework.security.oauth2.core.endpoint; import org.junit.Test; -import org.junit.runner.RunWith; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; import org.springframework.security.oauth2.core.AuthorizationGrantType; import java.util.Arrays; @@ -27,8 +24,7 @@ import java.util.Map; import java.util.Set; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.*; /** * Tests for {@link OAuth2AuthorizationRequest}. @@ -36,8 +32,6 @@ * @author Luander Ribeiro * @author Joe Grandja */ -@RunWith(PowerMockRunner.class) -@PrepareForTest(OAuth2AuthorizationRequest.class) public class OAuth2AuthorizationRequestTests { private static final String AUTHORIZATION_URI = "https://provider.com/oauth2/authorize"; private static final String CLIENT_ID = "client-id"; @@ -45,59 +39,107 @@ public class OAuth2AuthorizationRequestTests { private static final Set SCOPES = new LinkedHashSet<>(Arrays.asList("scope1", "scope2")); private static final String STATE = "state"; - @Test(expected = IllegalArgumentException.class) + @Test public void buildWhenAuthorizationUriIsNullThenThrowIllegalArgumentException() { - OAuth2AuthorizationRequest.authorizationCode() - .authorizationUri(null) - .clientId(CLIENT_ID) - .redirectUri(REDIRECT_URI) - .scopes(SCOPES) - .state(STATE) - .build(); + assertThatThrownBy(() -> + OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(null) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(STATE) + .build() + ).isInstanceOf(IllegalArgumentException.class); } - @Test(expected = IllegalArgumentException.class) + @Test public void buildWhenClientIdIsNullThenThrowIllegalArgumentException() { - OAuth2AuthorizationRequest.authorizationCode() - .authorizationUri(AUTHORIZATION_URI) - .clientId(null) - .redirectUri(REDIRECT_URI) - .scopes(SCOPES) - .state(STATE) - .build(); + assertThatThrownBy(() -> + OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(AUTHORIZATION_URI) + .clientId(null) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(STATE) + .build() + ).isInstanceOf(IllegalArgumentException.class); } - @Test(expected = IllegalArgumentException.class) + @Test public void buildWhenRedirectUriIsNullForImplicitThenThrowIllegalArgumentException() { - OAuth2AuthorizationRequest.implicit() - .authorizationUri(AUTHORIZATION_URI) - .clientId(CLIENT_ID) - .redirectUri(null) - .scopes(SCOPES) - .state(STATE) - .build(); + assertThatThrownBy(() -> + OAuth2AuthorizationRequest.implicit() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(null) + .scopes(SCOPES) + .state(STATE) + .build() + ).isInstanceOf(IllegalArgumentException.class); } @Test public void buildWhenRedirectUriIsNullForAuthorizationCodeThenDoesNotThrowAnyException() { - assertThatCode(() -> OAuth2AuthorizationRequest.authorizationCode() - .authorizationUri(AUTHORIZATION_URI) - .clientId(CLIENT_ID) - .redirectUri(null) - .scopes(SCOPES) - .state(STATE) - .build()).doesNotThrowAnyException(); + assertThatCode(() -> + OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(null) + .scopes(SCOPES) + .state(STATE) + .build()) + .doesNotThrowAnyException(); + } + + @Test + public void buildWhenScopesIsNullThenDoesNotThrowAnyException() { + assertThatCode(() -> + OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(null) + .state(STATE) + .build()) + .doesNotThrowAnyException(); + } + + @Test + public void buildWhenStateIsNullThenDoesNotThrowAnyException() { + assertThatCode(() -> + OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(null) + .build()) + .doesNotThrowAnyException(); + } + + @Test + public void buildWhenAdditionalParametersIsNullThenDoesNotThrowAnyException() { + assertThatCode(() -> + OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(STATE) + .additionalParameters(null) + .build()) + .doesNotThrowAnyException(); } @Test public void buildWhenImplicitThenGrantTypeResponseTypeIsSet() { OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.implicit() - .authorizationUri(AUTHORIZATION_URI) - .clientId(CLIENT_ID) - .redirectUri(REDIRECT_URI) - .scopes(SCOPES) - .state(STATE) - .build(); + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(STATE) + .build(); assertThat(authorizationRequest.getGrantType()).isEqualTo(AuthorizationGrantType.IMPLICIT); assertThat(authorizationRequest.getResponseType()).isEqualTo(OAuth2AuthorizationResponseType.TOKEN); } @@ -105,30 +147,31 @@ public void buildWhenImplicitThenGrantTypeResponseTypeIsSet() { @Test public void buildWhenAuthorizationCodeThenGrantTypeResponseTypeIsSet() { OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode() - .authorizationUri(AUTHORIZATION_URI) - .clientId(CLIENT_ID) - .redirectUri(null) - .scopes(SCOPES) - .state(STATE) - .build(); + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(null) + .scopes(SCOPES) + .state(STATE) + .build(); assertThat(authorizationRequest.getGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); assertThat(authorizationRequest.getResponseType()).isEqualTo(OAuth2AuthorizationResponseType.CODE); } @Test - public void buildWhenAllAttributesProvidedThenAllAttributesAreSet() { + public void buildWhenAllValuesProvidedThenAllValuesAreSet() { Map additionalParameters = new HashMap<>(); additionalParameters.put("param1", "value1"); additionalParameters.put("param2", "value2"); OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode() - .authorizationUri(AUTHORIZATION_URI) - .clientId(CLIENT_ID) - .redirectUri(REDIRECT_URI) - .scopes(SCOPES) - .state(STATE) - .additionalParameters(additionalParameters) - .build(); + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(STATE) + .additionalParameters(additionalParameters) + .authorizationRequestUri(AUTHORIZATION_URI) + .build(); assertThat(authorizationRequest.getAuthorizationUri()).isEqualTo(AUTHORIZATION_URI); assertThat(authorizationRequest.getGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); @@ -138,39 +181,113 @@ public void buildWhenAllAttributesProvidedThenAllAttributesAreSet() { assertThat(authorizationRequest.getScopes()).isEqualTo(SCOPES); assertThat(authorizationRequest.getState()).isEqualTo(STATE); assertThat(authorizationRequest.getAdditionalParameters()).isEqualTo(additionalParameters); + assertThat(authorizationRequest.getAuthorizationRequestUri()).isEqualTo(AUTHORIZATION_URI); } @Test - public void buildWhenScopesIsNullThenDoesNotThrowAnyException() { - assertThatCode(() -> OAuth2AuthorizationRequest.authorizationCode() - .authorizationUri(AUTHORIZATION_URI) - .clientId(CLIENT_ID) - .redirectUri(REDIRECT_URI) - .scopes(null) - .state(STATE) - .build()).doesNotThrowAnyException(); + public void buildWhenScopesMultiThenSeparatedByEncodedSpace() { + OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.implicit() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(STATE) + .build(); + + assertThat(authorizationRequest.getAuthorizationRequestUri()).isEqualTo("https://provider.com/oauth2/authorize?response_type=token&client_id=client-id&scope=scope1+scope2&state=state&redirect_uri=http%3A%2F%2Fexample.com"); } @Test - public void buildWhenStateIsNullThenDoesNotThrowAnyException() { - assertThatCode(() -> OAuth2AuthorizationRequest.authorizationCode() - .authorizationUri(AUTHORIZATION_URI) - .clientId(CLIENT_ID) - .redirectUri(REDIRECT_URI) - .scopes(SCOPES) - .state(null) - .build()).doesNotThrowAnyException(); + public void buildWhenAuthorizationRequestUriSetThenOverridesDefault() { + OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(STATE) + .authorizationRequestUri(AUTHORIZATION_URI) + .build(); + assertThat(authorizationRequest.getAuthorizationRequestUri()).isEqualTo(AUTHORIZATION_URI); } @Test - public void buildWhenAdditionalParametersIsNullThenDoesNotThrowAnyException() { - assertThatCode(() -> OAuth2AuthorizationRequest.authorizationCode() - .authorizationUri(AUTHORIZATION_URI) - .clientId(CLIENT_ID) - .redirectUri(REDIRECT_URI) - .scopes(SCOPES) - .state(STATE) - .additionalParameters(null) - .build()).doesNotThrowAnyException(); + public void buildWhenAuthorizationRequestUriNotSetThenDefaultSet() { + Map additionalParameters = new HashMap<>(); + additionalParameters.put("param1", "value1"); + additionalParameters.put("param2", "value2"); + + OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(STATE) + .additionalParameters(additionalParameters) + .build(); + + assertThat(authorizationRequest.getAuthorizationRequestUri()).isNotNull(); + assertThat(authorizationRequest.getAuthorizationRequestUri()).isEqualTo("https://provider.com/oauth2/authorize?response_type=code&client_id=client-id&scope=scope1+scope2&state=state&redirect_uri=http%3A%2F%2Fexample.com¶m1=value1¶m2=value2"); + } + + @Test + public void buildWhenRequiredParametersSetThenAuthorizationRequestUriIncludesRequiredParametersOnly() { + OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .build(); + + assertThat(authorizationRequest.getAuthorizationRequestUri()).isEqualTo("https://provider.com/oauth2/authorize?response_type=code&client_id=client-id"); + } + + @Test + public void buildWhenAuthorizationRequestIncludesRegistrationIdParameterThenAuthorizationRequestUriDoesNotIncludeRegistrationIdParameter() { + Map additionalParameters = new HashMap<>(); + additionalParameters.put("param1", "value1"); + additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, "registration1"); + + OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(STATE) + .additionalParameters(additionalParameters) + .build(); + + assertThat(authorizationRequest.getAuthorizationRequestUri()).isEqualTo("https://provider.com/oauth2/authorize?response_type=code&client_id=client-id&scope=scope1+scope2&state=state&redirect_uri=http%3A%2F%2Fexample.com¶m1=value1"); + } + + @Test + public void fromWhenAuthorizationRequestIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> OAuth2AuthorizationRequest.from(null)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void fromWhenAuthorizationRequestProvidedThenValuesAreCopied() { + Map additionalParameters = new HashMap<>(); + additionalParameters.put("param1", "value1"); + additionalParameters.put("param2", "value2"); + + OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(STATE) + .additionalParameters(additionalParameters) + .build(); + + OAuth2AuthorizationRequest authorizationRequestCopy = + OAuth2AuthorizationRequest.from(authorizationRequest).build(); + + assertThat(authorizationRequestCopy.getAuthorizationUri()).isEqualTo(authorizationRequest.getAuthorizationUri()); + assertThat(authorizationRequestCopy.getGrantType()).isEqualTo(authorizationRequest.getGrantType()); + assertThat(authorizationRequestCopy.getResponseType()).isEqualTo(authorizationRequest.getResponseType()); + assertThat(authorizationRequestCopy.getClientId()).isEqualTo(authorizationRequest.getClientId()); + assertThat(authorizationRequestCopy.getRedirectUri()).isEqualTo(authorizationRequest.getRedirectUri()); + assertThat(authorizationRequestCopy.getScopes()).isEqualTo(authorizationRequest.getScopes()); + assertThat(authorizationRequestCopy.getState()).isEqualTo(authorizationRequest.getState()); + assertThat(authorizationRequestCopy.getAdditionalParameters()).isEqualTo(authorizationRequest.getAdditionalParameters()); + assertThat(authorizationRequestCopy.getAuthorizationRequestUri()).isEqualTo(authorizationRequest.getAuthorizationRequestUri()); } } diff --git a/samples/boot/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java b/samples/boot/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java index 36ee34e3659..b078b6aacb3 100644 --- a/samples/boot/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java +++ b/samples/boot/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java @@ -88,7 +88,7 @@ public void requestWhenClientNotAuthorizedThenRedirectForAuthorization() throws MvcResult mvcResult = this.mockMvc.perform(get("/repos").with(user("user"))) .andExpect(status().is3xxRedirection()) .andReturn(); - assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://github.com/login/oauth/authorize\\?response_type=code&client_id=your-app-client-id&scope=public_repo&state=.{15,}&redirect_uri=http://localhost/github-repos"); + assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://github.com/login/oauth/authorize\\?response_type=code&client_id=your-app-client-id&scope=public_repo&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fgithub-repos"); } @Test From 75362c6fcc667fffc77b5a6ec38eca285f300b11 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Wed, 27 Jun 2018 17:06:47 -0400 Subject: [PATCH 120/226] Support anonymous Principal for OAuth2AuthorizedClient Fixes gh-5064 --- .../OAuth2ClientConfiguration.java | 4 +- .../oauth2/client/OAuth2ClientConfigurer.java | 8 +- ...cipalOAuth2AuthorizedClientRepository.java | 104 +++++++ ...ssionOAuth2AuthorizedClientRepository.java | 89 ++++++ .../OAuth2AuthorizationCodeGrantFilter.java | 30 +- .../web/OAuth2AuthorizedClientRepository.java | 84 ++++++ ...Auth2AuthorizedClientArgumentResolver.java | 25 +- ...OAuth2AuthorizedClientRepositoryTests.java | 122 ++++++++ ...OAuth2AuthorizedClientRepositoryTests.java | 261 ++++++++++++++++++ ...uth2AuthorizationCodeGrantFilterTests.java | 69 ++++- ...AuthorizedClientArgumentResolverTests.java | 45 +-- 11 files changed, 777 insertions(+), 64 deletions(-) create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthenticatedPrincipalOAuth2AuthorizedClientRepository.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepository.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizedClientRepository.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/AuthenticatedPrincipalOAuth2AuthorizedClientRepositoryTests.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepositoryTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java index 28ef9426096..aa538d3445d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java @@ -21,6 +21,7 @@ import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.method.annotation.OAuth2AuthorizedClientArgumentResolver; import org.springframework.util.ClassUtils; import org.springframework.web.method.support.HandlerMethodArgumentResolver; @@ -63,7 +64,8 @@ static class OAuth2ClientWebMvcSecurityConfiguration implements WebMvcConfigurer public void addArgumentResolvers(List argumentResolvers) { if (this.authorizedClientService != null) { OAuth2AuthorizedClientArgumentResolver authorizedClientArgumentResolver = - new OAuth2AuthorizedClientArgumentResolver(this.authorizedClientService); + new OAuth2AuthorizedClientArgumentResolver( + new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(this.authorizedClientService)); argumentResolvers.add(authorizedClientArgumentResolver); } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java index e7e6b47a80b..5e85ff4bf7d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java @@ -24,6 +24,7 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; @@ -287,9 +288,10 @@ private void configure(B builder, AuthorizationCodeGrantConfigurer authorization AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class); OAuth2AuthorizationCodeGrantFilter authorizationCodeGrantFilter = new OAuth2AuthorizationCodeGrantFilter( - OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder), - OAuth2ClientConfigurerUtils.getAuthorizedClientService(builder), - authenticationManager); + OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder), + new AuthenticatedPrincipalOAuth2AuthorizedClientRepository( + OAuth2ClientConfigurerUtils.getAuthorizedClientService(builder)), + authenticationManager); if (authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestRepository != null) { authorizationCodeGrantFilter.setAuthorizationRequestRepository( diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthenticatedPrincipalOAuth2AuthorizedClientRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthenticatedPrincipalOAuth2AuthorizedClientRepository.java new file mode 100644 index 00000000000..5e4f9a59e75 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthenticatedPrincipalOAuth2AuthorizedClientRepository.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.client.web; + +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.authentication.AuthenticationTrustResolverImpl; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.util.Assert; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * An implementation of an {@link OAuth2AuthorizedClientRepository} that + * delegates to the provided {@link OAuth2AuthorizedClientService} if the current + * {@code Principal} is authenticated, otherwise, + * to the default (or provided) {@link OAuth2AuthorizedClientRepository} + * if the current request is unauthenticated (or anonymous). + * The default {@code OAuth2AuthorizedClientRepository} is {@link HttpSessionOAuth2AuthorizedClientRepository}. + * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AuthorizedClientRepository + * @see OAuth2AuthorizedClient + * @see OAuth2AuthorizedClientService + * @see HttpSessionOAuth2AuthorizedClientRepository + */ +public final class AuthenticatedPrincipalOAuth2AuthorizedClientRepository implements OAuth2AuthorizedClientRepository { + private final AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl(); + private final OAuth2AuthorizedClientService authorizedClientService; + private OAuth2AuthorizedClientRepository anonymousAuthorizedClientRepository = new HttpSessionOAuth2AuthorizedClientRepository(); + + /** + * Constructs a {@code AuthenticatedPrincipalOAuth2AuthorizedClientRepository} using the provided parameters. + * + * @param authorizedClientService the authorized client service + */ + public AuthenticatedPrincipalOAuth2AuthorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) { + Assert.notNull(authorizedClientService, "authorizedClientService cannot be null"); + this.authorizedClientService = authorizedClientService; + } + + /** + * Sets the {@link OAuth2AuthorizedClientRepository} used for requests that are unauthenticated (or anonymous). + * The default is {@link HttpSessionOAuth2AuthorizedClientRepository}. + * + * @param anonymousAuthorizedClientRepository the repository used for requests that are unauthenticated (or anonymous) + */ + public final void setAnonymousAuthorizedClientRepository(OAuth2AuthorizedClientRepository anonymousAuthorizedClientRepository) { + Assert.notNull(anonymousAuthorizedClientRepository, "anonymousAuthorizedClientRepository cannot be null"); + this.anonymousAuthorizedClientRepository = anonymousAuthorizedClientRepository; + } + + @Override + public T loadAuthorizedClient(String clientRegistrationId, Authentication principal, + HttpServletRequest request) { + if (this.isPrincipalAuthenticated(principal)) { + return this.authorizedClientService.loadAuthorizedClient(clientRegistrationId, principal.getName()); + } else { + return this.anonymousAuthorizedClientRepository.loadAuthorizedClient(clientRegistrationId, principal, request); + } + } + + @Override + public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal, + HttpServletRequest request, HttpServletResponse response) { + if (this.isPrincipalAuthenticated(principal)) { + this.authorizedClientService.saveAuthorizedClient(authorizedClient, principal); + } else { + this.anonymousAuthorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, request, response); + } + } + + @Override + public void removeAuthorizedClient(String clientRegistrationId, Authentication principal, + HttpServletRequest request, HttpServletResponse response) { + if (this.isPrincipalAuthenticated(principal)) { + this.authorizedClientService.removeAuthorizedClient(clientRegistrationId, principal.getName()); + } else { + this.anonymousAuthorizedClientRepository.removeAuthorizedClient(clientRegistrationId, principal, request, response); + } + } + + private boolean isPrincipalAuthenticated(Authentication authentication) { + return authentication != null && + !this.authenticationTrustResolver.isAnonymous(authentication) && + authentication.isAuthenticated(); + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepository.java new file mode 100644 index 00000000000..ce76392f352 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepository.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.client.web; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.util.Assert; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.util.HashMap; +import java.util.Map; + +/** + * An implementation of an {@link OAuth2AuthorizedClientRepository} that stores + * {@link OAuth2AuthorizedClient}'s in the {@code HttpSession}. + * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AuthorizedClientRepository + * @see OAuth2AuthorizedClient + */ +public final class HttpSessionOAuth2AuthorizedClientRepository implements OAuth2AuthorizedClientRepository { + private static final String DEFAULT_AUTHORIZED_CLIENTS_ATTR_NAME = + HttpSessionOAuth2AuthorizedClientRepository.class.getName() + ".AUTHORIZED_CLIENTS"; + private final String sessionAttributeName = DEFAULT_AUTHORIZED_CLIENTS_ATTR_NAME; + + @SuppressWarnings("unchecked") + @Override + public T loadAuthorizedClient(String clientRegistrationId, Authentication principal, + HttpServletRequest request) { + Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty"); + Assert.notNull(request, "request cannot be null"); + return (T) this.getAuthorizedClients(request).get(clientRegistrationId); + } + + @Override + public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal, + HttpServletRequest request, HttpServletResponse response) { + Assert.notNull(authorizedClient, "authorizedClient cannot be null"); + Assert.notNull(request, "request cannot be null"); + Assert.notNull(response, "response cannot be null"); + Map authorizedClients = this.getAuthorizedClients(request); + authorizedClients.put(authorizedClient.getClientRegistration().getRegistrationId(), authorizedClient); + request.getSession().setAttribute(this.sessionAttributeName, authorizedClients); + } + + @Override + public void removeAuthorizedClient(String clientRegistrationId, Authentication principal, + HttpServletRequest request, HttpServletResponse response) { + Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty"); + Assert.notNull(request, "request cannot be null"); + Map authorizedClients = this.getAuthorizedClients(request); + if (!authorizedClients.isEmpty()) { + if (authorizedClients.remove(clientRegistrationId) != null) { + if (!authorizedClients.isEmpty()) { + request.getSession().setAttribute(this.sessionAttributeName, authorizedClients); + } else { + request.getSession().removeAttribute(this.sessionAttributeName); + } + } + } + } + + @SuppressWarnings("unchecked") + private Map getAuthorizedClients(HttpServletRequest request) { + HttpSession session = request.getSession(false); + Map authorizedClients = session == null ? null : + (Map) session.getAttribute(this.sessionAttributeName); + if (authorizedClients == null) { + authorizedClients = new HashMap<>(); + } + return authorizedClients; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java index eac55375a83..90497366de3 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java @@ -15,19 +15,11 @@ */ package org.springframework.security.oauth2.client.web; -import java.io.IOException; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationProvider; import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -51,6 +43,12 @@ import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.UriComponentsBuilder; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + /** * A {@code Filter} for the OAuth 2.0 Authorization Code Grant, * which handles the processing of the OAuth 2.0 Authorization Response. @@ -74,7 +72,7 @@ * Upon a successful authentication, an {@link OAuth2AuthorizedClient Authorized Client} is created by associating the * {@link OAuth2AuthorizationCodeAuthenticationToken#getClientRegistration() client} to the * {@link OAuth2AuthorizationCodeAuthenticationToken#getAccessToken() access token} and current {@code Principal} - * and saving it via the {@link OAuth2AuthorizedClientService}. + * and saving it via the {@link OAuth2AuthorizedClientRepository}. * * * @@ -88,13 +86,13 @@ * @see OAuth2AuthorizationRequestRedirectFilter * @see ClientRegistrationRepository * @see OAuth2AuthorizedClient - * @see OAuth2AuthorizedClientService + * @see OAuth2AuthorizedClientRepository * @see Section 4.1 Authorization Code Grant * @see Section 4.1.2 Authorization Response */ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter { private final ClientRegistrationRepository clientRegistrationRepository; - private final OAuth2AuthorizedClientService authorizedClientService; + private final OAuth2AuthorizedClientRepository authorizedClientRepository; private final AuthenticationManager authenticationManager; private AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository(); @@ -106,17 +104,17 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter { * Constructs an {@code OAuth2AuthorizationCodeGrantFilter} using the provided parameters. * * @param clientRegistrationRepository the repository of client registrations - * @param authorizedClientService the authorized client service + * @param authorizedClientRepository the authorized client repository * @param authenticationManager the authentication manager */ public OAuth2AuthorizationCodeGrantFilter(ClientRegistrationRepository clientRegistrationRepository, - OAuth2AuthorizedClientService authorizedClientService, + OAuth2AuthorizedClientRepository authorizedClientRepository, AuthenticationManager authenticationManager) { Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); - Assert.notNull(authorizedClientService, "authorizedClientService cannot be null"); + Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null"); Assert.notNull(authenticationManager, "authenticationManager cannot be null"); this.clientRegistrationRepository = clientRegistrationRepository; - this.authorizedClientService = authorizedClientService; + this.authorizedClientRepository = authorizedClientRepository; this.authenticationManager = authenticationManager; } @@ -201,7 +199,7 @@ private void processAuthorizationResponse(HttpServletRequest request, HttpServle authenticationResult.getAccessToken(), authenticationResult.getRefreshToken()); - this.authorizedClientService.saveAuthorizedClient(authorizedClient, currentAuthentication); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, currentAuthentication, request, response); String redirectUrl = authorizationResponse.getRedirectUri(); SavedRequest savedRequest = this.requestCache.getRequest(request, response); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizedClientRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizedClientRepository.java new file mode 100644 index 00000000000..b18c785861f --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizedClientRepository.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.client.web; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.OAuth2AccessToken; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Implementations of this interface are responsible for the persistence + * of {@link OAuth2AuthorizedClient Authorized Client(s)} between requests. + * + *

    + * The primary purpose of an {@link OAuth2AuthorizedClient Authorized Client} + * is to associate an {@link OAuth2AuthorizedClient#getAccessToken() Access Token} credential + * to a {@link OAuth2AuthorizedClient#getClientRegistration() Client} and Resource Owner, + * who is the {@link OAuth2AuthorizedClient#getPrincipalName() Principal} + * that originally granted the authorization. + * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AuthorizedClient + * @see ClientRegistration + * @see Authentication + * @see OAuth2AccessToken + */ +public interface OAuth2AuthorizedClientRepository { + + /** + * Returns the {@link OAuth2AuthorizedClient} associated to the + * provided client registration identifier and End-User {@link Authentication} (Resource Owner) + * or {@code null} if not available. + * + * @param clientRegistrationId the identifier for the client's registration + * @param principal the End-User {@link Authentication} (Resource Owner) + * @param request the {@code HttpServletRequest} + * @param a type of OAuth2AuthorizedClient + * @return the {@link OAuth2AuthorizedClient} or {@code null} if not available + */ + T loadAuthorizedClient(String clientRegistrationId, Authentication principal, + HttpServletRequest request); + + /** + * Saves the {@link OAuth2AuthorizedClient} associating it to + * the provided End-User {@link Authentication} (Resource Owner). + * + * @param authorizedClient the authorized client + * @param principal the End-User {@link Authentication} (Resource Owner) + * @param request the {@code HttpServletRequest} + * @param response the {@code HttpServletResponse} + */ + void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal, + HttpServletRequest request, HttpServletResponse response); + + /** + * Removes the {@link OAuth2AuthorizedClient} associated to the + * provided client registration identifier and End-User {@link Authentication} (Resource Owner). + * + * @param clientRegistrationId the identifier for the client's registration + * @param principal the End-User {@link Authentication} (Resource Owner) + * @param request the {@code HttpServletRequest} + * @param response the {@code HttpServletResponse} + */ + void removeAuthorizedClient(String clientRegistrationId, Authentication principal, + HttpServletRequest request, HttpServletResponse response); + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java index e5c0fd1b954..67daf2955be 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java @@ -23,9 +23,9 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -33,6 +33,8 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; +import javax.servlet.http.HttpServletRequest; + /** * An implementation of a {@link HandlerMethodArgumentResolver} that is capable * of resolving a method parameter to an argument value of type {@link OAuth2AuthorizedClient}. @@ -54,16 +56,16 @@ * @see RegisteredOAuth2AuthorizedClient */ public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMethodArgumentResolver { - private final OAuth2AuthorizedClientService authorizedClientService; + private final OAuth2AuthorizedClientRepository authorizedClientRepository; /** * Constructs an {@code OAuth2AuthorizedClientArgumentResolver} using the provided parameters. * - * @param authorizedClientService the authorized client service + * @param authorizedClientRepository the authorized client repository */ - public OAuth2AuthorizedClientArgumentResolver(OAuth2AuthorizedClientService authorizedClientService) { - Assert.notNull(authorizedClientService, "authorizedClientService cannot be null"); - this.authorizedClientService = authorizedClientService; + public OAuth2AuthorizedClientArgumentResolver(OAuth2AuthorizedClientRepository authorizedClientRepository) { + Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null"); + this.authorizedClientRepository = authorizedClientRepository; } @Override @@ -98,15 +100,8 @@ public Object resolveArgument(MethodParameter parameter, "It must be provided via @RegisteredOAuth2AuthorizedClient(\"client1\") or @RegisteredOAuth2AuthorizedClient(registrationId = \"client1\")."); } - if (principal == null) { - // An Authentication is required given that an OAuth2AuthorizedClient is associated to a Principal - throw new IllegalStateException("Unable to resolve the Authorized Client with registration identifier \"" + - clientRegistrationId + "\". An \"authenticated\" or \"unauthenticated\" session is required. " + - "To allow for unauthenticated access, ensure HttpSecurity.anonymous() is configured."); - } - - OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient( - clientRegistrationId, principal.getName()); + OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient( + clientRegistrationId, principal, webRequest.getNativeRequest(HttpServletRequest.class)); if (authorizedClient == null) { throw new ClientAuthorizationRequiredException(clientRegistrationId); } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/AuthenticatedPrincipalOAuth2AuthorizedClientRepositoryTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/AuthenticatedPrincipalOAuth2AuthorizedClientRepositoryTests.java new file mode 100644 index 00000000000..e24c1e74d97 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/AuthenticatedPrincipalOAuth2AuthorizedClientRepositoryTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.client.web; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link AuthenticatedPrincipalOAuth2AuthorizedClientRepository}. + * + * @author Joe Grandja + */ +public class AuthenticatedPrincipalOAuth2AuthorizedClientRepositoryTests { + private String registrationId = "registrationId"; + private String principalName = "principalName"; + private OAuth2AuthorizedClientService authorizedClientService; + private OAuth2AuthorizedClientRepository anonymousAuthorizedClientRepository; + private AuthenticatedPrincipalOAuth2AuthorizedClientRepository authorizedClientRepository; + private MockHttpServletRequest request; + private MockHttpServletResponse response; + + @Before + public void setup() { + this.authorizedClientService = mock(OAuth2AuthorizedClientService.class); + this.anonymousAuthorizedClientRepository = mock(OAuth2AuthorizedClientRepository.class); + this.authorizedClientRepository = new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(this.authorizedClientService); + this.authorizedClientRepository.setAnonymousAuthorizedClientRepository(this.anonymousAuthorizedClientRepository); + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + } + + @Test + public void constructorWhenAuthorizedClientServiceIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void setAuthorizedClientRepositoryWhenAuthorizedClientRepositoryIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.authorizedClientRepository.setAnonymousAuthorizedClientRepository(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void loadAuthorizedClientWhenAuthenticatedPrincipalThenLoadFromService() { + Authentication authentication = this.createAuthenticatedPrincipal(); + this.authorizedClientRepository.loadAuthorizedClient(this.registrationId, authentication, this.request); + verify(this.authorizedClientService).loadAuthorizedClient(this.registrationId, this.principalName); + } + + @Test + public void loadAuthorizedClientWhenAnonymousPrincipalThenLoadFromAnonymousRepository() { + Authentication authentication = this.createAnonymousPrincipal(); + this.authorizedClientRepository.loadAuthorizedClient(this.registrationId, authentication, this.request); + verify(this.anonymousAuthorizedClientRepository).loadAuthorizedClient(this.registrationId, authentication, this.request); + } + + @Test + public void saveAuthorizedClientWhenAuthenticatedPrincipalThenSaveToService() { + Authentication authentication = this.createAuthenticatedPrincipal(); + OAuth2AuthorizedClient authorizedClient = mock(OAuth2AuthorizedClient.class); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, authentication, this.request, this.response); + verify(this.authorizedClientService).saveAuthorizedClient(authorizedClient, authentication); + } + + @Test + public void saveAuthorizedClientWhenAnonymousPrincipalThenSaveToAnonymousRepository() { + Authentication authentication = this.createAnonymousPrincipal(); + OAuth2AuthorizedClient authorizedClient = mock(OAuth2AuthorizedClient.class); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, authentication, this.request, this.response); + verify(this.anonymousAuthorizedClientRepository).saveAuthorizedClient(authorizedClient, authentication, this.request, this.response); + } + + @Test + public void removeAuthorizedClientWhenAuthenticatedPrincipalThenRemoveFromService() { + Authentication authentication = this.createAuthenticatedPrincipal(); + this.authorizedClientRepository.removeAuthorizedClient(this.registrationId, authentication, this.request, this.response); + verify(this.authorizedClientService).removeAuthorizedClient(this.registrationId, this.principalName); + } + + @Test + public void removeAuthorizedClientWhenAnonymousPrincipalThenRemoveFromAnonymousRepository() { + Authentication authentication = this.createAnonymousPrincipal(); + this.authorizedClientRepository.removeAuthorizedClient(this.registrationId, authentication, this.request, this.response); + verify(this.anonymousAuthorizedClientRepository).removeAuthorizedClient(this.registrationId, authentication, this.request, this.response); + } + + private Authentication createAuthenticatedPrincipal() { + TestingAuthenticationToken authentication = new TestingAuthenticationToken(this.principalName, "password"); + authentication.setAuthenticated(true); + return authentication; + } + + private Authentication createAnonymousPrincipal() { + return new AnonymousAuthenticationToken("key-1234", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepositoryTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepositoryTests.java new file mode 100644 index 00000000000..1578dd2cdf8 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepositoryTests.java @@ -0,0 +1,261 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.client.web; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; + +import javax.servlet.http.HttpSession; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link HttpSessionOAuth2AuthorizedClientRepository}. + * + * @author Joe Grandja + */ +public class HttpSessionOAuth2AuthorizedClientRepositoryTests { + private String registrationId1 = "registration-1"; + private String registrationId2 = "registration-2"; + private String principalName1 = "principalName-1"; + + private ClientRegistration registration1 = ClientRegistration.withRegistrationId(this.registrationId1) + .clientId("client-1") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("user") + .authorizationUri("https://provider.com/oauth2/authorize") + .tokenUri("https://provider.com/oauth2/token") + .userInfoUri("https://provider.com/oauth2/user") + .userNameAttributeName("id") + .clientName("client-1") + .build(); + + private ClientRegistration registration2 = ClientRegistration.withRegistrationId(this.registrationId2) + .clientId("client-2") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email") + .authorizationUri("https://provider.com/oauth2/authorize") + .tokenUri("https://provider.com/oauth2/token") + .userInfoUri("https://provider.com/oauth2/userinfo") + .jwkSetUri("https://provider.com/oauth2/keys") + .clientName("client-2") + .build(); + + private HttpSessionOAuth2AuthorizedClientRepository authorizedClientRepository = + new HttpSessionOAuth2AuthorizedClientRepository(); + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + @Before + public void setup() { + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + } + + @Test + public void loadAuthorizedClientWhenClientRegistrationIdIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.authorizedClientRepository.loadAuthorizedClient(null, null, this.request)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void loadAuthorizedClientWhenPrincipalNameIsNullThenExceptionNotThrown() { + this.authorizedClientRepository.loadAuthorizedClient(this.registrationId1, null, this.request); + } + + @Test + public void loadAuthorizedClientWhenRequestIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.authorizedClientRepository.loadAuthorizedClient(this.registrationId1, null, null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void loadAuthorizedClientWhenClientRegistrationNotFoundThenReturnNull() { + OAuth2AuthorizedClient authorizedClient = + this.authorizedClientRepository.loadAuthorizedClient("registration-not-found", null, this.request); + assertThat(authorizedClient).isNull(); + } + + @Test + public void loadAuthorizedClientWhenSavedThenReturnAuthorizedClient() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + this.registration1, this.principalName1, mock(OAuth2AccessToken.class)); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, null, this.request, this.response); + + OAuth2AuthorizedClient loadedAuthorizedClient = + this.authorizedClientRepository.loadAuthorizedClient(this.registrationId1, null, this.request); + assertThat(loadedAuthorizedClient).isEqualTo(authorizedClient); + } + + @Test + public void saveAuthorizedClientWhenAuthorizedClientIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.authorizedClientRepository.saveAuthorizedClient(null, null, this.request, this.response)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void saveAuthorizedClientWhenAuthenticationIsNullThenExceptionNotThrown() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + this.registration2, this.principalName1, mock(OAuth2AccessToken.class)); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, null, this.request, this.response); + } + + @Test + public void saveAuthorizedClientWhenRequestIsNullThenThrowIllegalArgumentException() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + this.registration2, this.principalName1, mock(OAuth2AccessToken.class)); + assertThatThrownBy(() -> this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, null, null, this.response)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void saveAuthorizedClientWhenResponseIsNullThenThrowIllegalArgumentException() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + this.registration2, this.principalName1, mock(OAuth2AccessToken.class)); + assertThatThrownBy(() -> this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, null, this.request, null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void saveAuthorizedClientWhenSavedThenSavedToSession() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + this.registration2, this.principalName1, mock(OAuth2AccessToken.class)); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, null, this.request, this.response); + + HttpSession session = this.request.getSession(false); + assertThat(session).isNotNull(); + + @SuppressWarnings("unchecked") + Map authorizedClients = (Map) + session.getAttribute(HttpSessionOAuth2AuthorizedClientRepository.class.getName() + ".AUTHORIZED_CLIENTS"); + assertThat(authorizedClients).isNotEmpty(); + assertThat(authorizedClients).hasSize(1); + assertThat(authorizedClients.values().iterator().next()).isSameAs(authorizedClient); + } + + @Test + public void removeAuthorizedClientWhenClientRegistrationIdIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.authorizedClientRepository.removeAuthorizedClient( + null, null, this.request, this.response)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void removeAuthorizedClientWhenPrincipalNameIsNullThenExceptionNotThrown() { + this.authorizedClientRepository.removeAuthorizedClient(this.registrationId1, null, this.request, this.response); + } + + @Test + public void removeAuthorizedClientWhenRequestIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.authorizedClientRepository.removeAuthorizedClient( + this.registrationId1, null, null, this.response)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void removeAuthorizedClientWhenResponseIsNullThenExceptionNotThrown() { + this.authorizedClientRepository.removeAuthorizedClient(this.registrationId1, null, this.request, null); + } + + @Test + public void removeAuthorizedClientWhenNotSavedThenSessionNotCreated() { + this.authorizedClientRepository.removeAuthorizedClient( + this.registrationId2, null, this.request, this.response); + assertThat(this.request.getSession(false)).isNull(); + } + + @Test + public void removeAuthorizedClientWhenClient1SavedAndClient2RemovedThenClient1NotRemoved() { + OAuth2AuthorizedClient authorizedClient1 = new OAuth2AuthorizedClient( + this.registration1, this.principalName1, mock(OAuth2AccessToken.class)); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient1, null, this.request, this.response); + + // Remove registrationId2 (never added so is not removed either) + this.authorizedClientRepository.removeAuthorizedClient( + this.registrationId2, null, this.request, this.response); + + OAuth2AuthorizedClient loadedAuthorizedClient1 = this.authorizedClientRepository.loadAuthorizedClient( + this.registrationId1, null, this.request); + assertThat(loadedAuthorizedClient1).isNotNull(); + assertThat(loadedAuthorizedClient1).isSameAs(authorizedClient1); + } + + @Test + public void removeAuthorizedClientWhenSavedThenRemoved() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + this.registration2, this.principalName1, mock(OAuth2AccessToken.class)); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, null, this.request, this.response); + OAuth2AuthorizedClient loadedAuthorizedClient = this.authorizedClientRepository.loadAuthorizedClient( + this.registrationId2, null, this.request); + assertThat(loadedAuthorizedClient).isSameAs(authorizedClient); + this.authorizedClientRepository.removeAuthorizedClient( + this.registrationId2, null, this.request, this.response); + loadedAuthorizedClient = this.authorizedClientRepository.loadAuthorizedClient( + this.registrationId2, null, this.request); + assertThat(loadedAuthorizedClient).isNull(); + } + + @Test + public void removeAuthorizedClientWhenSavedThenRemovedFromSession() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + this.registration1, this.principalName1, mock(OAuth2AccessToken.class)); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, null, this.request, this.response); + OAuth2AuthorizedClient loadedAuthorizedClient = this.authorizedClientRepository.loadAuthorizedClient( + this.registrationId1, null, this.request); + assertThat(loadedAuthorizedClient).isSameAs(authorizedClient); + this.authorizedClientRepository.removeAuthorizedClient( + this.registrationId1, null, this.request, this.response); + + HttpSession session = this.request.getSession(false); + assertThat(session).isNotNull(); + assertThat(session.getAttribute(HttpSessionOAuth2AuthorizedClientRepository.class.getName() + ".AUTHORIZED_CLIENTS")).isNull(); + } + + @Test + public void removeAuthorizedClientWhenClient1Client2SavedAndClient1RemovedThenClient2NotRemoved() { + OAuth2AuthorizedClient authorizedClient1 = new OAuth2AuthorizedClient( + this.registration1, this.principalName1, mock(OAuth2AccessToken.class)); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient1, null, this.request, this.response); + + OAuth2AuthorizedClient authorizedClient2 = new OAuth2AuthorizedClient( + this.registration2, this.principalName1, mock(OAuth2AccessToken.class)); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient2, null, this.request, this.response); + + this.authorizedClientRepository.removeAuthorizedClient( + this.registrationId1, null, this.request, this.response); + + OAuth2AuthorizedClient loadedAuthorizedClient2 = this.authorizedClientRepository.loadAuthorizedClient( + this.registrationId2, null, this.request); + assertThat(loadedAuthorizedClient2).isNotNull(); + assertThat(loadedAuthorizedClient2).isSameAs(authorizedClient2); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java index 3093adc02b8..e9b615e98f1 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java @@ -15,6 +15,7 @@ */ package org.springframework.security.oauth2.client.web; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -23,9 +24,11 @@ import org.powermock.modules.junit4.PowerMockRunner; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; @@ -51,6 +54,7 @@ import javax.servlet.FilterChain; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; import java.util.HashMap; import java.util.Map; @@ -71,12 +75,13 @@ public class OAuth2AuthorizationCodeGrantFilterTests { private String principalName1 = "principal-1"; private ClientRegistrationRepository clientRegistrationRepository; private OAuth2AuthorizedClientService authorizedClientService; + private OAuth2AuthorizedClientRepository authorizedClientRepository; private AuthenticationManager authenticationManager; private AuthorizationRequestRepository authorizationRequestRepository; private OAuth2AuthorizationCodeGrantFilter filter; @Before - public void setUp() { + public void setup() { this.registration1 = ClientRegistration.withRegistrationId("registration-1") .clientId("client-1") .clientSecret("secret") @@ -92,32 +97,39 @@ public void setUp() { .build(); this.clientRegistrationRepository = new InMemoryClientRegistrationRepository(this.registration1); this.authorizedClientService = new InMemoryOAuth2AuthorizedClientService(this.clientRegistrationRepository); + this.authorizedClientRepository = new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(this.authorizedClientService); this.authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository(); this.authenticationManager = mock(AuthenticationManager.class); this.filter = spy(new OAuth2AuthorizationCodeGrantFilter( - this.clientRegistrationRepository, this.authorizedClientService, this.authenticationManager)); + this.clientRegistrationRepository, this.authorizedClientRepository, this.authenticationManager)); this.filter.setAuthorizationRequestRepository(this.authorizationRequestRepository); - + TestingAuthenticationToken authentication = new TestingAuthenticationToken(this.principalName1, "password"); + authentication.setAuthenticated(true); SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); - securityContext.setAuthentication(new TestingAuthenticationToken(this.principalName1, "password")); + securityContext.setAuthentication(authentication); SecurityContextHolder.setContext(securityContext); } + @After + public void cleanup() { + SecurityContextHolder.clearContext(); + } + @Test public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2AuthorizationCodeGrantFilter(null, this.authorizedClientService, this.authenticationManager)) + assertThatThrownBy(() -> new OAuth2AuthorizationCodeGrantFilter(null, this.authorizedClientRepository, this.authenticationManager)) .isInstanceOf(IllegalArgumentException.class); } @Test - public void constructorWhenAuthorizedClientServiceIsNullThenThrowIllegalArgumentException() { + public void constructorWhenAuthorizedClientRepositoryIsNullThenThrowIllegalArgumentException() { assertThatThrownBy(() -> new OAuth2AuthorizationCodeGrantFilter(this.clientRegistrationRepository, null, this.authenticationManager)) .isInstanceOf(IllegalArgumentException.class); } @Test public void constructorWhenAuthenticationManagerIsNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2AuthorizationCodeGrantFilter(this.clientRegistrationRepository, this.authorizedClientService, null)) + assertThatThrownBy(() -> new OAuth2AuthorizationCodeGrantFilter(this.clientRegistrationRepository, this.authorizedClientRepository, null)) .isInstanceOf(IllegalArgumentException.class); } @@ -218,7 +230,7 @@ public void doFilterWhenAuthenticationFailsThenHandleOAuth2AuthenticationExcepti } @Test - public void doFilterWhenAuthorizationResponseSuccessThenAuthorizedClientSaved() throws Exception { + public void doFilterWhenAuthorizationResponseSuccessThenAuthorizedClientSavedToService() throws Exception { String requestUri = "/callback/client-1"; MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); request.setServletPath(requestUri); @@ -285,6 +297,47 @@ public void doFilterWhenAuthorizationResponseSuccessHasSavedRequestThenRedirecte assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/saved-request"); } + @Test + public void doFilterWhenAuthorizationResponseSuccessAndAnonymousAccessThenAuthorizedClientSavedToHttpSession() throws Exception { + AnonymousAuthenticationToken anonymousPrincipal = + new AnonymousAuthenticationToken("key-1234", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(anonymousPrincipal); + SecurityContextHolder.setContext(securityContext); + + String requestUri = "/callback/client-1"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + request.addParameter(OAuth2ParameterNames.CODE, "code"); + request.addParameter(OAuth2ParameterNames.STATE, "state"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.setUpAuthorizationRequest(request, response, this.registration1); + this.setUpAuthenticationResult(this.registration1); + + this.filter.doFilter(request, response, filterChain); + + OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient( + this.registration1.getRegistrationId(), anonymousPrincipal, request); + assertThat(authorizedClient).isNotNull(); + + assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1); + assertThat(authorizedClient.getPrincipalName()).isEqualTo(anonymousPrincipal.getName()); + assertThat(authorizedClient.getAccessToken()).isNotNull(); + + HttpSession session = request.getSession(false); + assertThat(session).isNotNull(); + + @SuppressWarnings("unchecked") + Map authorizedClients = (Map) + session.getAttribute(HttpSessionOAuth2AuthorizedClientRepository.class.getName() + ".AUTHORIZED_CLIENTS"); + assertThat(authorizedClients).isNotEmpty(); + assertThat(authorizedClients).hasSize(1); + assertThat(authorizedClients.values().iterator().next()).isSameAs(authorizedClient); + } + private void setUpAuthorizationRequest(HttpServletRequest request, HttpServletResponse response, ClientRegistration registration) { Map additionalParameters = new HashMap<>(); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java index 65fd59725c9..d67d527da01 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java @@ -15,19 +15,23 @@ */ package org.springframework.security.oauth2.client.web.method.annotation; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.util.ReflectionUtils; +import org.springframework.web.context.request.ServletWebRequest; +import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Method; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; @@ -43,21 +47,29 @@ * @author Joe Grandja */ public class OAuth2AuthorizedClientArgumentResolverTests { - private OAuth2AuthorizedClientService authorizedClientService; + private OAuth2AuthorizedClientRepository authorizedClientRepository; private OAuth2AuthorizedClientArgumentResolver argumentResolver; private OAuth2AuthorizedClient authorizedClient; + private MockHttpServletRequest request; @Before - public void setUp() { - this.authorizedClientService = mock(OAuth2AuthorizedClientService.class); - this.argumentResolver = new OAuth2AuthorizedClientArgumentResolver(this.authorizedClientService); + public void setup() { + this.authorizedClientRepository = mock(OAuth2AuthorizedClientRepository.class); + this.argumentResolver = new OAuth2AuthorizedClientArgumentResolver(this.authorizedClientRepository); this.authorizedClient = mock(OAuth2AuthorizedClient.class); - when(this.authorizedClientService.loadAuthorizedClient(anyString(), any())).thenReturn(this.authorizedClient); + this.request = new MockHttpServletRequest(); + when(this.authorizedClientRepository.loadAuthorizedClient(anyString(), any(), any(HttpServletRequest.class))) + .thenReturn(this.authorizedClient); SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); securityContext.setAuthentication(mock(Authentication.class)); SecurityContextHolder.setContext(securityContext); } + @After + public void cleanup() { + SecurityContextHolder.clearContext(); + } + @Test public void constructorWhenOAuth2AuthorizedClientServiceIsNullThenThrowIllegalArgumentException() { assertThatThrownBy(() -> new OAuth2AuthorizedClientArgumentResolver(null)) @@ -104,31 +116,22 @@ public void resolveArgumentWhenRegistrationIdEmptyAndOAuth2AuthenticationThenRes securityContext.setAuthentication(authentication); SecurityContextHolder.setContext(securityContext); MethodParameter methodParameter = this.getMethodParameter("registrationIdEmpty", OAuth2AuthorizedClient.class); - this.argumentResolver.resolveArgument(methodParameter, null, null, null); - } - - @Test - public void resolveArgumentWhenParameterTypeOAuth2AuthorizedClientAndCurrentAuthenticationNullThenThrowIllegalStateException() throws Exception { - SecurityContextHolder.clearContext(); - MethodParameter methodParameter = this.getMethodParameter("paramTypeAuthorizedClient", OAuth2AuthorizedClient.class); - assertThatThrownBy(() -> this.argumentResolver.resolveArgument(methodParameter, null, null, null)) - .isInstanceOf(IllegalStateException.class) - .hasMessage("Unable to resolve the Authorized Client with registration identifier \"client1\". " + - "An \"authenticated\" or \"unauthenticated\" session is required. " + - "To allow for unauthenticated access, ensure HttpSecurity.anonymous() is configured."); + this.argumentResolver.resolveArgument(methodParameter, null, new ServletWebRequest(this.request), null); } @Test public void resolveArgumentWhenOAuth2AuthorizedClientFoundThenResolves() throws Exception { MethodParameter methodParameter = this.getMethodParameter("paramTypeAuthorizedClient", OAuth2AuthorizedClient.class); - assertThat(this.argumentResolver.resolveArgument(methodParameter, null, null, null)).isSameAs(this.authorizedClient); + assertThat(this.argumentResolver.resolveArgument( + methodParameter, null, new ServletWebRequest(this.request), null)).isSameAs(this.authorizedClient); } @Test public void resolveArgumentWhenOAuth2AuthorizedClientNotFoundThenThrowClientAuthorizationRequiredException() throws Exception { - when(this.authorizedClientService.loadAuthorizedClient(anyString(), any())).thenReturn(null); + when(this.authorizedClientRepository.loadAuthorizedClient(anyString(), any(), any(HttpServletRequest.class))) + .thenReturn(null); MethodParameter methodParameter = this.getMethodParameter("paramTypeAuthorizedClient", OAuth2AuthorizedClient.class); - assertThatThrownBy(() -> this.argumentResolver.resolveArgument(methodParameter, null, null, null)) + assertThatThrownBy(() -> this.argumentResolver.resolveArgument(methodParameter, null, new ServletWebRequest(this.request), null)) .isInstanceOf(ClientAuthorizationRequiredException.class); } From b1d3e3b9588b136f3d23ec9b77c0c62aa35a7cd0 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 11 Jun 2018 22:52:46 -0600 Subject: [PATCH 121/226] Transient Authentication Tokens This commit introduces support for transient authentication tokens which indicate to the filter chain, specifically the HttpSessionSecurityContextRepository, whether or not the token ought to be persisted across requests. To leverage this, simply annotate any Authentication implementation with @TransientAuthentication, extend from an Authentication that uses this annotation, or annotate a custom annotation. Implementations of SecurityContextRepository may choose to not persist tokens that are marked with @TransientAuthentication in the same way that HttpSessionSecurityContextRepository does. Fixes: gh-5481 --- ...onfigurerTransientAuthenticationTests.java | 123 +++++++++++++++ ...entConfigTransientAuthenticationTests.java | 98 ++++++++++++ ...ssionAlwaysWithTransientAuthentication.xml | 37 +++++ ...ationTests-WithTransientAuthentication.xml | 37 +++++ .../core/TransientAuthentication.java | 38 +++++ .../HttpSessionSecurityContextRepository.java | 11 ++ ...curityContextRepositoryServlet25Tests.java | 54 +++++++ ...SessionSecurityContextRepositoryTests.java | 146 ++++++++++++++---- 8 files changed, 513 insertions(+), 31 deletions(-) create mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTransientAuthenticationTests.java create mode 100644 config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests.java create mode 100644 config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests-CreateSessionAlwaysWithTransientAuthentication.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests-WithTransientAuthentication.xml create mode 100644 core/src/main/java/org/springframework/security/core/TransientAuthentication.java create mode 100644 web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryServlet25Tests.java diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTransientAuthenticationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTransientAuthenticationTests.java new file mode 100644 index 00000000000..673b59ee837 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTransientAuthenticationTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.annotation.web.configurers; + +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.TransientAuthentication; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; + +/** + * @author Josh Cummings + */ +public class SessionManagementConfigurerTransientAuthenticationTests { + + @Autowired + MockMvc mvc; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Test + public void postWhenTransientAuthenticationThenNoSessionCreated() + throws Exception { + + this.spring.register(WithTransientAuthenticationConfig.class).autowire(); + MvcResult result = this.mvc.perform(post("/login")).andReturn(); + assertThat(result.getRequest().getSession(false)).isNull(); + } + + @Test + public void postWhenTransientAuthenticationThenAlwaysSessionOverrides() + throws Exception { + + this.spring.register(AlwaysCreateSessionConfig.class).autowire(); + MvcResult result = this.mvc.perform(post("/login")).andReturn(); + assertThat(result.getRequest().getSession(false)).isNotNull(); + } + + @EnableWebSecurity + static class WithTransientAuthenticationConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + super.configure(http); + + http + .csrf().disable(); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth + .authenticationProvider(new TransientAuthenticationProvider()); + } + } + + @EnableWebSecurity + static class AlwaysCreateSessionConfig extends WithTransientAuthenticationConfig { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS); + } + } + + static class TransientAuthenticationProvider implements AuthenticationProvider { + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + return new SomeTransientAuthentication(); + } + + @Override + public boolean supports(Class authentication) { + return true; + } + } + + @TransientAuthentication + static class SomeTransientAuthentication extends AbstractAuthenticationToken { + SomeTransientAuthentication() { + super(null); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return null; + } + } +} diff --git a/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests.java b/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests.java new file mode 100644 index 00000000000..2bf529bb2db --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.http; + +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.TransientAuthentication; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; + +/** + * @author Josh Cummings + */ +public class SessionManagementConfigTransientAuthenticationTests { + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests"; + + @Autowired + MockMvc mvc; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Test + public void postWhenTransientAuthenticationThenNoSessionCreated() + throws Exception { + + this.spring.configLocations(this.xml("WithTransientAuthentication")).autowire(); + MvcResult result = this.mvc.perform(post("/login")).andReturn(); + assertThat(result.getRequest().getSession(false)).isNull(); + } + + @Test + public void postWhenTransientAuthenticationThenAlwaysSessionOverrides() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionAlwaysWithTransientAuthentication")).autowire(); + MvcResult result = this.mvc.perform(post("/login")).andReturn(); + assertThat(result.getRequest().getSession(false)).isNotNull(); + } + + static class TransientAuthenticationProvider implements AuthenticationProvider { + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + return new SomeTransientAuthentication(); + } + + @Override + public boolean supports(Class authentication) { + return true; + } + } + + @TransientAuthentication + static class SomeTransientAuthentication extends AbstractAuthenticationToken { + SomeTransientAuthentication() { + super(null); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return null; + } + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests-CreateSessionAlwaysWithTransientAuthentication.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests-CreateSessionAlwaysWithTransientAuthentication.xml new file mode 100644 index 00000000000..804e3dedd42 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests-CreateSessionAlwaysWithTransientAuthentication.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests-WithTransientAuthentication.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests-WithTransientAuthentication.xml new file mode 100644 index 00000000000..ae66dcbbb07 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests-WithTransientAuthentication.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/core/src/main/java/org/springframework/security/core/TransientAuthentication.java b/core/src/main/java/org/springframework/security/core/TransientAuthentication.java new file mode 100644 index 00000000000..997ab1dcd4c --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/TransientAuthentication.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.core; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A marker for {@link Authentication}s that should never be stored across requests, for example + * a bearer token authentication + * + * @author Josh Cummings + * @since 5.1 + */ +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface TransientAuthentication { +} diff --git a/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java b/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java index 49f541f9d2b..c6deefe8771 100644 --- a/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java +++ b/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java @@ -25,9 +25,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.core.Authentication; +import org.springframework.security.core.TransientAuthentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -387,6 +390,10 @@ private boolean contextChanged(SecurityContext context) { } private HttpSession createNewSessionIfAllowed(SecurityContext context) { + if (isTransientAuthentication(context.getAuthentication())) { + return null; + } + if (httpSessionExistedAtStartOfRequest) { if (logger.isDebugEnabled()) { logger.debug("HttpSession is now null, but was not null at start of request; " @@ -437,6 +444,10 @@ private HttpSession createNewSessionIfAllowed(SecurityContext context) { } } + private boolean isTransientAuthentication(Authentication authentication) { + return AnnotationUtils.getAnnotation(authentication.getClass(), TransientAuthentication.class) != null; + } + /** * Sets the {@link AuthenticationTrustResolver} to be used. The default is * {@link AuthenticationTrustResolverImpl}. diff --git a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryServlet25Tests.java b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryServlet25Tests.java new file mode 100644 index 00000000000..5f9f4c28127 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryServlet25Tests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.context; + +import javax.servlet.ServletRequest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.powermock.api.mockito.PowerMockito.spy; +import static org.powermock.api.mockito.PowerMockito.when; + +/** + * @author Luke Taylor + * @author Rob Winch + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({ ClassUtils.class }) +public class HttpSessionSecurityContextRepositoryServlet25Tests { + @Test + public void servlet25Compatability() throws Exception { + spy(ClassUtils.class); + when(ClassUtils.class, "hasMethod", ServletRequest.class, "startAsync", + new Class[] {}).thenReturn(false); + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, + response); + repo.loadContext(holder); + assertThat(holder.getRequest()).isSameAs(request); + } +} diff --git a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java index 1b9c027fb01..4a9b0845cf8 100644 --- a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java +++ b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java @@ -16,18 +16,11 @@ package org.springframework.security.web.context; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Matchers.anyBoolean; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.verify; -import static org.powermock.api.mockito.PowerMockito.mock; -import static org.powermock.api.mockito.PowerMockito.spy; -import static org.powermock.api.mockito.PowerMockito.when; -import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY; - +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import javax.servlet.ServletOutputStream; -import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; @@ -36,25 +29,32 @@ import org.junit.After; import org.junit.Test; -import org.junit.runner.RunWith; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; + import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.TransientAuthentication; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.when; +import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY; /** * @author Luke Taylor * @author Rob Winch */ -@RunWith(PowerMockRunner.class) -@PrepareForTest({ ClassUtils.class }) public class HttpSessionSecurityContextRepositoryTests { private final TestingAuthenticationToken testToken = new TestingAuthenticationToken( @@ -65,20 +65,6 @@ public void tearDown() { SecurityContextHolder.clearContext(); } - @Test - public void servlet25Compatability() throws Exception { - spy(ClassUtils.class); - when(ClassUtils.class, "hasMethod", ServletRequest.class, "startAsync", - new Class[] {}).thenReturn(false); - HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); - MockHttpServletRequest request = new MockHttpServletRequest(); - MockHttpServletResponse response = new MockHttpServletResponse(); - HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, - response); - repo.loadContext(holder); - assertThat(holder.getRequest()).isSameAs(request); - } - @Test public void startAsyncDisablesSaveOnCommit() throws Exception { HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); @@ -633,4 +619,102 @@ public void failsWithStandardResponse() { repo.saveContext(context, request, response); } + + @Test + public void saveContextWhenTransientAuthenticationThenSkipped() { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, + response); + SecurityContext context = repo.loadContext(holder); + + SomeTransientAuthentication authentication = new SomeTransientAuthentication(); + context.setAuthentication(authentication); + + repo.saveContext(context, holder.getRequest(), holder.getResponse()); + + MockHttpSession session = (MockHttpSession) request.getSession(false); + assertThat(session).isNull(); + } + + @Test + public void saveContextWhenTransientAuthenticationSubclassThenSkipped() { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, + response); + SecurityContext context = repo.loadContext(holder); + + SomeTransientAuthenticationSubclass authentication = new SomeTransientAuthenticationSubclass(); + context.setAuthentication(authentication); + + repo.saveContext(context, holder.getRequest(), holder.getResponse()); + + MockHttpSession session = (MockHttpSession) request.getSession(false); + assertThat(session).isNull(); + } + + @Test + public void saveContextWhenTransientAuthenticationWithCustomAnnotationThenSkipped() { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, + response); + SecurityContext context = repo.loadContext(holder); + + SomeOtherTransientAuthentication authentication = new SomeOtherTransientAuthentication(); + context.setAuthentication(authentication); + + repo.saveContext(context, holder.getRequest(), holder.getResponse()); + + MockHttpSession session = (MockHttpSession) request.getSession(false); + assertThat(session).isNull(); + } + + @TransientAuthentication + private static class SomeTransientAuthentication extends AbstractAuthenticationToken { + public SomeTransientAuthentication() { + super(null); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return null; + } + } + + private static class SomeTransientAuthenticationSubclass extends SomeTransientAuthentication { + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @TransientAuthentication + public @interface TestTransientAuthentication { + } + + @TestTransientAuthentication + private static class SomeOtherTransientAuthentication extends AbstractAuthenticationToken { + public SomeOtherTransientAuthentication() { + super(null); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return null; + } + } } From 64cdb481fe1ec826878b5b2fc6eecf4d97dea887 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 25 Jun 2018 16:44:03 -0600 Subject: [PATCH 122/226] Improved Session Creation Policy Configuration Other configurers can now offer their preference on session creation policy without trumping what a user provided via the sessionCreationPolicy method. This is valuable for configurer's like Resource Server that would like to have session management be stateless, but not at the expense of the user's direct configuration. Fixes: gh-5518 --- .../SessionManagementConfigurer.java | 22 ++-- ...tConfigurerSessionCreationPolicyTests.java | 115 ++++++++++++++++++ 2 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerSessionCreationPolicyTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java index fd814edc7d7..43ea86d7f01 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; - import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @@ -105,7 +104,7 @@ public final class SessionManagementConfigurer> private Integer maximumSessions; private String expiredUrl; private boolean maxSessionsPreventsLogin; - private SessionCreationPolicy sessionPolicy = SessionCreationPolicy.IF_REQUIRED; + private SessionCreationPolicy sessionPolicy; private boolean enableSessionUrlRewriting; private String invalidSessionUrl; private String sessionAuthenticationErrorUrl; @@ -549,7 +548,14 @@ AuthenticationFailureHandler getSessionAuthenticationFailureHandler() { * @return the {@link SessionCreationPolicy} */ SessionCreationPolicy getSessionCreationPolicy() { - return this.sessionPolicy; + if (this.sessionPolicy != null) { + return this.sessionPolicy; + } + + SessionCreationPolicy sessionPolicy = + getBuilder().getSharedObject(SessionCreationPolicy.class); + return sessionPolicy == null ? + SessionCreationPolicy.IF_REQUIRED : sessionPolicy; } /** @@ -558,8 +564,9 @@ SessionCreationPolicy getSessionCreationPolicy() { * @return true if the {@link SessionCreationPolicy} allows session creation */ private boolean isAllowSessionCreation() { - return SessionCreationPolicy.ALWAYS == this.sessionPolicy - || SessionCreationPolicy.IF_REQUIRED == this.sessionPolicy; + SessionCreationPolicy sessionPolicy = getSessionCreationPolicy(); + return SessionCreationPolicy.ALWAYS == sessionPolicy + || SessionCreationPolicy.IF_REQUIRED == sessionPolicy; } /** @@ -567,7 +574,8 @@ private boolean isAllowSessionCreation() { * @return */ private boolean isStateless() { - return SessionCreationPolicy.STATELESS == this.sessionPolicy; + SessionCreationPolicy sessionPolicy = getSessionCreationPolicy(); + return SessionCreationPolicy.STATELESS == sessionPolicy; } /** diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerSessionCreationPolicyTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerSessionCreationPolicyTests.java new file mode 100644 index 00000000000..0d125b89f72 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerSessionCreationPolicyTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.configurers; + +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Josh Cummings + */ +public class SessionManagementConfigurerSessionCreationPolicyTests { + @Autowired + MockMvc mvc; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Test + public void getWhenSharedObjectSessionCreationPolicyConfigurationThenOverrides() + throws Exception { + + this.spring.register(StatelessCreateSessionSharedObjectConfig.class).autowire(); + + MvcResult result = this.mvc.perform(get("/")).andReturn(); + + assertThat(result.getRequest().getSession(false)).isNull(); + } + + @EnableWebSecurity + static class StatelessCreateSessionSharedObjectConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + super.configure(http); + http.setSharedObject(SessionCreationPolicy.class, SessionCreationPolicy.STATELESS); + } + } + + @Test + public void getWhenUserSessionCreationPolicyConfigurationThenOverrides() + throws Exception { + + this.spring.register(StatelessCreateSessionUserConfig.class).autowire(); + + MvcResult result = this.mvc.perform(get("/")).andReturn(); + + assertThat(result.getRequest().getSession(false)).isNull(); + } + + @EnableWebSecurity + static class StatelessCreateSessionUserConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + super.configure(http); + http + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + + http.setSharedObject(SessionCreationPolicy.class, SessionCreationPolicy.ALWAYS); + } + } + + @Test + public void getWhenDefaultsThenLoginChallengeCreatesSession() + throws Exception { + + this.spring.register(DefaultConfig.class, BasicController.class).autowire(); + + MvcResult result = + this.mvc.perform(get("/")) + .andExpect(status().isUnauthorized()) + .andReturn(); + + assertThat(result.getRequest().getSession(false)).isNotNull(); + } + + @EnableWebSecurity + static class DefaultConfig extends WebSecurityConfigurerAdapter { + } + + @RestController + static class BasicController { + @GetMapping("/") + public String root() { + return "ok"; + } + } +} From 432f4f0e8bc297390a4d49df720780475ff1d404 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Wed, 30 May 2018 13:15:56 -0600 Subject: [PATCH 123/226] Disable CSRF by Request Matcher This introduces an evolution on CsrfConfigurer#ignoreAntMatchers, allowing users to specify a RequestMatcher in the circumstance where more than just the path needs to be analyzed to determine whether CsrfFilter should require a token for the request. Simply put, a user can now selectively disable csrf by request matcher in addition to the way it can already be done with ant matchers. Fixes: gh-5477 --- .../web/configurers/CsrfConfigurer.java | 33 ++++- ...onfigurerIgnoringRequestMatchersTests.java | 127 ++++++++++++++++++ 2 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerIgnoringRequestMatchersTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java index a1143cc3d59..9529703af6e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -128,7 +128,7 @@ public CsrfConfigurer requireCsrfProtectionMatcher( *

    * *

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

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

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

      + * + *

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

      + *
        + *
      • Any GET, HEAD, TRACE, OPTIONS (this is the default)
      • + *
      • We also explicitly state to ignore any request that has a "X-Requested-With: XMLHttpRequest" header
      • + *
      + * + *
      +	 * http
      +	 *     .csrf()
      +	 *         .ignoringRequestMatchers(request -> "XMLHttpRequest".equals(request.getHeader("X-Requested-With")))
      +	 *         .and()
      +	 *     ...
      +	 * 
      + * + * @since 5.1 + */ + public CsrfConfigurer ignoringRequestMatchers(RequestMatcher... requestMatchers) { + return new IgnoreCsrfProtectionRegistry(this.context).requestMatchers(requestMatchers) + .and(); + } + @SuppressWarnings("unchecked") @Override public void configure(H http) throws Exception { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerIgnoringRequestMatchersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerIgnoringRequestMatchersTests.java new file mode 100644 index 00000000000..75f04401d02 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerIgnoringRequestMatchersTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.configurers; + +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Josh Cummings + */ +public class CsrfConfigurerIgnoringRequestMatchersTests { + + @Autowired + MockMvc mvc; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Test + public void requestWhenIgnoringRequestMatchersThenAugmentedByConfiguredRequestMatcher() + throws Exception { + this.spring.register(IgnoringRequestMatchers.class, BasicController.class).autowire(); + + this.mvc.perform(get("/path")) + .andExpect(status().isForbidden()); + + this.mvc.perform(post("/path")) + .andExpect(status().isOk()); + } + + @EnableWebSecurity + static class IgnoringRequestMatchers extends WebSecurityConfigurerAdapter { + RequestMatcher requestMatcher = + request -> HttpMethod.POST.name().equals(request.getMethod()); + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .csrf() + .requireCsrfProtectionMatcher(new AntPathRequestMatcher("/path")) + .ignoringRequestMatchers(this.requestMatcher); + // @formatter:on + } + } + + @Test + public void requestWhenIgnoringRequestMatcherThenUnionsWithConfiguredIgnoringAntMatchers() + throws Exception { + + this.spring.register(IgnoringPathsAndMatchers.class, BasicController.class).autowire(); + + this.mvc.perform(put("/csrf")) + .andExpect(status().isForbidden()); + + this.mvc.perform(post("/csrf")) + .andExpect(status().isOk()); + + this.mvc.perform(put("/no-csrf")) + .andExpect(status().isOk()); + } + + @EnableWebSecurity + static class IgnoringPathsAndMatchers extends WebSecurityConfigurerAdapter { + RequestMatcher requestMatcher = + request -> HttpMethod.POST.name().equals(request.getMethod()); + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .csrf() + .ignoringAntMatchers("/no-csrf") + .ignoringRequestMatchers(this.requestMatcher); + // @formatter:on + } + } + + @RestController + public static class BasicController { + @RequestMapping("/path") + public String path() { + return "path"; + } + + @RequestMapping("/csrf") + public String csrf() { + return "csrf"; + } + + @RequestMapping("/no-csrf") + public String noCsrf() { + return "no-csrf"; + } + } +} From 073d9e542dd5b4c954d37aeba78488601cffdc68 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 12 Jun 2018 12:13:42 -0600 Subject: [PATCH 124/226] Access Denied Handling Defaults This introduces the capability for users to wire denial handling by request matcher, similar to how users can already do with authentication entry points. This is handy for when denial behavior differs based on the contents of the request, for example, when the Authorization header indicates an OAuth2 Bearer Token request vs Basic authentication. Fixes: gh-5478 --- .../ExceptionHandlingConfigurer.java | 67 ++++++++-- ...ingConfigurerAccessDeniedHandlerTests.java | 122 ++++++++++++++++++ ...tMatcherDelegatingAccessDeniedHandler.java | 76 +++++++++++ ...herDelegatingAccessDeniedHandlerTests.java | 100 ++++++++++++++ 4 files changed, 356 insertions(+), 9 deletions(-) create mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerAccessDeniedHandlerTests.java create mode 100644 web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingAccessDeniedHandler.java create mode 100644 web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingAccessDeniedHandlerTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java index 84665715fed..9fa3ad9c470 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.access.AccessDeniedHandlerImpl; import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.security.web.access.RequestMatcherDelegatingAccessDeniedHandler; import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint; import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; @@ -70,6 +71,8 @@ public final class ExceptionHandlingConfigurer> private LinkedHashMap defaultEntryPointMappings = new LinkedHashMap<>(); + private LinkedHashMap defaultDeniedHandlerMappings = new LinkedHashMap<>(); + /** * Creates a new instance * @see HttpSecurity#exceptionHandling() @@ -104,6 +107,26 @@ public ExceptionHandlingConfigurer accessDeniedHandler( return this; } + /** + * Sets a default {@link AccessDeniedHandler} to be used which prefers being + * invoked for the provided {@link RequestMatcher}. If only a single default + * {@link AccessDeniedHandler} is specified, it will be what is used for the + * default {@link AccessDeniedHandler}. If multiple default + * {@link AccessDeniedHandler} instances are configured, then a + * {@link RequestMatcherDelegatingAccessDeniedHandler} will be used. + * + * @param deniedHandler the {@link AccessDeniedHandler} to use + * @param preferredMatcher the {@link RequestMatcher} for this default + * {@link AccessDeniedHandler} + * @return the {@link ExceptionHandlingConfigurer} for further customizations + * @since 5.1 + */ + public ExceptionHandlingConfigurer defaultAccessDeniedHandlerFor( + AccessDeniedHandler deniedHandler, RequestMatcher preferredMatcher) { + this.defaultDeniedHandlerMappings.put(preferredMatcher, deniedHandler); + return this; + } + /** * Sets the {@link AuthenticationEntryPoint} to be used. * @@ -169,13 +192,27 @@ public void configure(H http) throws Exception { AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http); ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter( entryPoint, getRequestCache(http)); - if (accessDeniedHandler != null) { - exceptionTranslationFilter.setAccessDeniedHandler(accessDeniedHandler); - } + AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http); + exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler); exceptionTranslationFilter = postProcess(exceptionTranslationFilter); http.addFilter(exceptionTranslationFilter); } + /** + * Gets the {@link AccessDeniedHandler} according to the rules specified by + * {@link #accessDeniedHandler(AccessDeniedHandler)} + * @param http the {@link HttpSecurity} used to look up shared + * {@link AccessDeniedHandler} + * @return the {@link AccessDeniedHandler} to use + */ + AccessDeniedHandler getAccessDeniedHandler(H http) { + AccessDeniedHandler deniedHandler = this.accessDeniedHandler; + if (deniedHandler == null) { + deniedHandler = createDefaultDeniedHandler(http); + } + return deniedHandler; + } + /** * Gets the {@link AuthenticationEntryPoint} according to the rules specified by * {@link #authenticationEntryPoint(AuthenticationEntryPoint)} @@ -191,16 +228,28 @@ AuthenticationEntryPoint getAuthenticationEntryPoint(H http) { return entryPoint; } + private AccessDeniedHandler createDefaultDeniedHandler(H http) { + if (this.defaultDeniedHandlerMappings.isEmpty()) { + return new AccessDeniedHandlerImpl(); + } + if (this.defaultDeniedHandlerMappings.size() == 1) { + return this.defaultDeniedHandlerMappings.values().iterator().next(); + } + return new RequestMatcherDelegatingAccessDeniedHandler( + this.defaultDeniedHandlerMappings, + new AccessDeniedHandlerImpl()); + } + private AuthenticationEntryPoint createDefaultEntryPoint(H http) { - if (defaultEntryPointMappings.isEmpty()) { + if (this.defaultEntryPointMappings.isEmpty()) { return new Http403ForbiddenEntryPoint(); } - if (defaultEntryPointMappings.size() == 1) { - return defaultEntryPointMappings.values().iterator().next(); + if (this.defaultEntryPointMappings.size() == 1) { + return this.defaultEntryPointMappings.values().iterator().next(); } DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint( - defaultEntryPointMappings); - entryPoint.setDefaultEntryPoint(defaultEntryPointMappings.values().iterator() + this.defaultEntryPointMappings); + entryPoint.setDefaultEntryPoint(this.defaultEntryPointMappings.values().iterator() .next()); return entryPoint; } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerAccessDeniedHandlerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerAccessDeniedHandlerTests.java new file mode 100644 index 00000000000..91c1076d139 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerAccessDeniedHandlerTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.annotation.web.configurers; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.AccessDeniedHandlerImpl; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.AnyRequestMatcher; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Josh Cummings + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SecurityTestExecutionListeners +public class ExceptionHandlingConfigurerAccessDeniedHandlerTests { + @Autowired + MockMvc mvc; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Test + @WithMockUser(roles = "ANYTHING") + public void getWhenAccessDeniedOverriddenThenCustomizesResponseByRequest() + throws Exception { + this.spring.register(RequestMatcherBasedAccessDeniedHandlerConfig.class).autowire(); + + this.mvc.perform(get("/hello")) + .andExpect(status().isIAmATeapot()); + + this.mvc.perform(get("/goodbye")) + .andExpect(status().isForbidden()); + } + + @EnableWebSecurity + static class RequestMatcherBasedAccessDeniedHandlerConfig extends WebSecurityConfigurerAdapter { + AccessDeniedHandler teapotDeniedHandler = + (request, response, exception) -> + response.setStatus(HttpStatus.I_AM_A_TEAPOT.value()); + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().denyAll() + .and() + .exceptionHandling() + .defaultAccessDeniedHandlerFor( + this.teapotDeniedHandler, + new AntPathRequestMatcher("/hello/**")) + .defaultAccessDeniedHandlerFor( + new AccessDeniedHandlerImpl(), + AnyRequestMatcher.INSTANCE); + // @formatter:on + } + } + + @Test + @WithMockUser(roles = "ANYTHING") + public void getWhenAccessDeniedOverriddenByOnlyOneHandlerThenAllRequestsUseThatHandler() + throws Exception { + this.spring.register(SingleRequestMatcherAccessDeniedHandlerConfig.class).autowire(); + + this.mvc.perform(get("/hello")) + .andExpect(status().isIAmATeapot()); + + this.mvc.perform(get("/goodbye")) + .andExpect(status().isIAmATeapot()); + } + + @EnableWebSecurity + static class SingleRequestMatcherAccessDeniedHandlerConfig extends WebSecurityConfigurerAdapter { + AccessDeniedHandler teapotDeniedHandler = + (request, response, exception) -> + response.setStatus(HttpStatus.I_AM_A_TEAPOT.value()); + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().denyAll() + .and() + .exceptionHandling() + .defaultAccessDeniedHandlerFor( + this.teapotDeniedHandler, + new AntPathRequestMatcher("/hello/**")); + // @formatter:on + } + } +} diff --git a/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingAccessDeniedHandler.java b/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingAccessDeniedHandler.java new file mode 100644 index 00000000000..22e8eb10cdf --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingAccessDeniedHandler.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.web.access; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map.Entry; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; + +/** + * An {@link AccessDeniedHandler} that delegates to other {@link AccessDeniedHandler} + * instances based upon the type of {@link HttpServletRequest} passed into + * {@link #handle(HttpServletRequest, HttpServletResponse, AccessDeniedException)}. + * + * @author Josh Cummings + * @since 5.1 + * + */ +public final class RequestMatcherDelegatingAccessDeniedHandler implements AccessDeniedHandler { + private final LinkedHashMap handlers; + + private final AccessDeniedHandler defaultHandler; + + /** + * Creates a new instance + * + * @param handlers a map of {@link RequestMatcher}s to + * {@link AccessDeniedHandler}s that should be used. Each is considered in the order + * they are specified and only the first {@link AccessDeniedHandler} is used. + * @param defaultHandler the default {@link AccessDeniedHandler} that should be used + * if none of the matchers match. + */ + public RequestMatcherDelegatingAccessDeniedHandler( + LinkedHashMap handlers, + AccessDeniedHandler defaultHandler) { + Assert.notEmpty(handlers, "handlers cannot be null or empty"); + Assert.notNull(defaultHandler, "defaultHandler cannot be null"); + this.handlers = new LinkedHashMap<>(handlers); + this.defaultHandler = defaultHandler; + } + + public void handle(HttpServletRequest request, HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException, + ServletException { + for (Entry entry : this.handlers + .entrySet()) { + RequestMatcher matcher = entry.getKey(); + if (matcher.matches(request)) { + AccessDeniedHandler handler = entry.getValue(); + handler.handle(request, response, accessDeniedException); + return; + } + } + defaultHandler.handle(request, response, accessDeniedException); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingAccessDeniedHandlerTests.java b/web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingAccessDeniedHandlerTests.java new file mode 100644 index 00000000000..87e853ee733 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingAccessDeniedHandlerTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.access; + +import java.util.LinkedHashMap; +import javax.servlet.http.HttpServletRequest; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.web.util.matcher.RequestMatcher; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author Josh Cummings + */ +public class RequestMatcherDelegatingAccessDeniedHandlerTests { + private RequestMatcherDelegatingAccessDeniedHandler delegator; + private LinkedHashMap deniedHandlers; + private AccessDeniedHandler accessDeniedHandler; + private HttpServletRequest request; + + @Before + public void setup() { + this.accessDeniedHandler = mock(AccessDeniedHandler.class); + this.deniedHandlers = new LinkedHashMap<>(); + this.request = new MockHttpServletRequest(); + } + + @Test + public void handleWhenNothingMatchesThenOnlyDefaultHandlerInvoked() throws Exception { + AccessDeniedHandler handler = mock(AccessDeniedHandler.class); + RequestMatcher matcher = mock(RequestMatcher.class); + when(matcher.matches(this.request)).thenReturn(false); + this.deniedHandlers.put(matcher, handler); + this.delegator = new RequestMatcherDelegatingAccessDeniedHandler(this.deniedHandlers, this.accessDeniedHandler); + + this.delegator.handle(this.request, null, null); + + verify(this.accessDeniedHandler).handle(this.request, null, null); + verify(handler, never()).handle(this.request, null, null); + } + + @Test + public void handleWhenFirstMatchesThenOnlyFirstInvoked() throws Exception { + AccessDeniedHandler firstHandler = mock(AccessDeniedHandler.class); + RequestMatcher firstMatcher = mock(RequestMatcher.class); + AccessDeniedHandler secondHandler = mock(AccessDeniedHandler.class); + RequestMatcher secondMatcher = mock(RequestMatcher.class); + when(firstMatcher.matches(this.request)).thenReturn(true); + this.deniedHandlers.put(firstMatcher, firstHandler); + this.deniedHandlers.put(secondMatcher, secondHandler); + this.delegator = new RequestMatcherDelegatingAccessDeniedHandler(this.deniedHandlers, this.accessDeniedHandler); + + this.delegator.handle(this.request, null, null); + + verify(firstHandler).handle(this.request, null, null); + verify(secondHandler, never()).handle(this.request, null, null); + verify(this.accessDeniedHandler, never()).handle(this.request, null, null); + verify(secondMatcher, never()).matches(this.request); + } + + @Test + public void handleWhenSecondMatchesThenOnlySecondInvoked() throws Exception { + AccessDeniedHandler firstHandler = mock(AccessDeniedHandler.class); + RequestMatcher firstMatcher = mock(RequestMatcher.class); + AccessDeniedHandler secondHandler = mock(AccessDeniedHandler.class); + RequestMatcher secondMatcher = mock(RequestMatcher.class); + when(firstMatcher.matches(this.request)).thenReturn(false); + when(secondMatcher.matches(this.request)).thenReturn(true); + this.deniedHandlers.put(firstMatcher, firstHandler); + this.deniedHandlers.put(secondMatcher, secondHandler); + this.delegator = new RequestMatcherDelegatingAccessDeniedHandler(this.deniedHandlers, this.accessDeniedHandler); + + this.delegator.handle(this.request, null, null); + + verify(secondHandler).handle(this.request, null, null); + verify(firstHandler, never()).handle(this.request, null, null); + verify(this.accessDeniedHandler, never()).handle(this.request, null, null); + } +} From 7f7a4b233c613c3cbc7b8c41642bc6721ec7550c Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 29 Jun 2018 14:08:42 -0600 Subject: [PATCH 125/226] Remap Nimbus JSON Parsing Errors When Nimbus fails to parse either a JWK response or a JWT response, the error message contains information that either should or cannot be included in a Bearer Token response. For example, if the response from a JWK endpoint is invalid JSON, then Nimbus will send the entire response from the authentication server in the resulting exception message. This commit captures these exceptions and removes the parsing detail, replacing it with more generic information about the nature of the error. Fixes: gh-5517 --- .../jwt/NimbusJwtDecoderJwkSupport.java | 35 ++++++++--- .../jwt/NimbusJwtDecoderJwkSupportTests.java | 59 ++++++++++++++++++- 2 files changed, 83 insertions(+), 11 deletions(-) diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java index dc93bf0e05e..96df899fda6 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,15 @@ */ package org.springframework.security.oauth2.jwt; +import java.net.MalformedURLException; +import java.net.URL; +import java.text.ParseException; +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; + import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.RemoteKeySourceException; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.jwk.source.RemoteJWKSet; import com.nimbusds.jose.proc.JWSKeySelector; @@ -29,15 +37,10 @@ import com.nimbusds.jwt.SignedJWT; import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; import com.nimbusds.jwt.proc.DefaultJWTProcessor; + import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; import org.springframework.util.Assert; -import java.net.MalformedURLException; -import java.net.URL; -import java.time.Instant; -import java.util.LinkedHashMap; -import java.util.Map; - /** * An implementation of a {@link JwtDecoder} that "decodes" a * JSON Web Token (JWT) and additionally verifies it's digital signature if the JWT is a @@ -48,6 +51,7 @@ * NOTE: This implementation uses the Nimbus JOSE + JWT SDK internally. * * @author Joe Grandja + * @author Josh Cummings * @since 5.0 * @see JwtDecoder * @see JSON Web Token (JWT) @@ -56,6 +60,9 @@ * @see Nimbus JOSE + JWT SDK */ public final class NimbusJwtDecoderJwkSupport implements JwtDecoder { + private static final String DECODING_ERROR_MESSAGE_TEMPLATE = + "An error occurred while attempting to decode the Jwt: %s"; + private final URL jwkSetUrl; private final JWSAlgorithm jwsAlgorithm; private final ConfigurableJWTProcessor jwtProcessor; @@ -108,7 +115,7 @@ private JWT parse(String token) { try { return JWTParser.parse(token); } catch (Exception ex) { - throw new JwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex); + throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex); } } @@ -135,8 +142,18 @@ private Jwt createJwt(String token, JWT parsedJwt) { jwt = new Jwt(token, issuedAt, expiresAt, headers, jwtClaimsSet.getClaims()); + } catch (RemoteKeySourceException ex) { + if (ex.getCause() instanceof ParseException) { + throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, "Malformed Jwk set")); + } else { + throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex); + } } catch (Exception ex) { - throw new JwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex); + if (ex.getCause() instanceof ParseException) { + throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, "Malformed payload")); + } else { + throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex); + } } return jwt; diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java index 37ed64f0bee..e90560cca7f 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java @@ -22,10 +22,14 @@ import com.nimbusds.jwt.JWTParser; import com.nimbusds.jwt.SignedJWT; import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; import org.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.security.oauth2.jose.jws.JwsAlgorithms; import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; @@ -46,11 +50,17 @@ */ @RunWith(PowerMockRunner.class) @PrepareForTest({NimbusJwtDecoderJwkSupport.class, JWTParser.class}) +@PowerMockIgnore("okhttp3.*") public class NimbusJwtDecoderJwkSupportTests { private static final String JWK_SET_URL = "https://provider.com/oauth2/keys"; private static final String JWS_ALGORITHM = JwsAlgorithms.RS256; - private String unsignedToken = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOi0yMDMzMjI0OTcsImp0aSI6IjEyMyIsInR5cCI6IkpXVCJ9."; + private static final String JWK_SET = "{\"keys\":[{\"p\":\"49neceJFs8R6n7WamRGy45F5Tv0YM-R2ODK3eSBUSLOSH2tAqjEVKOkLE5fiNA3ygqq15NcKRadB2pTVf-Yb5ZIBuKzko8bzYIkIqYhSh_FAdEEr0vHF5fq_yWSvc6swsOJGqvBEtuqtJY027u-G2gAQasCQdhyejer68zsTn8M\",\"kty\":\"RSA\",\"q\":\"tWR-ysspjZ73B6p2vVRVyHwP3KQWL5KEQcdgcmMOE_P_cPs98vZJfLhxobXVmvzuEWBpRSiqiuyKlQnpstKt94Cy77iO8m8ISfF3C9VyLWXi9HUGAJb99irWABFl3sNDff5K2ODQ8CmuXLYM25OwN3ikbrhEJozlXg_NJFSGD4E\",\"d\":\"FkZHYZlw5KSoqQ1i2RA2kCUygSUOf1OqMt3uomtXuUmqKBm_bY7PCOhmwbvbn4xZYEeHuTR8Xix-0KpHe3NKyWrtRjkq1T_un49_1LLVUhJ0dL-9_x0xRquVjhl_XrsRXaGMEHs8G9pLTvXQ1uST585gxIfmCe0sxPZLvwoic-bXf64UZ9BGRV3lFexWJQqCZp2S21HfoU7wiz6kfLRNi-K4xiVNB1gswm_8o5lRuY7zB9bRARQ3TS2G4eW7p5sxT3CgsGiQD3_wPugU8iDplqAjgJ5ofNJXZezoj0t6JMB_qOpbrmAM1EnomIPebSLW7Ky9SugEd6KMdL5lW6AuAQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"wdkFu_tV2V1l_PWUUimG516Zvhqk2SWDw1F7uNDD-Lvrv_WNRIJVzuffZ8WYiPy8VvYQPJUrT2EXL8P0ocqwlaSTuXctrORcbjwgxDQDLsiZE0C23HYzgi0cofbScsJdhcBg7d07LAf7cdJWG0YVl1FkMCsxUlZ2wTwHfKWf-v4\",\"dp\":\"uwnPxqC-IxG4r33-SIT02kZC1IqC4aY7PWq0nePiDEQMQWpjjNH50rlq9EyLzbtdRdIouo-jyQXB01K15-XXJJ60dwrGLYNVqfsTd0eGqD1scYJGHUWG9IDgCsxyEnuG3s0AwbW2UolWVSsU2xMZGb9PurIUZECeD1XDZwMp2s0\",\"dq\":\"hra786AunB8TF35h8PpROzPoE9VJJMuLrc6Esm8eZXMwopf0yhxfN2FEAvUoTpLJu93-UH6DKenCgi16gnQ0_zt1qNNIVoRfg4rw_rjmsxCYHTVL3-RDeC8X_7TsEySxW0EgFTHh-nr6I6CQrAJjPM88T35KHtdFATZ7BCBB8AE\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}"; + private static final String MALFORMED_JWK_SET = "malformed"; + + private static final String SIGNED_JWT = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJzY3AiOlsibWVzc2FnZTpyZWFkIl0sImV4cCI6NDY4Mzg5Nzc3Nn0.LtMVtIiRIwSyc3aX35Zl0JVwLTcQZAB3dyBOMHNaHCKUljwMrf20a_gT79LfhjDzE_fUVUmFiAO32W1vFnYpZSVaMDUgeIOIOpxfoe9shj_uYenAwIS-_UxqGVIJiJoXNZh_MK80ShNpvsQwamxWEEOAMBtpWNiVYNDMdfgho9n3o5_Z7Gjy8RLBo1tbDREbO9kTFwGIxm_EYpezmRCRq4w1DdS6UDW321hkwMxPnCMSWOvp-hRpmgY2yjzLgPJ6Aucmg9TJ8jloAP1DjJoF1gRR7NTAk8LOGkSjTzVYDYMbCF51YdpojhItSk80YzXiEsv1mTz4oMM49jXBmfXFMA"; + private static final String MALFORMED_JWT = "eyJhbGciOiJSUzI1NiJ9.eyJuYmYiOnt9LCJleHAiOjQ2ODQyMjUwODd9.guoQvujdWvd3xw7FYQEn4D6-gzM_WqFvXdmvAUNSLbxG7fv2_LLCNujPdrBHJoYPbOwS1BGNxIKQWS1tylvqzmr1RohQ-RZ2iAM1HYQzboUlkoMkcd8ENM__ELqho8aNYBfqwkNdUOyBFoy7Syu_w2SoJADw2RTjnesKO6CVVa05bW118pDS4xWxqC4s7fnBjmZoTn4uQ-Kt9YSQZQk8YQxkJSiyanozzgyfgXULA6mPu1pTNU3FVFaK1i1av_xtH_zAPgb647ZeaNe4nahgqC5h8nhOlm8W2dndXbwAt29nd2ZWBsru_QwZz83XSKLhTPFz-mPBByZZDsyBbIHf9A"; + private static final String UNSIGNED_JWT = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOi0yMDMzMjI0OTcsImp0aSI6IjEyMyIsInR5cCI6IkpXVCJ9."; @Test public void constructorWhenJwkSetUrlIsNullThenThrowIllegalArgumentException() { @@ -102,8 +112,53 @@ public void decodeWhenExpClaimNullThenDoesNotThrowException() throws Exception { public void decodeWhenPlainJwtThenExceptionDoesNotMentionClass() throws Exception { NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(JWK_SET_URL, JWS_ALGORITHM); - assertThatCode(() -> jwtDecoder.decode(this.unsignedToken)) + assertThatCode(() -> jwtDecoder.decode(UNSIGNED_JWT)) .isInstanceOf(JwtException.class) .hasMessageContaining("Unsupported algorithm of none"); } + + @Test + public void decodeWhenJwtIsMalformedThenReturnsStockException() throws Exception { + try ( MockWebServer server = new MockWebServer() ) { + server.enqueue(new MockResponse().setBody(JWK_SET)); + String jwkSetUrl = server.url("/.well-known/jwks.json").toString(); + + NimbusJwtDecoderJwkSupport decoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl); + + assertThatCode(() -> decoder.decode(MALFORMED_JWT)) + .isInstanceOf(JwtException.class) + .hasMessage("An error occurred while attempting to decode the Jwt: Malformed payload"); + } + } + + @Test + public void decodeWhenJwkResponseIsMalformedThenReturnsStockException() throws Exception { + try ( MockWebServer server = new MockWebServer() ) { + server.enqueue(new MockResponse().setBody(MALFORMED_JWK_SET)); + String jwkSetUrl = server.url("/.well-known/jwks.json").toString(); + + NimbusJwtDecoderJwkSupport decoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl); + + assertThatCode(() -> decoder.decode(SIGNED_JWT)) + .isInstanceOf(JwtException.class) + .hasMessage("An error occurred while attempting to decode the Jwt: Malformed Jwk set"); + } + } + + @Test + public void decodeWhenJwkEndpointIsUnresponsiveThenReturnsStockException() throws Exception { + try ( MockWebServer server = new MockWebServer() ) { + server.enqueue(new MockResponse().setBody(MALFORMED_JWK_SET)); + String jwkSetUrl = server.url("/.well-known/jwks.json").toString(); + + NimbusJwtDecoderJwkSupport decoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl); + + server.shutdown(); + + assertThatCode(() -> decoder.decode(SIGNED_JWT)) + .isInstanceOf(JwtException.class) + .hasMessage("An error occurred while attempting to decode the Jwt: " + + "Couldn't retrieve remote JWK set: Connection refused (Connection refused)"); + } + } } From 304f32eca58bee6b484a139c034517a13a9492a3 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 12 Jun 2018 21:33:26 -0600 Subject: [PATCH 126/226] Resource Server Jwt Support Introducing initial support for Jwt-Encoded Bearer Token authorization with remote JWK set signature verification. High-level features include: - Accepting bearer tokens as headers and form or query parameters - Verifying signatures from a remote Jwk set And: - A DSL for easy configuration - A sample to demonstrate usage Fixes: gh-5128 Fixes: gh-5125 Fixes: gh-5121 Fixes: gh-5130 Fixes: gh-5226 Fixes: gh-5237 --- config/spring-security-config.gradle | 1 + .../configurers/oauth2/OAuth2Configurer.java | 29 + .../OAuth2ResourceServerConfigurer.java | 225 +++++ .../OAuth2ResourceServerConfigurerTests.java | 828 ++++++++++++++++++ ...ResourceServerConfigurerTests-Default.jwks | 1 + ...h2ResourceServerConfigurerTests-Empty.jwks | 1 + ...esourceServerConfigurerTests-Expired.token | 1 + ...th2ResourceServerConfigurerTests-Kid.token | 1 + ...rverConfigurerTests-MalformedPayload.token | 1 + ...sourceServerConfigurerTests-TooEarly.token | 1 + ...ResourceServerConfigurerTests-TwoKeys.jwks | 1 + ...sourceServerConfigurerTests-Unsigned.token | 1 + ...onfigurerTests-ValidMessageReadScope.token | 1 + ...rConfigurerTests-ValidMessageReadScp.token | 1 + ...ConfigurerTests-ValidMessageWriteScp.token | 1 + ...eServerConfigurerTests-ValidNoScopes.token | 1 + ...ing-security-oauth2-resource-server.gradle | 14 + .../BearerTokenAuthenticationToken.java | 79 ++ .../server/resource/BearerTokenError.java | 125 +++ .../resource/BearerTokenErrorCodes.java | 46 + ...bstractOAuth2TokenAuthenticationToken.java | 102 +++ .../JwtAuthenticationProvider.java | 151 ++++ .../JwtAuthenticationToken.java | 74 ++ .../resource/authentication/package-info.java | 20 + .../oauth2/server/resource/package-info.java | 20 + .../BearerTokenAuthenticationEntryPoint.java | 124 +++ .../web/BearerTokenAuthenticationFilter.java | 143 +++ .../resource/web/BearerTokenResolver.java | 43 + .../web/DefaultBearerTokenResolver.java | 127 +++ .../BearerTokenAccessDeniedHandler.java | 137 +++ .../resource/web/access/package-info.java | 20 + .../server/resource/web/package-info.java | 20 + .../BearerTokenAuthenticationTokenTests.java | 52 ++ .../resource/BearerTokenErrorTests.java | 138 +++ .../JwtAuthenticationProviderTests.java | 230 +++++ .../JwtAuthenticationTokenTests.java | 107 +++ ...rerTokenAuthenticationEntryPointTests.java | 202 +++++ .../BearerTokenAuthenticationFilterTests.java | 173 ++++ .../web/DefaultBearerTokenResolverTests.java | 160 ++++ .../BearerTokenAccessDeniedHandlerTests.java | 250 ++++++ samples/boot/oauth2resourceserver/README.adoc | 104 +++ ...y-samples-boot-oauth2resourceserver.gradle | 13 + ...OAuth2ResourceServerApplicationITests.java | 101 +++ .../resources/application-test.yml | 1 + .../OAuth2ResourceServerApplication.java | 30 + .../OAuth2ResourceServerController.java | 38 + ...h2ResourceServerSecurityConfiguration.java | 45 + .../java/sample/provider/MockProvider.java | 115 +++ .../main/resources/META-INF/spring.factories | 1 + .../src/main/resources/application.yml | 1 + 50 files changed, 4101 insertions(+) create mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java create mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java create mode 100644 config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Default.jwks create mode 100644 config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Empty.jwks create mode 100644 config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Expired.token create mode 100644 config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Kid.token create mode 100644 config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-MalformedPayload.token create mode 100644 config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-TooEarly.token create mode 100644 config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-TwoKeys.jwks create mode 100644 config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Unsigned.token create mode 100644 config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageReadScope.token create mode 100644 config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageReadScp.token create mode 100644 config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageWriteScp.token create mode 100644 config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidNoScopes.token create mode 100644 oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenAuthenticationToken.java create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenError.java create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenErrorCodes.java create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.java create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/package-info.java create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/package-info.java create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPoint.java create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilter.java create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenResolver.java create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolver.java create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandler.java create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/package-info.java create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/package-info.java create mode 100644 oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/BearerTokenAuthenticationTokenTests.java create mode 100644 oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/BearerTokenErrorTests.java create mode 100644 oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProviderTests.java create mode 100644 oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java create mode 100644 oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPointTests.java create mode 100644 oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilterTests.java create mode 100644 oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolverTests.java create mode 100644 oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandlerTests.java create mode 100644 samples/boot/oauth2resourceserver/README.adoc create mode 100644 samples/boot/oauth2resourceserver/spring-security-samples-boot-oauth2resourceserver.gradle create mode 100644 samples/boot/oauth2resourceserver/src/integration-test/java/sample/OAuth2ResourceServerApplicationITests.java create mode 100644 samples/boot/oauth2resourceserver/src/integration-test/resources/application-test.yml create mode 100644 samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerApplication.java create mode 100644 samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerController.java create mode 100644 samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java create mode 100644 samples/boot/oauth2resourceserver/src/main/java/sample/provider/MockProvider.java create mode 100644 samples/boot/oauth2resourceserver/src/main/resources/META-INF/spring.factories create mode 100644 samples/boot/oauth2resourceserver/src/main/resources/application.yml diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index ecce84d7a9d..4891d67cc4e 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -13,6 +13,7 @@ dependencies { optional project(':spring-security-messaging') optional project(':spring-security-oauth2-client') optional project(':spring-security-oauth2-jose') + optional project(':spring-security-oauth2-resource-server') optional project(':spring-security-openid') optional project(':spring-security-web') optional 'io.projectreactor:reactor-core' diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java index 57d42deb2c6..772e8122970 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java @@ -21,6 +21,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2ClientConfigurer; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; /** * An {@link AbstractHttpConfigurer} that provides support for the @@ -40,6 +41,8 @@ public final class OAuth2Configurer> private OAuth2ClientConfigurer clientConfigurer; + private OAuth2ResourceServerConfigurer resourceServerConfigurer; + /** * Returns the {@link OAuth2ClientConfigurer} for configuring OAuth 2.0 Client support. * @@ -52,11 +55,27 @@ public OAuth2ClientConfigurer client() { return this.clientConfigurer; } + /** + * Returns the {@link OAuth2ResourceServerConfigurer} for configuring OAuth 2.0 Resource Server support. + * + * @return the {@link OAuth2ResourceServerConfigurer} + */ + public OAuth2ResourceServerConfigurer resourceServer() { + if (this.resourceServerConfigurer == null) { + this.initResourceServerConfigurer(); + } + return this.resourceServerConfigurer; + } + @Override public void init(B builder) throws Exception { if (this.clientConfigurer != null) { this.clientConfigurer.init(builder); } + + if (this.resourceServerConfigurer != null) { + this.resourceServerConfigurer.init(builder); + } } @Override @@ -64,6 +83,10 @@ public void configure(B builder) throws Exception { if (this.clientConfigurer != null) { this.clientConfigurer.configure(builder); } + + if (this.resourceServerConfigurer != null) { + this.resourceServerConfigurer.configure(builder); + } } private void initClientConfigurer() { @@ -71,4 +94,10 @@ private void initClientConfigurer() { this.clientConfigurer.setBuilder(this.getBuilder()); this.clientConfigurer.addObjectPostProcessor(this.objectPostProcessor); } + + private void initResourceServerConfigurer() { + this.resourceServerConfigurer = new OAuth2ResourceServerConfigurer<>(); + this.resourceServerConfigurer.setBuilder(this.getBuilder()); + this.resourceServerConfigurer.addObjectPostProcessor(this.objectPostProcessor); + } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java new file mode 100644 index 00000000000..faba20fa3e9 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java @@ -0,0 +1,225 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.configurers.oauth2.server.resource; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; +import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint; +import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter; +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; +import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver; +import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; + +/** + * + * An {@link AbstractHttpConfigurer} for OAuth 2.0 Resource Server Support. + * + * By default, this wires a {@link BearerTokenAuthenticationFilter}, which can be used to parse the request + * for bearer tokens and make an authentication attempt. + * + *

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

        + *
      • {@link #jwt()} - enables Jwt-encoded bearer token support
      • + *
      + * + *

      + * When using {@link #jwt()}, a Jwk Set Uri must be supplied via {@link JwtConfigurer#jwkSetUri} + * + *

      Security Filters

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

      Shared Objects Created

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

      Shared Objects Used

      + * + * The following shared objects are used: + * + *
        + *
      • {@link AuthenticationManager}
      • + *
      + * + * If {@link #jwt()} isn't supplied, then the {@link BearerTokenAuthenticationFilter} is still added, but without + * any OAuth 2.0 {@link AuthenticationProvider}s. This is useful if needing to switch out Spring Security's Jwt support + * for a custom one. + * + * @author Josh Cummings + * @since 5.1 + * @see BearerTokenAuthenticationFilter + * @see JwtAuthenticationProvider + * @see NimbusJwtDecoderJwkSupport + * @see AbstractHttpConfigurer + */ +public final class OAuth2ResourceServerConfigurer> extends + AbstractHttpConfigurer, H> { + + private BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver(); + private BearerTokenRequestMatcher requestMatcher = new BearerTokenRequestMatcher(); + + private BearerTokenAuthenticationEntryPoint authenticationEntryPoint + = new BearerTokenAuthenticationEntryPoint(); + + private BearerTokenAccessDeniedHandler accessDeniedHandler + = new BearerTokenAccessDeniedHandler(); + + private JwtConfigurer jwtConfigurer = new JwtConfigurer(); + + public JwtConfigurer jwt() { + return this.jwtConfigurer; + } + + @Override + public void setBuilder(H http) { + super.setBuilder(http); + initSessionCreationPolicy(http); + } + + @Override + public void init(H http) throws Exception { + registerDefaultDeniedHandler(http); + registerDefaultEntryPoint(http); + registerDefaultCsrfOverride(http); + } + + @Override + public void configure(H http) throws Exception { + BearerTokenResolver bearerTokenResolver = getBearerTokenResolver(); + this.requestMatcher.setBearerTokenResolver(bearerTokenResolver); + + AuthenticationManager manager = http.getSharedObject(AuthenticationManager.class); + + BearerTokenAuthenticationFilter filter = + new BearerTokenAuthenticationFilter(manager); + filter.setBearerTokenResolver(bearerTokenResolver); + filter = postProcess(filter); + + http.addFilterBefore(filter, BasicAuthenticationFilter.class); + + JwtDecoder decoder = this.jwtConfigurer.getJwtDecoder(); + + if (decoder != null) { + JwtAuthenticationProvider provider = + new JwtAuthenticationProvider(decoder); + provider = postProcess(provider); + + http.authenticationProvider(provider); + } else { + throw new IllegalStateException("Jwt is the only supported format for bearer tokens " + + "in Spring Security and no instance of JwtDecoder could be found. Make sure to specify " + + "a jwk set uri by doing http.oauth2().resourceServer().jwt().jwkSetUri(uri)"); + } + } + + public class JwtConfigurer { + private JwtDecoder decoder; + + private JwtConfigurer() {} + + public OAuth2ResourceServerConfigurer jwkSetUri(String uri) { + this.decoder = new NimbusJwtDecoderJwkSupport(uri); + return OAuth2ResourceServerConfigurer.this; + } + + private JwtDecoder getJwtDecoder() { + return this.decoder; + } + } + + private void initSessionCreationPolicy(H http) { + if (http.getSharedObject(SessionCreationPolicy.class) == null) { + http.setSharedObject(SessionCreationPolicy.class, SessionCreationPolicy.STATELESS); + } + } + + private void registerDefaultDeniedHandler(H http) { + ExceptionHandlingConfigurer exceptionHandling = http + .getConfigurer(ExceptionHandlingConfigurer.class); + if (exceptionHandling == null) { + return; + } + + exceptionHandling.defaultAccessDeniedHandlerFor( + this.accessDeniedHandler, + this.requestMatcher); + } + + private void registerDefaultEntryPoint(H http) { + ExceptionHandlingConfigurer exceptionHandling = http + .getConfigurer(ExceptionHandlingConfigurer.class); + if (exceptionHandling == null) { + return; + } + + exceptionHandling.defaultAuthenticationEntryPointFor( + this.authenticationEntryPoint, + this.requestMatcher); + } + + private void registerDefaultCsrfOverride(H http) { + CsrfConfigurer csrf = http + .getConfigurer(CsrfConfigurer.class); + if (csrf == null) { + return; + } + + csrf.ignoringRequestMatchers(this.requestMatcher); + } + + private BearerTokenResolver getBearerTokenResolver() { + return this.bearerTokenResolver; + } + + private static final class BearerTokenRequestMatcher implements RequestMatcher { + private BearerTokenResolver bearerTokenResolver + = new DefaultBearerTokenResolver(); + + @Override + public boolean matches(HttpServletRequest request) { + return this.bearerTokenResolver.resolve(request) != null; + } + + public void setBearerTokenResolver(BearerTokenResolver tokenResolver) { + Assert.notNull(tokenResolver, "resolver cannot be null"); + this.bearerTokenResolver = tokenResolver; + } + } +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java new file mode 100644 index 00000000000..4f58b098964 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -0,0 +1,828 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.configurers.oauth2.server.resource; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.stream.Collectors; +import javax.annotation.PreDestroy; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.util.ReflectionUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.POST; + +/** + * Tests for {@link OAuth2ResourceServerConfigurer} + * + * @author Josh Cummings + */ +public class OAuth2ResourceServerConfigurerTests { + + @Autowired + MockMvc mvc; + + @Autowired(required = false) + MockWebServer authz; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Test + public void getWhenUsingDefaultsWithValidBearerTokenThenAcceptsRequest() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidNoScopes"); + + this.mvc.perform(get("/").with(bearerToken(token))) + .andExpect(status().isOk()) + .andExpect(content().string("ok")); + } + + @Test + public void getWhenUsingDefaultsWithExpiredBearerTokenThenInvalidToken() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("Expired"); + + this.mvc.perform(get("/").with(bearerToken(token))) + .andExpect(status().isUnauthorized()) + .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: Expired JWT")); + } + + @Test + public void getWhenUsingDefaultsWithBadJwkEndpointThenInvalidToken() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class).autowire(); + this.authz.enqueue(new MockResponse().setBody("malformed")); + String token = this.token("ValidNoScopes"); + + this.mvc.perform(get("/").with(bearerToken(token))) + .andExpect(status().isUnauthorized()) + .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: Malformed Jwk set")); + } + + @Test + public void getWhenUsingDefaultsWithUnavailableJwkEndpointThenInvalidToken() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class).autowire(); + this.authz.shutdown(); + String token = this.token("ValidNoScopes"); + + this.mvc.perform(get("/").with(bearerToken(token))) + .andExpect(status().isUnauthorized()) + .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: " + + "Couldn't retrieve remote JWK set: Connection refused (Connection refused)")); + } + + @Test + public void getWhenUsingDefaultsWithMalformedBearerTokenThenInvalidToken() + throws Exception { + + this.spring.register(DefaultConfig.class).autowire(); + + this.mvc.perform(get("/").with(bearerToken("an\"invalid\"token"))) + .andExpect(status().isUnauthorized()) + .andExpect(invalidTokenHeader("Bearer token is malformed")); + } + + @Test + public void getWhenUsingDefaultsWithMalformedPayloadThenInvalidToken() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("MalformedPayload"); + + this.mvc.perform(get("/").with(bearerToken(token))) + .andExpect(status().isUnauthorized()) + .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: " + + "Malformed payload")); + } + + @Test + public void getWhenUsingDefaultsWithUnsignedBearerTokenThenInvalidToken() + throws Exception { + + this.spring.register(DefaultConfig.class).autowire(); + String token = this.token("Unsigned"); + + this.mvc.perform(get("/").with(bearerToken(token))) + .andExpect(status().isUnauthorized()) + .andExpect(invalidTokenHeader("Unsupported algorithm of none")); + } + + @Test + public void getWhenUsingDefaultsWithBearerTokenBeforeNotBeforeThenInvalidToken() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("TooEarly"); + + this.mvc.perform(get("/").with(bearerToken(token))) + .andExpect(status().isUnauthorized()) + .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: " + + "JWT before use time")); + } + + @Test + public void getWhenUsingDefaultsWithBearerTokenInTwoPlacesThenInvalidRequest() + throws Exception { + + this.spring.register(DefaultConfig.class).autowire(); + + this.mvc.perform(get("/") + .with(bearerToken("token")) + .with(bearerToken("token").asParam())) + .andExpect(status().isBadRequest()) + .andExpect(invalidRequestHeader("Found multiple bearer tokens in the request")); + } + + @Test + public void getWhenUsingDefaultsWithBearerTokenInTwoParametersThenInvalidRequest() + throws Exception { + + this.spring.register(DefaultConfig.class).autowire(); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("access_token", "token1"); + params.add("access_token", "token2"); + + this.mvc.perform(get("/") + .params(params)) + .andExpect(status().isBadRequest()) + .andExpect(invalidRequestHeader("Found multiple bearer tokens in the request")); + } + + @Test + public void postWhenUsingDefaultsWithBearerTokenAsFormParameterThenIgnoresToken() + throws Exception { + + this.spring.register(DefaultConfig.class).autowire(); + + this.mvc.perform(post("/") // engage csrf + .with(bearerToken("token").asParam())) + .andExpect(status().isForbidden()) + .andExpect(header().doesNotExist(HttpHeaders.WWW_AUTHENTICATE)); + } + + @Test + public void postWhenCsrfDisabledWithBearerTokenAsFormParameterThenIgnoresToken() + throws Exception { + + this.spring.register(CsrfDisabledConfig.class).autowire(); + + this.mvc.perform(post("/") + .with(bearerToken("token").asParam())) + .andExpect(status().isUnauthorized()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer")); + } + + @Test + public void getWhenUsingDefaultsWithNoBearerTokenThenUnauthorized() + throws Exception { + + this.spring.register(DefaultConfig.class).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isUnauthorized()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer")); + } + + @Test + public void getWhenUsingDefaultsWithSufficientlyScopedBearerTokenThenAcceptsRequest() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidMessageReadScope"); + + this.mvc.perform(get("/requires-read-scope") + .with(bearerToken(token))) + .andExpect(status().isOk()) + .andExpect(content().string("SCOPE_message:read")); + } + + @Test + public void getWhenUsingDefaultsWithInsufficientScopeThenInsufficientScopeError() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidNoScopes"); + + this.mvc.perform(get("/requires-read-scope") + .with(bearerToken(token))) + .andExpect(status().isForbidden()) + .andExpect(insufficientScopeHeader("")); + } + + @Test + public void getWhenUsingDefaultsWithInsufficientScpThenInsufficientScopeError() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidMessageWriteScp"); + + this.mvc.perform(get("/requires-read-scope") + .with(bearerToken(token))) + .andExpect(status().isForbidden()) + .andExpect(insufficientScopeHeader("message:write")); + } + + @Test + public void getWhenUsingDefaultsAndAuthorizationServerHasNoMatchingKeyThenInvalidToken() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class).autowire(); + this.authz.enqueue(this.jwks("Empty")); + String token = this.token("ValidNoScopes"); + + this.mvc.perform(get("/") + .with(bearerToken(token))) + .andExpect(status().isUnauthorized()) + .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: " + + "Signed JWT rejected: Another algorithm expected, or no matching key(s) found")); + } + + @Test + public void getWhenUsingDefaultsAndAuthorizationServerHasMultipleMatchingKeysThenOk() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("TwoKeys")); + String token = this.token("ValidNoScopes"); + + this.mvc.perform(get("/authenticated") + .with(bearerToken(token))) + .andExpect(status().isOk()) + .andExpect(content().string("test-subject")); + } + + @Test + public void getWhenUsingDefaultsAndKeyMatchesByKidThenOk() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("TwoKeys")); + String token = this.token("Kid"); + + this.mvc.perform(get("/authenticated") + .with(bearerToken(token))) + .andExpect(status().isOk()) + .andExpect(content().string("test-subject")); + } + + // -- Method Security + + @Test + public void getWhenUsingMethodSecurityWithValidBearerTokenThenAcceptsRequest() + throws Exception { + + this.spring.register(WebServerConfig.class, MethodSecurityConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidMessageReadScope"); + + this.mvc.perform(get("/ms-requires-read-scope") + .with(bearerToken(token))) + .andExpect(status().isOk()) + .andExpect(content().string("SCOPE_message:read")); + } + + @Test + public void getWhenUsingMethodSecurityWithValidBearerTokenHavingScpAttributeThenAcceptsRequest() + throws Exception { + + this.spring.register(WebServerConfig.class, MethodSecurityConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidMessageReadScp"); + + this.mvc.perform(get("/ms-requires-read-scope") + .with(bearerToken(token))) + .andExpect(status().isOk()) + .andExpect(content().string("SCOPE_message:read")); + } + + @Test + public void getWhenUsingMethodSecurityWithInsufficientScopeThenInsufficientScopeError() + throws Exception { + + this.spring.register(WebServerConfig.class, MethodSecurityConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidNoScopes"); + + this.mvc.perform(get("/ms-requires-read-scope") + .with(bearerToken(token))) + .andExpect(status().isForbidden()) + .andExpect(insufficientScopeHeader("")); + + } + + @Test + public void getWhenUsingMethodSecurityWithInsufficientScpThenInsufficientScopeError() + throws Exception { + + this.spring.register(WebServerConfig.class, MethodSecurityConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidMessageWriteScp"); + + this.mvc.perform(get("/ms-requires-read-scope") + .with(bearerToken(token))) + .andExpect(status().isForbidden()) + .andExpect(insufficientScopeHeader("message:write")); + } + + @Test + public void getWhenUsingMethodSecurityWithDenyAllThenInsufficientScopeError() + throws Exception { + + this.spring.register(WebServerConfig.class, MethodSecurityConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidMessageReadScope"); + + this.mvc.perform(get("/ms-deny") + .with(bearerToken(token))) + .andExpect(status().isForbidden()) + .andExpect(insufficientScopeHeader("message:read")); + } + + // -- Resource Server should not engage csrf + + @Test + public void postWhenUsingDefaultsWithValidBearerTokenAndNoCsrfTokenThenOk() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidNoScopes"); + + this.mvc.perform(post("/authenticated") + .with(bearerToken(token))) + .andExpect(status().isOk()) + .andExpect(content().string("test-subject")); + } + + @Test + public void postWhenUsingDefaultsWithNoBearerTokenThenCsrfDenies() + throws Exception { + + this.spring.register(DefaultConfig.class).autowire(); + + this.mvc.perform(post("/authenticated")) + .andExpect(status().isForbidden()) + .andExpect(header().doesNotExist(HttpHeaders.WWW_AUTHENTICATE)); + } + + @Test + public void postWhenUsingDefaultsWithExpiredBearerTokenAndNoCsrfThenInvalidToken() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("Expired"); + + this.mvc.perform(post("/authenticated") + .with(bearerToken(token))) + .andExpect(status().isUnauthorized()) + .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: Expired JWT")); + } + + // -- Resource Server should not create sessions + + @Test + public void requestWhenDefaultConfiguredThenSessionIsNotCreated() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidNoScopes"); + + MvcResult result = this.mvc.perform(get("/") + .with(bearerToken(token))) + .andExpect(status().isOk()) + .andReturn(); + + assertThat(result.getRequest().getSession(false)).isNull(); + } + + @Test + public void requestWhenUsingDefaultsAndNoBearerTokenThenSessionIsNotCreated() + throws Exception { + + this.spring.register(DefaultConfig.class, BasicController.class).autowire(); + + MvcResult result = this.mvc.perform(get("/")) + .andExpect(status().isUnauthorized()) + .andReturn(); + + assertThat(result.getRequest().getSession(false)).isNull(); + } + + @Test + public void requestWhenSessionManagementConfiguredThenUserConfigurationOverrides() + throws Exception { + + this.spring.register(WebServerConfig.class, AlwaysSessionCreationConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidNoScopes"); + + MvcResult result = this.mvc.perform(get("/") + .with(bearerToken(token))) + .andExpect(status().isOk()) + .andReturn(); + + assertThat(result.getRequest().getSession(false)).isNotNull(); + } + + // -- In combination with other authentication providers + + @Test + public void getWhenAlsoUsingHttpBasicThenCorrectProviderEngages() + throws Exception { + + this.spring.register(WebServerConfig.class, BasicAndResourceServerConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidNoScopes"); + + this.mvc.perform(get("/authenticated") + .with(bearerToken(token))) + .andExpect(status().isOk()) + .andExpect(content().string("test-subject")); + + this.mvc.perform(get("/authenticated") + .with(httpBasic("basic-user", "basic-password"))) + .andExpect(status().isOk()) + .andExpect(content().string("basic-user")); + } + + // -- Incorrect Configuration + + @Test + public void configuredWhenMissingJwtAuthenticationProviderThenWiringException() { + + assertThatCode(() -> this.spring.register(JwtlessConfig.class).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasMessageContaining("no instance of JwtDecoder"); + } + + @Test + public void configureWhenMissingJwkSetUriThenWiringException() { + + assertThatCode(() -> this.spring.register(JwtHalfConfiguredConfig.class).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasMessageContaining("no instance of JwtDecoder"); + } + + // -- support + + @EnableWebSecurity + static class DefaultConfig extends WebSecurityConfigurerAdapter { + @Value("${mock.jwk-set-uri:https://example.org}") String uri; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .antMatchers("/requires-read-scope").access("hasAuthority('SCOPE_message:read')") + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt() + .jwkSetUri(this.uri); + // @formatter:on + } + } + + @EnableWebSecurity + static class CsrfDisabledConfig extends WebSecurityConfigurerAdapter { + @Value("${mock.jwk-set-uri:https://example.org}") String uri; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .antMatchers("/requires-read-scope").access("hasAuthority('SCOPE_message:read')") + .anyRequest().authenticated() + .and() + .csrf().disable() + .oauth2() + .resourceServer() + .jwt() + .jwkSetUri(this.uri); + // @formatter:on + } + } + + @EnableWebSecurity + @EnableGlobalMethodSecurity(prePostEnabled = true) + static class MethodSecurityConfig extends WebSecurityConfigurerAdapter { + @Value("${mock.jwk-set-uri:https://example.org}") String uri; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt() + .jwkSetUri(this.uri); + // @formatter:on + } + } + + @EnableWebSecurity + static class JwtlessConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer(); + // @formatter:on + } + } + + @EnableWebSecurity + static class BasicAndResourceServerConfig extends WebSecurityConfigurerAdapter { + @Value("${mock.jwk-set-uri:https://example.org}") String uri; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .httpBasic() + .and() + .oauth2() + .resourceServer() + .jwt() + .jwkSetUri(this.uri); + // @formatter:on + } + + @Bean + public UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager( + org.springframework.security.core.userdetails.User.withDefaultPasswordEncoder() + .username("basic-user") + .password("basic-password") + .roles("USER") + .build()); + } + } + + @EnableWebSecurity + static class JwtHalfConfiguredConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt(); // missing key configuration, e.g. jwkSetUri + // @formatter:on + } + } + + @EnableWebSecurity + static class AlwaysSessionCreationConfig extends WebSecurityConfigurerAdapter { + @Value("${mock.jwk-set-uri}") String uri; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.ALWAYS) + .and() + .oauth2() + .resourceServer() + .jwt() + .jwkSetUri(this.uri); + // @formatter:on + } + } + + @RestController + static class BasicController { + @GetMapping("/") + public String get() { + return "ok"; + } + + @PostMapping("/post") + public String post() { + return "post"; + } + + @RequestMapping(value = "/authenticated", method = { GET, POST }) + public String authenticated(@AuthenticationPrincipal Authentication authentication) { + return authentication.getName(); + } + + @GetMapping("/requires-read-scope") + public String requiresReadScope(@AuthenticationPrincipal JwtAuthenticationToken token) { + return token.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .filter(auth -> auth.endsWith("message:read")) + .findFirst().orElse(null); + } + + @GetMapping("/ms-requires-read-scope") + @PreAuthorize("hasAuthority('SCOPE_message:read')") + public String msRequiresReadScope(@AuthenticationPrincipal JwtAuthenticationToken token) { + return requiresReadScope(token); + } + + @GetMapping("/ms-deny") + @PreAuthorize("denyAll") + public String deny() { + return "hmm, that's odd"; + } + } + + @Configuration + static class WebServerConfig implements BeanPostProcessor { + private final MockWebServer server = new MockWebServer(); + + @PreDestroy + public void shutdown() throws IOException { + this.server.shutdown(); + } + + @Bean + public MockWebServer authz() { + return this.server; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof WebSecurityConfigurerAdapter) { + Field f = ReflectionUtils.findField(bean.getClass(), field -> + field.getAnnotation(Value.class) != null); + if (f != null) { + ReflectionUtils.setField(f, bean, this.server.url("/.well-known/jwks.json").toString()); + } + } + return null; + } + } + + private static class BearerTokenRequestPostProcessor implements RequestPostProcessor { + private boolean asRequestParameter; + + private String token; + + public BearerTokenRequestPostProcessor(String token) { + this.token = token; + } + + public BearerTokenRequestPostProcessor asParam() { + this.asRequestParameter = true; + return this; + } + + @Override + public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { + if (this.asRequestParameter) { + request.setParameter("access_token", this.token); + } else { + request.addHeader("Authorization", "Bearer " + this.token); + } + + return request; + } + } + + private static BearerTokenRequestPostProcessor bearerToken(String token) { + return new BearerTokenRequestPostProcessor(token); + } + + private static ResultMatcher invalidRequestHeader(String message) { + return header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer " + + "error=\"invalid_request\", " + + "error_description=\"" + message + "\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\""); + } + + private static ResultMatcher invalidTokenHeader(String message) { + return header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer " + + "error=\"invalid_token\", " + + "error_description=\"" + message + "\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\""); + } + + private static ResultMatcher insufficientScopeHeader(String scope) { + return header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer " + + "error=\"insufficient_scope\"" + + ", error_description=\"The token provided has insufficient scope [" + scope + "] for this request\"" + + ", error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"" + + (StringUtils.hasText(scope) ? ", scope=\"" + scope + "\"" : "")); + } + + private String token(String name) throws IOException { + return resource(name + ".token"); + } + + private MockResponse jwks(String name) throws IOException { + String response = resource(name + ".jwks"); + return new MockResponse() + .setResponseCode(200) + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .setBody(response); + } + + private String resource(String suffix) throws IOException { + String name = this.getClass().getSimpleName() + "-" + suffix; + ClassPathResource resource = new ClassPathResource(name, this.getClass()); + try ( BufferedReader reader = new BufferedReader(new FileReader(resource.getFile())) ) { + return reader.lines().collect(Collectors.joining()); + } + } +} diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Default.jwks b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Default.jwks new file mode 100644 index 00000000000..ce5e6fbf2b4 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Default.jwks @@ -0,0 +1 @@ +{"keys":[{"p":"49neceJFs8R6n7WamRGy45F5Tv0YM-R2ODK3eSBUSLOSH2tAqjEVKOkLE5fiNA3ygqq15NcKRadB2pTVf-Yb5ZIBuKzko8bzYIkIqYhSh_FAdEEr0vHF5fq_yWSvc6swsOJGqvBEtuqtJY027u-G2gAQasCQdhyejer68zsTn8M","kty":"RSA","q":"tWR-ysspjZ73B6p2vVRVyHwP3KQWL5KEQcdgcmMOE_P_cPs98vZJfLhxobXVmvzuEWBpRSiqiuyKlQnpstKt94Cy77iO8m8ISfF3C9VyLWXi9HUGAJb99irWABFl3sNDff5K2ODQ8CmuXLYM25OwN3ikbrhEJozlXg_NJFSGD4E","d":"FkZHYZlw5KSoqQ1i2RA2kCUygSUOf1OqMt3uomtXuUmqKBm_bY7PCOhmwbvbn4xZYEeHuTR8Xix-0KpHe3NKyWrtRjkq1T_un49_1LLVUhJ0dL-9_x0xRquVjhl_XrsRXaGMEHs8G9pLTvXQ1uST585gxIfmCe0sxPZLvwoic-bXf64UZ9BGRV3lFexWJQqCZp2S21HfoU7wiz6kfLRNi-K4xiVNB1gswm_8o5lRuY7zB9bRARQ3TS2G4eW7p5sxT3CgsGiQD3_wPugU8iDplqAjgJ5ofNJXZezoj0t6JMB_qOpbrmAM1EnomIPebSLW7Ky9SugEd6KMdL5lW6AuAQ","e":"AQAB","use":"sig","kid":"one","qi":"wdkFu_tV2V1l_PWUUimG516Zvhqk2SWDw1F7uNDD-Lvrv_WNRIJVzuffZ8WYiPy8VvYQPJUrT2EXL8P0ocqwlaSTuXctrORcbjwgxDQDLsiZE0C23HYzgi0cofbScsJdhcBg7d07LAf7cdJWG0YVl1FkMCsxUlZ2wTwHfKWf-v4","dp":"uwnPxqC-IxG4r33-SIT02kZC1IqC4aY7PWq0nePiDEQMQWpjjNH50rlq9EyLzbtdRdIouo-jyQXB01K15-XXJJ60dwrGLYNVqfsTd0eGqD1scYJGHUWG9IDgCsxyEnuG3s0AwbW2UolWVSsU2xMZGb9PurIUZECeD1XDZwMp2s0","dq":"hra786AunB8TF35h8PpROzPoE9VJJMuLrc6Esm8eZXMwopf0yhxfN2FEAvUoTpLJu93-UH6DKenCgi16gnQ0_zt1qNNIVoRfg4rw_rjmsxCYHTVL3-RDeC8X_7TsEySxW0EgFTHh-nr6I6CQrAJjPM88T35KHtdFATZ7BCBB8AE","n":"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw"}]} diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Empty.jwks b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Empty.jwks new file mode 100644 index 00000000000..9d15e791b4f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Empty.jwks @@ -0,0 +1 @@ +{"keys":[]} diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Expired.token b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Expired.token new file mode 100644 index 00000000000..8010d048938 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Expired.token @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE1MzAyMzE3MTB9.c8vXYFwe1cBuglaZbmZFXJOmLsu_IQf-OsOiiOGhEJYOzu6h6v_qEzf2xxbu5TSvwAERmDITUSK41UIIvgU75WebtgilNnTR83B_gPM-7_FI2FLzlgVH7WayzvbYTQqepE_ZUMLFkGkK4r-dRiOyB9_cfl6jq_b5hE_biH1qrgPQrjlEhU8YxeK2EE05wsARLzyjoIYifkStjPC6rC-MLFIVk5JoITNzkTh7zYYSWtKWEgwd8S_vluVtJaPk-yKPb4tXcFRzCFl_qd7aCF8_LHyhw-4wvhWRIi8DmQmRU_a1RxR0mi-UCp0jMwmBZxxkSdqJ4l_EHI1yVqpgnbMLDw diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Kid.token b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Kid.token new file mode 100644 index 00000000000..4f53fdea73f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Kid.token @@ -0,0 +1 @@ +eyJraWQiOiJvbmUiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJleHAiOjQ2ODM4ODM1NzJ9.UhukjNEowC5lLCccvdjCUJad5J9FGNModegMZGe9qKIbXxmfseTttZUNn3_K_6aNCfimtmRktCRbw3fUTcje2TFJOJ6SmomLcQyjq7S41Wq6oBSA2fdqOOU4vNvrk8_pSExsSyN9bfWiJ51I8Agzbq5eUDNo_HEpaJZimrIe9f2_njU1GxvAWsq_h4UhHEgPPb3kY9kN9hVYX_oShhh7JxbLJBnfsKBOKGEWOsE65GlmDgQV4om6RGjJaz6jFHKJTCpH08ADA3j2dqT0LNy4PrUmbnjPjWVtSQJkGcgUkcQW6qz0K86ZfJZZng_iB2VadRm5qO-99ySKmlxa5A-_Iw diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-MalformedPayload.token b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-MalformedPayload.token new file mode 100644 index 00000000000..a6c9be5a210 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-MalformedPayload.token @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiJ9.eyJuYmYiOnt9LCJleHAiOjQ2ODM4OTEyMTl9.kpdv6ZXyYszZUzA4mJpviCBPzPftk6tIbIn5OoMuM09MKZCUCAFD8Y1tDmjzbWdkR_5CYiFMvSLq6DzAlugtGRAShc93dmDlyZmhcct2G477FxWaRKbtmFDjzuCjGyn7xHWpS7Wz6-Ngb-JyGI2m7FxXCgCpiYYBl-4-ONTuAT0fArJi_voA8K6YLnnjEjEprI3wsQRoS3Twa_fVdGkpMNlOGsQOqmlfjDrXpyfiANOe_ZztHxbDtJEZ9zfELxx9fzkZgTL1fD2Sj6HueDU-tMt-6IaGpBCLsg7d85RK001-U9u3Ph9awQC4QZK-8-F9OUUCY5RNcRJ57KEh9PjUfA diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-TooEarly.token b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-TooEarly.token new file mode 100644 index 00000000000..780cf108364 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-TooEarly.token @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiJ9.eyJuYmYiOjQ2ODM4OTI2NTUsImV4cCI6NDY4Mzg5MjY1NX0.MIaECJrmYjAByKNJoWHlP5ewg2xiW7GIxL8Vepp3ZIKf_jjM2OSMQlAWGmfD3Kf3bfesvSI7glw5qg_ZIv4FdIPaTvnmLRjWQkpk-QiLTJr_HM2wWeNbUJ1zciGWQlWAvabtQuyeGt1dsfQq53QLVNpvuioYdVg-gz_76uwDTxCKQU_99ksQhMMJsYJVDA_-uWGTzBANszcZykqwWFMaoXF4lkVPK4U68n18ISBB761wFusUCtyGWzwevX7wBAEJxcRy6ZVk3h7GyxZBsbRAd5fPn3dPMxNvL_CEp5jUYSAH-arAdDkvAph5Vk1yXof7FFRcffJpAy76HC66hR2JQA diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-TwoKeys.jwks b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-TwoKeys.jwks new file mode 100644 index 00000000000..16d3a00859f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-TwoKeys.jwks @@ -0,0 +1 @@ +{"keys":[{"p":"49neceJFs8R6n7WamRGy45F5Tv0YM-R2ODK3eSBUSLOSH2tAqjEVKOkLE5fiNA3ygqq15NcKRadB2pTVf-Yb5ZIBuKzko8bzYIkIqYhSh_FAdEEr0vHF5fq_yWSvc6swsOJGqvBEtuqtJY027u-G2gAQasCQdhyejer68zsTn8M","kty":"RSA","q":"tWR-ysspjZ73B6p2vVRVyHwP3KQWL5KEQcdgcmMOE_P_cPs98vZJfLhxobXVmvzuEWBpRSiqiuyKlQnpstKt94Cy77iO8m8ISfF3C9VyLWXi9HUGAJb99irWABFl3sNDff5K2ODQ8CmuXLYM25OwN3ikbrhEJozlXg_NJFSGD4E","d":"FkZHYZlw5KSoqQ1i2RA2kCUygSUOf1OqMt3uomtXuUmqKBm_bY7PCOhmwbvbn4xZYEeHuTR8Xix-0KpHe3NKyWrtRjkq1T_un49_1LLVUhJ0dL-9_x0xRquVjhl_XrsRXaGMEHs8G9pLTvXQ1uST585gxIfmCe0sxPZLvwoic-bXf64UZ9BGRV3lFexWJQqCZp2S21HfoU7wiz6kfLRNi-K4xiVNB1gswm_8o5lRuY7zB9bRARQ3TS2G4eW7p5sxT3CgsGiQD3_wPugU8iDplqAjgJ5ofNJXZezoj0t6JMB_qOpbrmAM1EnomIPebSLW7Ky9SugEd6KMdL5lW6AuAQ","e":"AQAB","use":"sig","kid":"one","qi":"wdkFu_tV2V1l_PWUUimG516Zvhqk2SWDw1F7uNDD-Lvrv_WNRIJVzuffZ8WYiPy8VvYQPJUrT2EXL8P0ocqwlaSTuXctrORcbjwgxDQDLsiZE0C23HYzgi0cofbScsJdhcBg7d07LAf7cdJWG0YVl1FkMCsxUlZ2wTwHfKWf-v4","dp":"uwnPxqC-IxG4r33-SIT02kZC1IqC4aY7PWq0nePiDEQMQWpjjNH50rlq9EyLzbtdRdIouo-jyQXB01K15-XXJJ60dwrGLYNVqfsTd0eGqD1scYJGHUWG9IDgCsxyEnuG3s0AwbW2UolWVSsU2xMZGb9PurIUZECeD1XDZwMp2s0","dq":"hra786AunB8TF35h8PpROzPoE9VJJMuLrc6Esm8eZXMwopf0yhxfN2FEAvUoTpLJu93-UH6DKenCgi16gnQ0_zt1qNNIVoRfg4rw_rjmsxCYHTVL3-RDeC8X_7TsEySxW0EgFTHh-nr6I6CQrAJjPM88T35KHtdFATZ7BCBB8AE","n":"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw"},{"p":"_CI5g5In9T4ZgakV1i62UU6yjorEr5t2URHfRYqxN7S4aKsQOzggcPoqa78xRj8PAPuf3P0ArPEAHdS6bFK7RLrFXdvyEmSNTJa1gcLCf2Zmep8bsrhrCvh6seZNvfrSMV0ULmk0B75Fs8mqE7nwcIbPtBYkinlSIw-sKRv62DM","kty":"RSA","q":"pqfexT3HBAagH-iydGsWbjG6CcYyvSQZdFtUu4LIOBCYVA0dvkN9s7uU1eoevHN_ksf-hfrF5AQH0a5P0dIJ2pp1bFa9uo9DJ7khU9sIBk9_o8nST2QLHwPQmGTW8vVlcSF7Vffvzm2fV3cQ3dfI5lvtkqfX_Z3WkF8UjFjADe8","d":"FzB5xChO8e89JisxSueY5j1RUBmatIAs_8Z3LUHOw16GlAhBhbSNl-7bXkbcUWLq9M1zTLCD91SSZXBohf9j1ebqWnbjMqQmdkxlQcVRoKcnMJ5YBabCTMBXghQnJetUMh6x6hXRnR1CSBNRdZPf-K2bnxL3xRNRSfY_7bjpb_q5pyUsK66ugSKwuEOUDNf1ttOZi4PBTsxWMDyXi_7fNFjl-B831uWNDVwdY4j68PVwGPT87zjZYjZRTZXB4ILUP11ztw4s3s_bU1Lj0PeZJsA5rmjU1iBzqCNdzgYxNlfV7M62VCkE1Wtd6M97jtysiT-5wQUMxNugoOTc9thc1Q","e":"AQAB","use":"sig","kid":"two","qi":"bnGriiVGVea9vSaN_48YYTEoKYM1kF7TrCRKERkMWdi4EHF7pZNWBv8arxaLUzElllvtGlVTNwkZlG0gOhXBoLYbcfqVikDklkBxtsuZEBKgvX7zFlDIBlNjh98lcZqDqz7Rqwr-tavxTCq2LNNlK6x-dYL61Agw_LOilYqbSfA","dp":"MmT4z-ZnnCn0WSkdlziw8iFjqP_tfhf5lwyWbsTg1PyHG0yNqvh1637k-bI2PA8ghZbFhhr_hpGI7210cXA7w-n8xtzOToTQhS1eS_hMfcBO3VVt6NPZeVDe3S3l_gHi_0DWZsxaPO336o51MwooF6WqYBlI5nCHTUC1rWXNRmc","dq":"dd_ybywc4boV87vQzQsZWGOPpG4tYR5xap1WtzHvj8gdFgYY7YQrGr8orIzlpIFE0Hroibcv1PEM3sAd8NhQ4--v8isAEz5VT3lgG0Gm0V_VdfG_8StfulYmakOYzUvIrlXyOIIfebCLrX-nzGFd1aFbzgktelLzejXmAMadQL0","n":"pCOHBsaoxlt9-qVE_INhrbkmxm7WqwEeqUBBIgHvm_JzXbmJ4iQzVF5tzAbRayxUmPbZ4E80R5HlIC2CQ7yyweTbIIWIw_TcQzXR4u3twEN1awP4s1n-00Eeurr-s9c_txZQQiDkyrCMYc9vlmsneFfubyoTvg9h_rckd8w34AyE8-wxgBRqUbm1x4ozcVmUJHkaPbQfbhIighl7osoQ4t_wXjAhTN_c9XttVjXlRwqVYPFNYUcC9GoaXWJRHjydHNFeBboOZY3E8ND6DbJ4nVtxydpUQSjTC-N-wQmhKmtYadd2hh2yywvtXpL5Q98XSphrrIHK-GWY0j8kimpunQ"}]} diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Unsigned.token b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Unsigned.token new file mode 100644 index 00000000000..f0b557652d3 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Unsigned.token @@ -0,0 +1 @@ +eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJuYmYiOjE1MzAzMDA4MzgsImV4cCI6MjE0NjAwMzE5OSwiaWF0IjoxNTMwMzAwODM4LCJ0eXAiOiJKV1QifQ. diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageReadScope.token b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageReadScope.token new file mode 100644 index 00000000000..0020772ffe3 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageReadScope.token @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6NDY4Mzg4MzIxMX0.cM7Eq9H20503czYVy1aVo8MqTQd8YsYGpv_lAV4PKr3y8NgvvosNjCSUs8rrGjQ0Sp3c4iXK6UVXq8pOJVeWXbSZa1IKAsIhiMIcg2xPFM6e71MVdX4bo255Yh8Nuh0p3xxP9isK_iAKNdMuVBOGfe9KATlmp2dOi0OpAjwSmxPJD1A7AC5f62YIe3Yx2gO6mbfANZJWQ7TxlUuCT_D5FEqg2FfYFqlFaluqWd_2X-esIsiDTxa1R9oF5XwgT6tsgvS7iYSiJw_uNKX0yU4eyLzYuIhnN_hVsr4jOZqPlsqCrkEohOGZg_Jir-7tLxZu0PqoH4ejC24FeDtC9xVa0w diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageReadScp.token b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageReadScp.token new file mode 100644 index 00000000000..3c2a2811527 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageReadScp.token @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJzY3AiOlsibWVzc2FnZTpyZWFkIl0sImV4cCI6NDY4Mzg5Nzc3Nn0.LtMVtIiRIwSyc3aX35Zl0JVwLTcQZAB3dyBOMHNaHCKUljwMrf20a_gT79LfhjDzE_fUVUmFiAO32W1vFnYpZSVaMDUgeIOIOpxfoe9shj_uYenAwIS-_UxqGVIJiJoXNZh_MK80ShNpvsQwamxWEEOAMBtpWNiVYNDMdfgho9n3o5_Z7Gjy8RLBo1tbDREbO9kTFwGIxm_EYpezmRCRq4w1DdS6UDW321hkwMxPnCMSWOvp-hRpmgY2yjzLgPJ6Aucmg9TJ8jloAP1DjJoF1gRR7NTAk8LOGkSjTzVYDYMbCF51YdpojhItSk80YzXiEsv1mTz4oMM49jXBmfXFMA diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageWriteScp.token b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageWriteScp.token new file mode 100644 index 00000000000..7cdb29ea59e --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageWriteScp.token @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJzY3AiOlsibWVzc2FnZTp3cml0ZSJdLCJleHAiOjQ2ODM4OTY0OTl9.mxAFzoNjjo-7E4D_XYVme69Y7F-J--q41x6lHDTSOxzVNfQqtJ-U-N4pn7St5jElm9y3mSUxTtmwCnukaVVZkeI8aJjUc8V8nxUAsiZIDvQWjr9uW4xUIcE6MiwC0A9rhY-3I87u6No-KBTxyT80zLnCjtS2XpTId-NSd3vcYmM7Vzn4-8KoR_m-7XrjvrO69HlRrH2uUAXGnr1sn6vLp7YruupqKrHqa0e9pIpN-VRzC8Bx2LQP9mVMlQy4b1hx5MdjOTV3HUSnWiT-93z4rTMOoHScKDwmzFYoS7e00F5hyd4jzbpHdpDKnjLdwPQYz_HCmQ5MV21-Q4Q1jparIg diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidNoScopes.token b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidNoScopes.token new file mode 100644 index 00000000000..7d4a3251d24 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidNoScopes.token @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJleHAiOjQ2ODM4Mjg2NzR9.LV_i9lzN_gAB2MUuZHJKm2tOfa3xWq_qfE2lx67eoYJZsY_20Ma98A3Hh2k0wnb_mNn6jfQhXbqvUy1llmQtsx3gMNhN2Axfe3UccSKYEb2Ow5OFlrMFYby1d_D4GfXKUFKq8jyMWVlrjk_XrfJyfzeo0MyZVzURSOXv1Ehbl5-xAS_N72jiAI7cIHlHGm93Hwdk8h7Tkkf_5t2dOMJM0mh0fOT9ou3J2_ngaNDfvlAmBLxHQiJ6JrFH5njqe4lSBTxJocDcgZwGVKd0WvV4W-jwA267tZjssDFmS3xZ9hoDO_M-EjlOiEPuWLd9nQCGJpBJ3z3WeC4qrKYghHTNLA diff --git a/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle b/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle new file mode 100644 index 00000000000..b05fa58afe4 --- /dev/null +++ b/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle @@ -0,0 +1,14 @@ +apply plugin: 'io.spring.convention.spring-module' + +dependencies { + compile project(':spring-security-core') + compile project(':spring-security-oauth2-core') + compile project(':spring-security-web') + compile springCoreDependency + + optional project(':spring-security-oauth2-jose') + + testCompile 'com.squareup.okhttp3:mockwebserver' + + provided 'javax.servlet:javax.servlet-api' +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenAuthenticationToken.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenAuthenticationToken.java new file mode 100644 index 00000000000..ed84ab6347e --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenAuthenticationToken.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource; + +import java.util.Collections; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter; +import org.springframework.util.Assert; + +/** + * An {@link Authentication} that contains a + * Bearer Token. + * + * Used by {@link BearerTokenAuthenticationFilter} to prepare an authentication attempt and supported + * by {@link JwtAuthenticationProvider}. + * + * @author Josh Cummings + * @since 5.1 + */ +public class BearerTokenAuthenticationToken extends AbstractAuthenticationToken { + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + + private String token; + + /** + * Create a {@code BearerTokenAuthenticationToken} using the provided parameter(s) + * + * @param token - the bearer token + */ + public BearerTokenAuthenticationToken(String token) { + super(Collections.emptyList()); + + Assert.hasText(token, "token cannot be empty"); + + this.token = token; + } + + /** + * Get the Bearer Token + * @return the token that proves the caller's authority to perform the {@link javax.servlet.http.HttpServletRequest} + */ + public String getToken() { + return this.token; + } + + /** + * {@inheritDoc} + */ + @Override + public Object getCredentials() { + return this.getToken(); + } + + /** + * {@inheritDoc} + */ + @Override + public Object getPrincipal() { + return this.getToken(); + } +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenError.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenError.java new file mode 100644 index 00000000000..7b3abbaeee0 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenError.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource; + +import org.springframework.http.HttpStatus; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.util.Assert; + +/** + * A representation of a Bearer Token Error. + * + * @author Vedran Pavic + * @author Josh Cummings + * @since 5.1 + * @see BearerTokenErrorCodes + * @see RFC 6750 Section 3: The WWW-Authenticate + * Response Header Field + */ +public final class BearerTokenError extends OAuth2Error { + + private final HttpStatus httpStatus; + + private final String scope; + + /** + * Create a {@code BearerTokenError} using the provided parameters + * + * @param errorCode the error code + * @param httpStatus the HTTP status + */ + public BearerTokenError(String errorCode, HttpStatus httpStatus, String description, String errorUri) { + this(errorCode, httpStatus, description, errorUri, null); + } + + /** + * Create a {@code BearerTokenError} using the provided parameters + * + * @param errorCode the error code + * @param httpStatus the HTTP status + * @param description the description + * @param errorUri the URI + * @param scope the scope + */ + public BearerTokenError(String errorCode, HttpStatus httpStatus, String description, String errorUri, String scope) { + super(errorCode, description, errorUri); + Assert.notNull(httpStatus, "httpStatus cannot be null"); + + Assert.isTrue(isDescriptionValid(description), + "description contains invalid ASCII characters, it must conform to RFC 6750"); + Assert.isTrue(isErrorCodeValid(errorCode), + "errorCode contains invalid ASCII characters, it must conform to RFC 6750"); + Assert.isTrue(isErrorUriValid(errorUri), + "errorUri contains invalid ASCII characters, it must conform to RFC 6750"); + Assert.isTrue(isScopeValid(scope), + "scope contains invalid ASCII characters, it must conform to RFC 6750"); + + this.httpStatus = httpStatus; + this.scope = scope; + } + + /** + * Return the HTTP status. + * @return the HTTP status + */ + public HttpStatus getHttpStatus() { + return this.httpStatus; + } + + /** + * Return the scope. + * @return the scope + */ + public String getScope() { + return this.scope; + } + + private static boolean isDescriptionValid(String description) { + return description == null || + description.chars().allMatch(c -> + withinTheRangeOf(c, 0x20, 0x21) || + withinTheRangeOf(c, 0x23, 0x5B) || + withinTheRangeOf(c, 0x5D, 0x7E)); + } + + private static boolean isErrorCodeValid(String errorCode) { + return errorCode.chars().allMatch(c -> + withinTheRangeOf(c, 0x20, 0x21) || + withinTheRangeOf(c, 0x23, 0x5B) || + withinTheRangeOf(c, 0x5D, 0x7E)); + } + + private static boolean isErrorUriValid(String errorUri) { + return errorUri == null || + errorUri.chars().allMatch(c -> + c == 0x21 || + withinTheRangeOf(c, 0x23, 0x5B) || + withinTheRangeOf(c, 0x5D, 0x7E)); + } + + private static boolean isScopeValid(String scope) { + return scope == null || + scope.chars().allMatch(c -> + withinTheRangeOf(c, 0x20, 0x21) || + withinTheRangeOf(c, 0x23, 0x5B) || + withinTheRangeOf(c, 0x5D, 0x7E)); + } + + private static boolean withinTheRangeOf(int c, int min, int max) { + return c >= min && c <= max; + } +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenErrorCodes.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenErrorCodes.java new file mode 100644 index 00000000000..06cb884868b --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenErrorCodes.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource; + +/** + * Standard error codes defined by the OAuth 2.0 Authorization Framework: Bearer Token Usage. + * + * @author Vedran Pavic + * @since 5.1 + * @see RFC 6750 Section 3.1: Error Codes + */ +public interface BearerTokenErrorCodes { + + /** + * {@code invalid_request} - The request is missing a required parameter, includes an unsupported parameter or + * parameter value, repeats the same parameter, uses more than one method for including an access token, or is + * otherwise malformed. + */ + String INVALID_REQUEST = "invalid_request"; + + /** + * {@code invalid_token} - The access token provided is expired, revoked, malformed, or invalid for other + * reasons. + */ + String INVALID_TOKEN = "invalid_token"; + + /** + * {@code insufficient_scope} - The request requires higher privileges than provided by the access token. + */ + String INSUFFICIENT_SCOPE = "insufficient_scope"; + +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java new file mode 100644 index 00000000000..896ac031d86 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.resource.authentication; + +import java.util.Collection; +import java.util.Map; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.security.oauth2.core.AbstractOAuth2Token; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.util.Assert; + +/** + * Base class for {@link AbstractAuthenticationToken} implementations + * that expose common attributes between different OAuth 2.0 Access Token Formats. + * + *

      + * For example, a {@link Jwt} could expose its {@link Jwt#getClaims() claims} via + * {@link #getTokenAttributes()} or an "Introspected" OAuth 2.0 Access Token + * could expose the attributes of the Introspection Response via {@link #getTokenAttributes()}. + * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AccessToken + * @see Jwt + * @see 2.2 Introspection Response + */ +public abstract class AbstractOAuth2TokenAuthenticationToken extends AbstractAuthenticationToken { + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + + private T token; + + /** + * Sub-class constructor. + */ + protected AbstractOAuth2TokenAuthenticationToken(T token) { + + this(token, null); + } + + /** + * Sub-class constructor. + * + * @param authorities the authorities assigned to the Access Token + */ + protected AbstractOAuth2TokenAuthenticationToken( + T token, + Collection authorities) { + + super(authorities); + + Assert.notNull(token, "token cannot be null"); + this.token = token; + } + + /** + * {@inheritDoc} + */ + @Override + public Object getPrincipal() { + return this.getToken(); + } + + /** + * {@inheritDoc} + */ + @Override + public Object getCredentials() { + return this.getToken(); + } + + /** + * Get the token bound to this {@link Authentication}. + */ + public final T getToken() { + return this.token; + } + + /** + * Returns the attributes of the access token. + * + * @return a {@code Map} of the attributes in the access token. + */ + public abstract Map getTokenAttributes(); +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.java new file mode 100644 index 00000000000..68c022ff7d3 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.java @@ -0,0 +1,151 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.resource.authentication; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * An {@link AuthenticationProvider} implementation of the {@link Jwt}-encoded + * Bearer Tokens + * for protecting OAuth 2.0 Resource Servers. + *

      + *

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

      + *

      + * Scopes are translated into {@link GrantedAuthority}s according to the following algorithm: + * + * 1. If there is a "scope" or "scp" attribute, then + * if a {@link String}, then split by spaces and return, or + * if a {@link Collection}, then simply return + * 2. Take the resulting {@link Collection} of {@link String}s and prepend the "SCOPE_" keyword, adding + * as {@link GrantedAuthority}s. + * + * @author Josh Cummings + * @author Joe Grandja + * @since 5.1 + * @see AuthenticationProvider + * @see JwtDecoder + */ +public final class JwtAuthenticationProvider implements AuthenticationProvider { + private final JwtDecoder jwtDecoder; + + private static final Collection WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES = + Arrays.asList("scope", "scp"); + + private static final String SCOPE_AUTHORITY_PREFIX = "SCOPE_"; + + public JwtAuthenticationProvider(JwtDecoder jwtDecoder) { + Assert.notNull(jwtDecoder, "jwtDecoder cannot be null"); + + this.jwtDecoder = jwtDecoder; + } + + /** + * Decode and validate the + * Bearer Token. + * + * @param authentication the authentication request object. + * + * @return A successful authentication + * @throws AuthenticationException if authentication failed for some reason + */ + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication; + + Jwt jwt; + try { + jwt = this.jwtDecoder.decode(bearer.getToken()); + } catch (JwtException failed) { + OAuth2Error invalidToken; + try { + invalidToken = invalidToken(failed.getMessage()); + } catch ( IllegalArgumentException malformed ) { + // some third-party library error messages are not suitable for RFC 6750's error message charset + invalidToken = invalidToken("An error occurred while attempting to decode the Jwt: Invalid token"); + } + throw new OAuth2AuthenticationException(invalidToken, failed); + } + + Collection authorities = + this.getScopes(jwt) + .stream() + .map(authority -> SCOPE_AUTHORITY_PREFIX + authority) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + JwtAuthenticationToken token = new JwtAuthenticationToken(jwt, authorities); + + token.setDetails(bearer.getDetails()); + + return token; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean supports(Class authentication) { + return BearerTokenAuthenticationToken.class.isAssignableFrom(authentication); + } + + private static OAuth2Error invalidToken(String message) { + return new BearerTokenError( + BearerTokenErrorCodes.INVALID_TOKEN, + HttpStatus.UNAUTHORIZED, + message, + "https://tools.ietf.org/html/rfc6750#section-3.1"); + } + + private static Collection getScopes(Jwt jwt) { + for ( String attributeName : WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES ) { + Object scopes = jwt.getClaims().get(attributeName); + if (scopes instanceof String) { + if (StringUtils.hasText((String) scopes)) { + return Arrays.asList(((String) scopes).split(" ")); + } else { + return Collections.emptyList(); + } + } else if (scopes instanceof Collection) { + return (Collection) scopes; + } + } + + return Collections.emptyList(); + } +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java new file mode 100644 index 00000000000..8358125b42a --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.resource.authentication; + +import java.util.Collection; +import java.util.Map; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.security.core.TransientAuthentication; +import org.springframework.security.oauth2.jwt.Jwt; + +/** + * An implementation of an {@link AbstractOAuth2TokenAuthenticationToken} + * representing a {@link Jwt} {@code Authentication}. + * + * @author Joe Grandja + * @since 5.1 + * @see AbstractOAuth2TokenAuthenticationToken + * @see Jwt + */ +@TransientAuthentication +public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationToken { + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + + /** + * Constructs a {@code JwtAuthenticationToken} using the provided parameters. + * + * @param jwt the JWT + */ + public JwtAuthenticationToken(Jwt jwt) { + super(jwt); + } + + /** + * Constructs a {@code JwtAuthenticationToken} using the provided parameters. + * + * @param jwt the JWT + * @param authorities the authorities assigned to the JWT + */ + public JwtAuthenticationToken(Jwt jwt, Collection authorities) { + super(jwt, authorities); + this.setAuthenticated(true); + } + + /** + * {@inheritDoc} + */ + @Override + public Map getTokenAttributes() { + return this.getToken().getClaims(); + } + + /** + * The {@link Jwt}'s subject, if any + */ + @Override + public String getName() { + return this.getToken().getSubject(); + } +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/package-info.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/package-info.java new file mode 100644 index 00000000000..0b0a403f624 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * OAuth 2.0 Resource Server {@code Authentication}s and supporting classes and interfaces. + */ +package org.springframework.security.oauth2.server.resource.authentication; diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/package-info.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/package-info.java new file mode 100644 index 00000000000..43e164f9786 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * OAuth 2.0 Resource Server core classes and interfaces providing support. + */ +package org.springframework.security.oauth2.server.resource; diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPoint.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPoint.java new file mode 100644 index 00000000000..ce6e4214d06 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPoint.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource.web; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.util.StringUtils; + +/** + * An {@link AuthenticationEntryPoint} implementation used to commence authentication of protected resource requests + * using {@link BearerTokenAuthenticationFilter}. + *

      + * Uses information provided by {@link BearerTokenError} to set HTTP response status code and populate + * {@code WWW-Authenticate} HTTP header. + * + * @author Vedran Pavic + * @since 5.1 + * @see BearerTokenError + * @see RFC 6750 Section 3: The WWW-Authenticate + * Response Header Field + */ +public final class BearerTokenAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private String realmName; + + /** + * Collect error details from the provided parameters and format according to + * RFC 6750, specifically {@code error}, {@code error_description}, {@code error_uri}, and {@scope scope}. + * + * @param request that resulted in an AuthenticationException + * @param response so that the user agent can begin authentication + * @param authException that caused the invocation + */ + @Override + public void commence( + HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) + throws IOException, ServletException { + + HttpStatus status = HttpStatus.UNAUTHORIZED; + + Map parameters = new LinkedHashMap<>(); + + if (this.realmName != null) { + parameters.put("realm", this.realmName); + } + + if (authException instanceof OAuth2AuthenticationException) { + OAuth2Error error = ((OAuth2AuthenticationException) authException).getError(); + + parameters.put("error", error.getErrorCode()); + + if (StringUtils.hasText(error.getDescription())) { + parameters.put("error_description", error.getDescription()); + } + + if (StringUtils.hasText(error.getUri())) { + parameters.put("error_uri", error.getUri()); + } + + if (error instanceof BearerTokenError) { + BearerTokenError bearerTokenError = (BearerTokenError) error; + + if (StringUtils.hasText(bearerTokenError.getScope())) { + parameters.put("scope", bearerTokenError.getScope()); + } + + status = ((BearerTokenError) error).getHttpStatus(); + } + } + + String wwwAuthenticate = computeWWWAuthenticateHeaderValue(parameters); + + response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate); + response.setStatus(status.value()); + } + + /** + * Set the default realm name to use in the bearer token error response + * + * @param realmName + */ + public final void setRealmName(String realmName) { + this.realmName = realmName; + } + + private static String computeWWWAuthenticateHeaderValue(Map parameters) { + String wwwAuthenticate = "Bearer"; + if (!parameters.isEmpty()) { + wwwAuthenticate += parameters.entrySet().stream() + .map(attribute -> attribute.getKey() + "=\"" + attribute.getValue() + "\"") + .collect(Collectors.joining(", ", " ", "")); + } + + return wwwAuthenticate; + } +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilter.java new file mode 100644 index 00000000000..5137d1d1a1b --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilter.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource.web; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.authentication.AuthenticationDetailsSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.Assert; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * Authenticates requests that contain an OAuth 2.0 + * Bearer Token. + * + * This filter should be wired with an {@link AuthenticationManager} that can authenticate a + * {@link BearerTokenAuthenticationToken}. + * + * @author Josh Cummings + * @author Vedran Pavic + * @author Joe Grandja + * @since 5.1 + * @see The OAuth 2.0 Authorization Framework: Bearer Token Usage + * @see JwtAuthenticationProvider + */ +public final class BearerTokenAuthenticationFilter extends OncePerRequestFilter { + private final AuthenticationManager authenticationManager; + + private final AuthenticationDetailsSource authenticationDetailsSource = + new WebAuthenticationDetailsSource(); + + private BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver(); + + private AuthenticationEntryPoint authenticationEntryPoint = new BearerTokenAuthenticationEntryPoint(); + + /** + * Construct a {@code BearerTokenAuthenticationFilter} using the provided parameter(s) + * @param authenticationManager + */ + public BearerTokenAuthenticationFilter(AuthenticationManager authenticationManager) { + Assert.notNull(authenticationManager, "authenticationManager cannot be null"); + this.authenticationManager = authenticationManager; + } + + /** + * Extract any Bearer Token from + * the request and attempt an authentication. + * + * @param request + * @param response + * @param filterChain + * @throws ServletException + * @throws IOException + */ + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + final boolean debug = this.logger.isDebugEnabled(); + + String token; + + try { + token = this.bearerTokenResolver.resolve(request); + } catch ( OAuth2AuthenticationException invalid ) { + this.authenticationEntryPoint.commence(request, response, invalid); + return; + } + + if (token == null) { + filterChain.doFilter(request, response); + return; + } + + BearerTokenAuthenticationToken authenticationRequest = new BearerTokenAuthenticationToken(token); + + authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); + + try { + Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest); + + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authenticationResult); + SecurityContextHolder.setContext(context); + + filterChain.doFilter(request, response); + } catch (AuthenticationException failed) { + SecurityContextHolder.clearContext(); + + if (debug) { + this.logger.debug("Authentication request for failed: " + failed); + } + + this.authenticationEntryPoint.commence(request, response, failed); + } + } + + /** + * Set the {@link BearerTokenResolver} to use. Defaults to {@link DefaultBearerTokenResolver}. + * @param bearerTokenResolver the {@code BearerTokenResolver} to use + */ + public final void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver) { + Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null"); + this.bearerTokenResolver = bearerTokenResolver; + } + + /** + * Set the {@link AuthenticationEntryPoint} to use. Defaults to {@link BearerTokenAuthenticationEntryPoint}. + * @param authenticationEntryPoint the {@code AuthenticationEntryPoint} to use + */ + public final void setAuthenticationEntryPoint(final AuthenticationEntryPoint authenticationEntryPoint) { + Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null"); + this.authenticationEntryPoint = authenticationEntryPoint; + } + +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenResolver.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenResolver.java new file mode 100644 index 00000000000..b73be65ee38 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenResolver.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource.web; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; + +/** + * A strategy for resolving Bearer Tokens + * from the {@link HttpServletRequest}. + * + * @author Vedran Pavic + * @since 5.1 + * @see RFC 6750 Section 2: Authenticated Requests + */ +public interface BearerTokenResolver { + + /** + * Resolve any Bearer Token + * value from the request. + * + * @param request the request + * @return the Bearer Token value or {@code null} if none found + * @throws OAuth2AuthenticationException if the found token is invalid + */ + String resolve(HttpServletRequest request); + +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolver.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolver.java new file mode 100644 index 00000000000..c4532fa0297 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolver.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource.web; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes; +import org.springframework.util.StringUtils; + +/** + * The default {@link BearerTokenResolver} implementation based on RFC 6750. + * + * @author Vedran Pavic + * @since 5.1 + * @see RFC 6750 Section 2: Authenticated Requests + */ +public final class DefaultBearerTokenResolver implements BearerTokenResolver { + + private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?[a-zA-Z0-9-._~+/]+)=*$"); + + private boolean allowFormEncodedBodyParameter = false; + + private boolean allowUriQueryParameter = false; + + /** + * {@inheritDoc} + */ + @Override + public String resolve(HttpServletRequest request) { + String authorizationHeaderToken = resolveFromAuthorizationHeader(request); + String parameterToken = resolveFromRequestParameters(request); + if (authorizationHeaderToken != null) { + if (parameterToken != null) { + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST, + HttpStatus.BAD_REQUEST, + "Found multiple bearer tokens in the request", + "https://tools.ietf.org/html/rfc6750#section-3.1"); + throw new OAuth2AuthenticationException(error); + } + return authorizationHeaderToken; + } + else if (parameterToken != null && isParameterTokenSupportedForRequest(request)) { + return parameterToken; + } + return null; + } + + /** + * Set if transport of access token using form-encoded body parameter is supported. Defaults to {@code false}. + * @param allowFormEncodedBodyParameter if the form-encoded body parameter is supported + */ + public void setAllowFormEncodedBodyParameter(boolean allowFormEncodedBodyParameter) { + this.allowFormEncodedBodyParameter = allowFormEncodedBodyParameter; + } + + /** + * Set if transport of access token using URI query parameter is supported. Defaults to {@code false}. + * + * The spec recommends against using this mechanism for sending bearer tokens, and even goes as far as + * stating that it was only included for completeness. + * + * @param allowUriQueryParameter if the URI query parameter is supported + */ + public void setAllowUriQueryParameter(boolean allowUriQueryParameter) { + this.allowUriQueryParameter = allowUriQueryParameter; + } + + private static String resolveFromAuthorizationHeader(HttpServletRequest request) { + String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StringUtils.hasText(authorization) && authorization.startsWith("Bearer")) { + Matcher matcher = authorizationPattern.matcher(authorization); + + if (!matcher.matches()) { + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_TOKEN, + HttpStatus.UNAUTHORIZED, + "Bearer token is malformed", + "https://tools.ietf.org/html/rfc6750#section-3.1"); + throw new OAuth2AuthenticationException(error); + } + + return matcher.group("token"); + } + return null; + } + + private static String resolveFromRequestParameters(HttpServletRequest request) { + String[] values = request.getParameterValues("access_token"); + if (values == null || values.length == 0) { + return null; + } + + if (values.length == 1) { + return values[0]; + } + + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST, + HttpStatus.BAD_REQUEST, + "Found multiple bearer tokens in the request", + "https://tools.ietf.org/html/rfc6750#section-3.1"); + throw new OAuth2AuthenticationException(error); + } + + private boolean isParameterTokenSupportedForRequest(HttpServletRequest request) { + return ((this.allowFormEncodedBodyParameter && "POST".equals(request.getMethod())) + || (this.allowUriQueryParameter && "GET".equals(request.getMethod()))); + } +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandler.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandler.java new file mode 100644 index 00000000000..fa3212d3734 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandler.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource.web.access; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes; +import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.util.StringUtils; + +/** + * Translates any {@link AccessDeniedException} into an HTTP response in accordance with + * RFC 6750 Section 3: The WWW-Authenticate. + * + * So long as the class can prove that the request has a valid OAuth 2.0 {@link Authentication}, then will return an + * insufficient scope error; otherwise, + * it will simply indicate the scheme (Bearer) and any configured realm. + * + * @author Josh Cummings + * @since 5.1 + */ +public final class BearerTokenAccessDeniedHandler implements AccessDeniedHandler { + + private static final Collection WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES = + Arrays.asList("scope", "scp"); + + private String realmName; + + /** + * Collect error details from the provided parameters and format according to + * RFC 6750, specifically {@code error}, {@code error_description}, {@code error_uri}, and {@scope scope}. + * + * @param request that resulted in an AccessDeniedException + * @param response so that the user agent can be advised of the failure + * @param accessDeniedException that caused the invocation + * + */ + @Override + public void handle( + HttpServletRequest request, HttpServletResponse response, + AccessDeniedException accessDeniedException) + throws IOException, ServletException { + + Map parameters = new LinkedHashMap<>(); + + if (this.realmName != null) { + parameters.put("realm", this.realmName); + } + + if (request.getUserPrincipal() instanceof AbstractOAuth2TokenAuthenticationToken) { + AbstractOAuth2TokenAuthenticationToken token = + (AbstractOAuth2TokenAuthenticationToken) request.getUserPrincipal(); + + String scope = getScope(token); + + parameters.put("error", BearerTokenErrorCodes.INSUFFICIENT_SCOPE); + parameters.put("error_description", + String.format("The token provided has insufficient scope [%s] for this request", scope)); + parameters.put("error_uri", "https://tools.ietf.org/html/rfc6750#section-3.1"); + + if (StringUtils.hasText(scope)) { + parameters.put("scope", scope); + } + } + + String wwwAuthenticate = computeWWWAuthenticateHeaderValue(parameters); + + response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate); + response.setStatus(HttpStatus.FORBIDDEN.value()); + } + + /** + * Set the default realm name to use in the bearer token error response + * + * @param realmName + */ + public final void setRealmName(String realmName) { + this.realmName = realmName; + } + + private static String getScope(AbstractOAuth2TokenAuthenticationToken token) { + + Map attributes = token.getTokenAttributes(); + + for (String attributeName : WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES) { + Object scopes = attributes.get(attributeName); + if (scopes instanceof String) { + return (String) scopes; + } else if (scopes instanceof Collection) { + Collection coll = (Collection) scopes; + return (String) coll.stream() + .map(String::valueOf) + .collect(Collectors.joining(" ")); + } + } + + return ""; + } + + private static String computeWWWAuthenticateHeaderValue(Map parameters) { + String wwwAuthenticate = "Bearer"; + if (!parameters.isEmpty()) { + wwwAuthenticate += parameters.entrySet().stream() + .map(attribute -> attribute.getKey() + "=\"" + attribute.getValue() + "\"") + .collect(Collectors.joining(", ", " ", "")); + } + + return wwwAuthenticate; + } +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/package-info.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/package-info.java new file mode 100644 index 00000000000..948c0ca58f6 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * OAuth 2.0 Resource Server access denial classes and interfaces. + */ +package org.springframework.security.oauth2.server.resource.web.access; diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/package-info.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/package-info.java new file mode 100644 index 00000000000..5391fba2f42 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * OAuth 2.0 Resource Server {@code Filter}'s and supporting classes and interfaces. + */ +package org.springframework.security.oauth2.server.resource.web; diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/BearerTokenAuthenticationTokenTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/BearerTokenAuthenticationTokenTests.java new file mode 100644 index 00000000000..cb5fcc426bc --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/BearerTokenAuthenticationTokenTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * Tests for {@link BearerTokenAuthenticationToken} + * + * @author Josh Cummings + */ +public class BearerTokenAuthenticationTokenTests { + @Test + public void constructorWhenTokenIsNullThenThrowsException() { + assertThatCode(() -> new BearerTokenAuthenticationToken(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("token cannot be empty"); + } + + @Test + public void constructorWhenTokenIsEmptyThenThrowsException() { + assertThatCode(() -> new BearerTokenAuthenticationToken("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("token cannot be empty"); + } + + @Test + public void constructorWhenTokenHasValueThenConstructedCorrectly() { + BearerTokenAuthenticationToken token = new BearerTokenAuthenticationToken("token"); + + assertThat(token.getToken()).isEqualTo("token"); + assertThat(token.getPrincipal()).isEqualTo("token"); + assertThat(token.getCredentials()).isEqualTo("token"); + } +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/BearerTokenErrorTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/BearerTokenErrorTests.java new file mode 100644 index 00000000000..6ac6b651fc8 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/BearerTokenErrorTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource; + +import org.junit.Test; + +import org.springframework.http.HttpStatus; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * Tests for {@link BearerTokenError} + * + * @author Vedran Pavic + * @author Josh Cummings + */ +public class BearerTokenErrorTests { + + private static final String TEST_ERROR_CODE = "test-code"; + + private static final HttpStatus TEST_HTTP_STATUS = HttpStatus.UNAUTHORIZED; + + private static final String TEST_DESCRIPTION = "test-description"; + + private static final String TEST_URI = "http://example.com"; + + private static final String TEST_SCOPE = "test-scope"; + + @Test + public void constructorWithErrorCodeWhenErrorCodeIsValidThenCreated() { + BearerTokenError error = new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, null, null); + + assertThat(error.getErrorCode()).isEqualTo(TEST_ERROR_CODE); + assertThat(error.getHttpStatus()).isEqualTo(TEST_HTTP_STATUS); + assertThat(error.getDescription()).isNull(); + assertThat(error.getUri()).isNull(); + assertThat(error.getScope()).isNull(); + } + + @Test + public void constructorWithErrorCodeAndHttpStatusWhenErrorCodeIsNullThenThrowIllegalArgumentException() { + assertThatCode(() -> new BearerTokenError(null, TEST_HTTP_STATUS, null, null)) + .isInstanceOf(IllegalArgumentException.class).hasMessage("errorCode cannot be empty"); + } + + @Test + public void constructorWithErrorCodeAndHttpStatusWhenErrorCodeIsEmptyThenThrowIllegalArgumentException() { + assertThatCode(() -> new BearerTokenError("", TEST_HTTP_STATUS, null, null)) + .isInstanceOf(IllegalArgumentException.class).hasMessage("errorCode cannot be empty"); + } + + @Test + public void constructorWithErrorCodeAndHttpStatusWhenHttpStatusIsNullThenThrowIllegalArgumentException() { + assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE, null, null, null)) + .isInstanceOf(IllegalArgumentException.class).hasMessage("httpStatus cannot be null"); + } + + @Test + public void constructorWithAllParametersWhenAllParametersAreValidThenCreated() { + BearerTokenError error = new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, TEST_DESCRIPTION, TEST_URI, + TEST_SCOPE); + + assertThat(error.getErrorCode()).isEqualTo(TEST_ERROR_CODE); + assertThat(error.getHttpStatus()).isEqualTo(TEST_HTTP_STATUS); + assertThat(error.getDescription()).isEqualTo(TEST_DESCRIPTION); + assertThat(error.getUri()).isEqualTo(TEST_URI); + assertThat(error.getScope()).isEqualTo(TEST_SCOPE); + } + + @Test + public void constructorWithAllParametersWhenErrorCodeIsNullThenThrowIllegalArgumentException() { + assertThatCode(() -> new BearerTokenError(null, TEST_HTTP_STATUS, TEST_DESCRIPTION, TEST_URI, TEST_SCOPE)) + .isInstanceOf(IllegalArgumentException.class).hasMessage("errorCode cannot be empty"); + } + + @Test + public void constructorWithAllParametersWhenErrorCodeIsEmptyThenThrowIllegalArgumentException() { + assertThatCode(() -> new BearerTokenError("", TEST_HTTP_STATUS, TEST_DESCRIPTION, TEST_URI, TEST_SCOPE)) + .isInstanceOf(IllegalArgumentException.class).hasMessage("errorCode cannot be empty"); + } + + @Test + public void constructorWithAllParametersWhenHttpStatusIsNullThenThrowIllegalArgumentException() { + assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE, null, TEST_DESCRIPTION, TEST_URI, TEST_SCOPE)) + .isInstanceOf(IllegalArgumentException.class).hasMessage("httpStatus cannot be null"); + } + + @Test + public void constructorWithAllParametersWhenErrorCodeIsInvalidThenThrowIllegalArgumentException() { + assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE + "\"", TEST_HTTP_STATUS, TEST_DESCRIPTION, + TEST_URI, TEST_SCOPE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("errorCode") + .hasMessageContaining("RFC 6750"); + } + + @Test + public void constructorWithAllParametersWhenDescriptionIsInvalidThenThrowIllegalArgumentException() { + assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, TEST_DESCRIPTION + "\"", + TEST_URI, TEST_SCOPE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("description") + .hasMessageContaining("RFC 6750"); + } + + @Test + public void constructorWithAllParametersWhenErrorUriIsInvalidThenThrowIllegalArgumentException() { + assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, TEST_DESCRIPTION, + TEST_URI + "\"", TEST_SCOPE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("errorUri") + .hasMessageContaining("RFC 6750"); + } + + @Test + public void constructorWithAllParametersWhenScopeIsInvalidThenThrowIllegalArgumentException() { + assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, TEST_DESCRIPTION, + TEST_URI, TEST_SCOPE + "\"")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("scope") + .hasMessageContaining("RFC 6750"); + } +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProviderTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProviderTests.java new file mode 100644 index 00000000000..51e898a5eae --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProviderTests.java @@ -0,0 +1,230 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.resource.authentication; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Predicate; + +import org.assertj.core.util.Maps; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link JwtAuthenticationProvider} + * + * @author Josh Cummings + */ +@RunWith(MockitoJUnitRunner.class) +public class JwtAuthenticationProviderTests { + @Mock + JwtDecoder jwtDecoder; + + JwtAuthenticationProvider provider; + + @Before + public void setup() { + this.provider = + new JwtAuthenticationProvider(this.jwtDecoder); + } + + @Test + public void authenticateWhenJwtDecodesThenAuthenticationHasAttributesContainedInJwt() { + BearerTokenAuthenticationToken token = this.authentication(); + + Map claims = new HashMap<>(); + claims.put("name", "value"); + Jwt jwt = this.jwt(claims); + + when(this.jwtDecoder.decode("token")).thenReturn(jwt); + + JwtAuthenticationToken authentication = + (JwtAuthenticationToken) this.provider.authenticate(token); + + assertThat(authentication.getTokenAttributes()).isEqualTo(claims); + } + + @Test + public void authenticateWhenJwtDecodeFailsThenRespondsWithInvalidToken() { + BearerTokenAuthenticationToken token = this.authentication(); + + when(this.jwtDecoder.decode("token")).thenThrow(JwtException.class); + + assertThatCode(() -> this.provider.authenticate(token)) + .matches(failed -> failed instanceof OAuth2AuthenticationException) + .matches(errorCode(BearerTokenErrorCodes.INVALID_TOKEN)); + } + + @Test + public void authenticateWhenTokenHasScopeAttributeThenTranslatedToAuthorities() { + BearerTokenAuthenticationToken token = this.authentication(); + + Jwt jwt = this.jwt(Maps.newHashMap("scope", "message:read message:write")); + + when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt); + + JwtAuthenticationToken authentication = + (JwtAuthenticationToken) this.provider.authenticate(token); + + Collection authorities = authentication.getAuthorities(); + + assertThat(authorities).containsExactly( + new SimpleGrantedAuthority("SCOPE_message:read"), + new SimpleGrantedAuthority("SCOPE_message:write")); + } + + @Test + public void authenticateWhenTokenHasEmptyScopeAttributeThenTranslatedToNoAuthorities() { + BearerTokenAuthenticationToken token = this.authentication(); + + Jwt jwt = this.jwt(Maps.newHashMap("scope", "")); + + when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt); + + JwtAuthenticationToken authentication = + (JwtAuthenticationToken) this.provider.authenticate(token); + + Collection authorities = authentication.getAuthorities(); + + assertThat(authorities).containsExactly(); + } + + @Test + public void authenticateWhenTokenHasScpAttributeThenTranslatedToAuthorities() { + BearerTokenAuthenticationToken token = this.authentication(); + + Jwt jwt = this.jwt(Maps.newHashMap("scp", Arrays.asList("message:read", "message:write"))); + + when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt); + + JwtAuthenticationToken authentication = + (JwtAuthenticationToken) this.provider.authenticate(token); + + Collection authorities = authentication.getAuthorities(); + + assertThat(authorities).containsExactly( + new SimpleGrantedAuthority("SCOPE_message:read"), + new SimpleGrantedAuthority("SCOPE_message:write")); + } + + @Test + public void authenticateWhenTokenHasEmptyScpAttributeThenTranslatedToNoAuthorities() { + BearerTokenAuthenticationToken token = this.authentication(); + + Jwt jwt = this.jwt(Maps.newHashMap("scp", Arrays.asList())); + + when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt); + + JwtAuthenticationToken authentication = + (JwtAuthenticationToken) this.provider.authenticate(token); + + Collection authorities = authentication.getAuthorities(); + + assertThat(authorities).containsExactly(); + } + + @Test + public void authenticateWhenTokenHasBothScopeAndScpThenScopeAttributeIsTranslatedToAuthorities() { + BearerTokenAuthenticationToken token = this.authentication(); + + Map claims = Maps.newHashMap("scp", Arrays.asList("message:read", "message:write")); + claims.put("scope", "missive:read missive:write"); + Jwt jwt = this.jwt(claims); + + when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt); + + JwtAuthenticationToken authentication = + (JwtAuthenticationToken) this.provider.authenticate(token); + + Collection authorities = authentication.getAuthorities(); + + assertThat(authorities).containsExactly( + new SimpleGrantedAuthority("SCOPE_missive:read"), + new SimpleGrantedAuthority("SCOPE_missive:write")); + } + + @Test + public void authenticateWhenTokenHasEmptyScopeAndNonEmptyScpThenScopeAttributeIsTranslatedToNoAuthorities() { + BearerTokenAuthenticationToken token = this.authentication(); + + Map claims = Maps.newHashMap("scp", Arrays.asList("message:read", "message:write")); + claims.put("scope", ""); + Jwt jwt = this.jwt(claims); + + when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt); + + JwtAuthenticationToken authentication = + (JwtAuthenticationToken) this.provider.authenticate(token); + + Collection authorities = authentication.getAuthorities(); + + assertThat(authorities).containsExactly(); + } + + @Test + public void authenticateWhenDecoderThrowsIncompatibleErrorMessageThenWrapsWithGenericOne() { + BearerTokenAuthenticationToken token = this.authentication(); + + when(this.jwtDecoder.decode(token.getToken())).thenThrow(new JwtException("with \"invalid\" chars")); + + assertThatCode(() -> this.provider.authenticate(token)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasFieldOrPropertyWithValue( + "error.description", + "An error occurred while attempting to decode the Jwt: Invalid token"); + } + + @Test + public void supportsWhenBearerTokenAuthenticationTokenThenReturnsTrue() { + assertThat(this.provider.supports(BearerTokenAuthenticationToken.class)).isTrue(); + } + + private BearerTokenAuthenticationToken authentication() { + return new BearerTokenAuthenticationToken("token"); + } + + private Jwt jwt(Map claims) { + Map headers = new HashMap<>(); + headers.put("alg", JwsAlgorithms.RS256); + + return new Jwt("token", Instant.now(), Instant.now().plusSeconds(3600), headers, claims); + } + + private Predicate errorCode(String errorCode) { + return failed -> + ((OAuth2AuthenticationException) failed).getError().getErrorCode() == errorCode; + } +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java new file mode 100644 index 00000000000..d75a8c1629d --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource.authentication; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.assertj.core.util.Maps; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; +import org.springframework.security.oauth2.jwt.Jwt; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * Tests for {@link JwtAuthenticationToken} + * + * @author Josh Cummings + */ +@RunWith(MockitoJUnitRunner.class) +public class JwtAuthenticationTokenTests { + + @Test + public void getNameWhenJwtHasSubjectThenReturnsSubject() { + Jwt jwt = this.jwt(Maps.newHashMap("sub", "Carl")); + + JwtAuthenticationToken token = new JwtAuthenticationToken(jwt); + + assertThat(token.getName()).isEqualTo("Carl"); + } + + @Test + public void getNameWhenJwtHasNoSubjectThenReturnsNull() { + Jwt jwt = this.jwt(Maps.newHashMap("claim", "value")); + + JwtAuthenticationToken token = new JwtAuthenticationToken(jwt); + + assertThat(token.getName()).isNull(); + } + + @Test + public void constructorWhenJwtIsNullThenThrowsException() { + assertThatCode(() -> new JwtAuthenticationToken(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("token cannot be null"); + } + + @Test + public void constructorWhenUsingCorrectParametersThenConstructedCorrectly() { + Collection authorities = Arrays.asList(new SimpleGrantedAuthority("test")); + Map claims = Maps.newHashMap("claim", "value"); + Jwt jwt = this.jwt(claims); + + JwtAuthenticationToken token = new JwtAuthenticationToken(jwt, authorities); + + assertThat(token.getAuthorities()).isEqualTo(authorities); + assertThat(token.getPrincipal()).isEqualTo(jwt); + assertThat(token.getCredentials()).isEqualTo(jwt); + assertThat(token.getToken()).isEqualTo(jwt); + assertThat(token.getTokenAttributes()).isEqualTo(claims); + assertThat(token.isAuthenticated()).isTrue(); + } + + @Test + public void constructorWhenUsingOnlyJwtThenConstructedCorrectly() { + Map claims = Maps.newHashMap("claim", "value"); + Jwt jwt = this.jwt(claims); + + JwtAuthenticationToken token = new JwtAuthenticationToken(jwt); + + assertThat(token.getAuthorities()).isEmpty(); + assertThat(token.getPrincipal()).isEqualTo(jwt); + assertThat(token.getCredentials()).isEqualTo(jwt); + assertThat(token.getToken()).isEqualTo(jwt); + assertThat(token.getTokenAttributes()).isEqualTo(claims); + assertThat(token.isAuthenticated()).isFalse(); + } + + private Jwt jwt(Map claims) { + Map headers = new HashMap<>(); + headers.put("alg", JwsAlgorithms.RS256); + + return new Jwt("token", Instant.now(), Instant.now().plusSeconds(3600), headers, claims); + } +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPointTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPointTests.java new file mode 100644 index 00000000000..0515ccf225c --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPointTests.java @@ -0,0 +1,202 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource.web; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * Tests for {@link BearerTokenAuthenticationEntryPoint}. + * + * @author Vedran Pavic + * @author Josh Cummings + */ +public class BearerTokenAuthenticationEntryPointTests { + + private BearerTokenAuthenticationEntryPoint authenticationEntryPoint; + + @Before + public void setUp() { + this.authenticationEntryPoint = new BearerTokenAuthenticationEntryPoint(); + } + + @Test + public void commenceWhenNoBearerTokenErrorThenStatus401AndAuthHeader() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + this.authenticationEntryPoint.commence(request, response, new BadCredentialsException("test")); + + assertThat(response.getStatus()).isEqualTo(401); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer"); + } + + @Test + public void commenceWhenNoBearerTokenErrorAndRealmSetThenStatus401AndAuthHeaderWithRealm() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + this.authenticationEntryPoint.setRealmName("test"); + this.authenticationEntryPoint.commence(request, response, new BadCredentialsException("test")); + + assertThat(response.getStatus()).isEqualTo(401); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer realm=\"test\""); + } + + @Test + public void commenceWhenInvalidRequestErrorThenStatus400AndHeaderWithError() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + BearerTokenError error = new BearerTokenError( + BearerTokenErrorCodes.INVALID_REQUEST, + HttpStatus.BAD_REQUEST, + null, + null); + + this.authenticationEntryPoint.commence(request, response, + new OAuth2AuthenticationException(error)); + + assertThat(response.getStatus()).isEqualTo(400); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"invalid_request\""); + } + + @Test + public void commenceWhenInvalidRequestErrorThenStatus400AndHeaderWithErrorDetails() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST, HttpStatus.BAD_REQUEST, + "The access token expired", null, null); + + this.authenticationEntryPoint.commence(request, response, + new OAuth2AuthenticationException(error)); + + assertThat(response.getStatus()).isEqualTo(400); + assertThat(response.getHeader("WWW-Authenticate")) + .isEqualTo("Bearer error=\"invalid_request\", error_description=\"The access token expired\""); + } + + @Test + public void commenceWhenInvalidRequestErrorThenStatus400AndHeaderWithErrorUri() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST, HttpStatus.BAD_REQUEST, + null, "http://example.com", null); + + this.authenticationEntryPoint.commence(request, response, + new OAuth2AuthenticationException(error)); + + assertThat(response.getStatus()).isEqualTo(400); + assertThat(response.getHeader("WWW-Authenticate")) + .isEqualTo("Bearer error=\"invalid_request\", error_uri=\"http://example.com\""); + } + + @Test + public void commenceWhenInvalidTokenErrorThenStatus401AndHeaderWithError() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_TOKEN, HttpStatus.UNAUTHORIZED, + null, null); + + this.authenticationEntryPoint.commence(request, response, + new OAuth2AuthenticationException(error)); + + assertThat(response.getStatus()).isEqualTo(401); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"invalid_token\""); + } + + @Test + public void commenceWhenInsufficientScopeErrorThenStatus403AndHeaderWithError() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INSUFFICIENT_SCOPE, HttpStatus.FORBIDDEN, + null, null); + + this.authenticationEntryPoint.commence(request, response, + new OAuth2AuthenticationException(error)); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\""); + } + + @Test + public void commenceWhenInsufficientScopeErrorThenStatus403AndHeaderWithErrorAndScope() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INSUFFICIENT_SCOPE, HttpStatus.FORBIDDEN, + null, null, "test.read test.write"); + + this.authenticationEntryPoint.commence(request, response, + new OAuth2AuthenticationException(error)); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")) + .isEqualTo("Bearer error=\"insufficient_scope\", scope=\"test.read test.write\""); + } + + @Test + public void commenceWhenInsufficientScopeAndRealmSetThenStatus403AndHeaderWithErrorAndAllDetails() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INSUFFICIENT_SCOPE, HttpStatus.FORBIDDEN, + "Insufficient scope", "http://example.com", "test.read test.write"); + + this.authenticationEntryPoint.setRealmName("test"); + this.authenticationEntryPoint.commence(request, response, + new OAuth2AuthenticationException(error)); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo( + "Bearer realm=\"test\", error=\"insufficient_scope\", error_description=\"Insufficient scope\", " + + "error_uri=\"http://example.com\", scope=\"test.read test.write\""); + } + + @Test + public void setRealmNameWhenNullRealmNameThenNoExceptionThrown() { + assertThatCode(() -> this.authenticationEntryPoint.setRealmName(null)) + .doesNotThrowAnyException(); + } + +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilterTests.java new file mode 100644 index 00000000000..56bc183b4c3 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilterTests.java @@ -0,0 +1,173 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.resource.web; + +import java.io.IOException; +import javax.servlet.ServletException; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes; +import org.springframework.security.web.AuthenticationEntryPoint; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +/** + * Tests {@link BearerTokenAuthenticationFilterTests} + * + * @author Josh Cummings + */ +@RunWith(MockitoJUnitRunner.class) +public class BearerTokenAuthenticationFilterTests { + @Mock + AuthenticationEntryPoint authenticationEntryPoint; + + @Mock + AuthenticationManager authenticationManager; + + @Mock + BearerTokenResolver bearerTokenResolver; + + MockHttpServletRequest request; + + MockHttpServletResponse response; + + MockFilterChain filterChain; + + @InjectMocks + BearerTokenAuthenticationFilter filter; + + @Before + public void httpMocks() { + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + this.filterChain = new MockFilterChain(); + } + + @Before + public void setterMocks() { + this.filter.setAuthenticationEntryPoint(this.authenticationEntryPoint); + this.filter.setBearerTokenResolver(this.bearerTokenResolver); + } + + @Test + public void doFilterWhenBearerTokenPresentThenAuthenticates() throws ServletException, IOException { + when(this.bearerTokenResolver.resolve(this.request)).thenReturn("token"); + + this.filter.doFilter(this.request, this.response, this.filterChain); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(BearerTokenAuthenticationToken.class); + + verify(this.authenticationManager).authenticate(captor.capture()); + + assertThat(captor.getValue().getPrincipal()).isEqualTo("token"); + } + + @Test + public void doFilterWhenNoBearerTokenPresentThenDoesNotAuthenticate() + throws ServletException, IOException { + + when(this.bearerTokenResolver.resolve(this.request)).thenReturn(null); + + dontAuthenticate(); + } + + @Test + public void doFilterWhenMalformedBearerTokenThenPropagatesError() throws ServletException, IOException { + BearerTokenError error = new BearerTokenError( + BearerTokenErrorCodes.INVALID_REQUEST, + HttpStatus.BAD_REQUEST, + "description", + "uri"); + + OAuth2AuthenticationException exception = new OAuth2AuthenticationException(error); + + when(this.bearerTokenResolver.resolve(this.request)).thenThrow(exception); + + dontAuthenticate(); + + verify(this.authenticationEntryPoint).commence(this.request, this.response, exception); + } + + @Test + public void doFilterWhenAuthenticationFailsThenPropagatesError() throws ServletException, IOException { + BearerTokenError error = new BearerTokenError( + BearerTokenErrorCodes.INVALID_TOKEN, + HttpStatus.UNAUTHORIZED, + "description", + "uri" + ); + + OAuth2AuthenticationException exception = new OAuth2AuthenticationException(error); + + when(this.bearerTokenResolver.resolve(this.request)).thenReturn("token"); + when(this.authenticationManager.authenticate(any(BearerTokenAuthenticationToken.class))) + .thenThrow(exception); + + this.filter.doFilter(this.request, this.response, this.filterChain); + + verify(this.authenticationEntryPoint).commence(this.request, this.response, exception); + } + + @Test + public void setAuthenticationEntryPointWhenNullThenThrowsException() { + assertThatCode(() -> this.filter.setAuthenticationEntryPoint(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("authenticationEntryPoint cannot be null"); + } + + @Test + public void setBearerTokenResolverWhenNullThenThrowsException() { + assertThatCode(() -> this.filter.setBearerTokenResolver(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("bearerTokenResolver cannot be null"); + } + + @Test + public void constructorWhenNullAuthenticationManagerThenThrowsException() { + assertThatCode(() -> new BearerTokenAuthenticationFilter(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("authenticationManager cannot be null"); + } + + private void dontAuthenticate() + throws ServletException, IOException { + + this.filter.doFilter(this.request, this.response, this.filterChain); + + verifyNoMoreInteractions(this.authenticationManager); + } +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolverTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolverTests.java new file mode 100644 index 00000000000..32518f4d1a6 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolverTests.java @@ -0,0 +1,160 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource.web; + +import java.util.Base64; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * Tests for {@link DefaultBearerTokenResolver}. + * + * @author Vedran Pavic + */ +public class DefaultBearerTokenResolverTests { + + private static final String TEST_TOKEN = "test-token"; + + private DefaultBearerTokenResolver resolver; + + @Before + public void setUp() { + this.resolver = new DefaultBearerTokenResolver(); + } + + @Test + public void resolveWhenValidHeaderIsPresentThenTokenIsResolved() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer " + TEST_TOKEN); + + assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN); + } + + @Test + public void resolveWhenNoHeaderIsPresentThenTokenIsNotResolved() { + MockHttpServletRequest request = new MockHttpServletRequest(); + + assertThat(this.resolver.resolve(request)).isNull(); + } + + @Test + public void resolveWhenHeaderWithWrongSchemeIsPresentThenTokenIsNotResolved() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString("test:test".getBytes())); + + assertThat(this.resolver.resolve(request)).isNull(); + } + + @Test + public void resolveWhenHeaderWithMissingTokenIsPresentThenAuthenticationExceptionIsThrown() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer "); + + assertThatCode(() -> this.resolver.resolve(request)).isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining(("Bearer token is malformed")); + } + + @Test + public void resolveWhenHeaderWithInvalidCharactersIsPresentThenAuthenticationExceptionIsThrown() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer an\"invalid\"token"); + + assertThatCode(() -> this.resolver.resolve(request)).isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining(("Bearer token is malformed")); + } + + @Test + public void resolveWhenValidHeaderIsPresentTogetherWithFormParameterThenAuthenticationExceptionIsThrown() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer " + TEST_TOKEN); + request.setMethod("POST"); + request.setContentType("application/x-www-form-urlencoded"); + request.addParameter("access_token", TEST_TOKEN); + + assertThatCode(() -> this.resolver.resolve(request)).isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("Found multiple bearer tokens in the request"); + } + + @Test + public void resolveWhenValidHeaderIsPresentTogetherWithQueryParameterThenAuthenticationExceptionIsThrown() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer " + TEST_TOKEN); + request.setMethod("GET"); + request.addParameter("access_token", TEST_TOKEN); + + assertThatCode(() -> this.resolver.resolve(request)).isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("Found multiple bearer tokens in the request"); + } + + @Test + public void resolveWhenRequestContainsTwoAccessTokenParametersThenAuthenticationExceptionIsThrown() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("access_token", "token1", "token2"); + + assertThatCode(() -> this.resolver.resolve(request)).isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("Found multiple bearer tokens in the request"); + } + + @Test + public void resolveWhenFormParameterIsPresentAndSupportedThenTokenIsResolved() { + this.resolver.setAllowFormEncodedBodyParameter(true); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.setContentType("application/x-www-form-urlencoded"); + request.addParameter("access_token", TEST_TOKEN); + + assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN); + } + + @Test + public void resolveWhenFormParameterIsPresentAndNotSupportedThenTokenIsNotResolved() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.setContentType("application/x-www-form-urlencoded"); + request.addParameter("access_token", TEST_TOKEN); + + assertThat(this.resolver.resolve(request)).isNull(); + } + + @Test + public void resolveWhenQueryParameterIsPresentAndSupportedThenTokenIsResolved() { + this.resolver.setAllowUriQueryParameter(true); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + request.addParameter("access_token", TEST_TOKEN); + + assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN); + } + + @Test + public void resolveWhenQueryParameterIsPresentAndNotSupportedThenTokenIsNotResolved() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + request.addParameter("access_token", TEST_TOKEN); + + assertThat(this.resolver.resolve(request)).isNull(); + } +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandlerTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandlerTests.java new file mode 100644 index 00000000000..fd9b598a398 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandlerTests.java @@ -0,0 +1,250 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource.web.access; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +import org.assertj.core.util.Maps; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AbstractOAuth2Token; +import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * Tests for {@link BearerTokenAccessDeniedHandlerTests} + * + * @author Josh Cummings + */ +public class BearerTokenAccessDeniedHandlerTests { + private BearerTokenAccessDeniedHandler accessDeniedHandler; + + @Before + public void setUp() { + this.accessDeniedHandler = new BearerTokenAccessDeniedHandler(); + } + + @Test + public void handleWhenNotOAuth2AuthenticatedThenStatus403() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + Authentication authentication = new TestingAuthenticationToken("user", "pass"); + request.setUserPrincipal(authentication); + + this.accessDeniedHandler.handle(request, response, null); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer"); + } + + @Test + public void handleWhenNotOAuth2AuthenticatedAndRealmSetThenStatus403AndAuthHeaderWithRealm() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + Authentication authentication = new TestingAuthenticationToken("user", "pass"); + request.setUserPrincipal(authentication); + + this.accessDeniedHandler.setRealmName("test"); + this.accessDeniedHandler.handle(request, response, null); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer realm=\"test\""); + } + + @Test + public void handleWhenTokenHasNoScopesThenInsufficientScopeError() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + Authentication token = new TestingOAuth2TokenAuthenticationToken(Collections.emptyMap()); + request.setUserPrincipal(token); + + this.accessDeniedHandler.handle(request, response, null); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " + + "error_description=\"The token provided has insufficient scope [] for this request\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\""); + } + + + @Test + public void handleWhenTokenHasScopeAttributeThenInsufficientScopeErrorWithScopes() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + Map attributes = Maps.newHashMap("scope", "message:read message:write"); + Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes); + request.setUserPrincipal(token); + + this.accessDeniedHandler.handle(request, response, null); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " + + "error_description=\"The token provided has insufficient scope [message:read message:write] for this request\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\", " + + "scope=\"message:read message:write\""); + } + + @Test + public void handleWhenTokenHasEmptyScopeAttributeThenInsufficientScopeError() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + Map attributes = Maps.newHashMap("scope", ""); + Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes); + request.setUserPrincipal(token); + + this.accessDeniedHandler.handle(request, response, null); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " + + "error_description=\"The token provided has insufficient scope [] for this request\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\""); + } + + @Test + public void handleWhenTokenHasScpAttributeThenInsufficientScopeErrorWithScopes() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + Map attributes = Maps.newHashMap("scp", Arrays.asList("message:read", "message:write")); + Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes); + request.setUserPrincipal(token); + + this.accessDeniedHandler.handle(request, response, null); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " + + "error_description=\"The token provided has insufficient scope [message:read message:write] for this request\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\", " + + "scope=\"message:read message:write\""); + } + + @Test + public void handleWhenTokenHasEmptyScpAttributeThenInsufficientScopeError() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + Map attributes = Maps.newHashMap("scp", Collections.emptyList()); + Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes); + request.setUserPrincipal(token); + + this.accessDeniedHandler.handle(request, response, null); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " + + "error_description=\"The token provided has insufficient scope [] for this request\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\""); + } + + @Test + public void handleWhenTokenHasBothScopeAndScpAttributesTheInsufficientErrorBasedOnScopeAttribute() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + Map attributes = Maps.newHashMap("scp", Arrays.asList("message:read", "message:write")); + Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes); + request.setUserPrincipal(token); + attributes.put("scope", "missive:read missive:write"); + + this.accessDeniedHandler.handle(request, response, null); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " + + "error_description=\"The token provided has insufficient scope [missive:read missive:write] for this request\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\", " + + "scope=\"missive:read missive:write\""); + } + + @Test + public void handleWhenTokenHasScopeAttributeAndRealmIsSetThenInsufficientScopeErrorWithScopesAndRealm() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + Map attributes = Maps.newHashMap("scope", "message:read message:write"); + Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes); + request.setUserPrincipal(token); + + this.accessDeniedHandler.setRealmName("test"); + this.accessDeniedHandler.handle(request, response, null); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer realm=\"test\", " + + "error=\"insufficient_scope\", " + + "error_description=\"The token provided has insufficient scope [message:read message:write] for this request\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\", " + + "scope=\"message:read message:write\""); + } + + @Test + public void setRealmNameWhenNullRealmNameThenNoExceptionThrown() { + assertThatCode(() -> this.accessDeniedHandler.setRealmName(null)) + .doesNotThrowAnyException(); + } + + static class TestingOAuth2TokenAuthenticationToken + extends AbstractOAuth2TokenAuthenticationToken { + + private Map attributes; + + protected TestingOAuth2TokenAuthenticationToken(Map attributes) { + super(new TestingOAuth2Token("token")); + this.attributes = attributes; + } + + @Override + public Map getTokenAttributes() { + return this.attributes; + } + + static class TestingOAuth2Token extends AbstractOAuth2Token { + public TestingOAuth2Token(String tokenValue) { + super(tokenValue); + } + } + } +} diff --git a/samples/boot/oauth2resourceserver/README.adoc b/samples/boot/oauth2resourceserver/README.adoc new file mode 100644 index 00000000000..d3ba0b8238c --- /dev/null +++ b/samples/boot/oauth2resourceserver/README.adoc @@ -0,0 +1,104 @@ += OAuth 2.0 Resource Server Sample + +This sample demonstrates integrating Resource Server with a mock Authorization Server, though it can be modified to integrate +with your favorite Authorization Server. + +With it, you can run the integration tests or run the application as a stand-alone service to explore how you can +secure your own service with OAuth 2.0 Bearer Tokens using Spring Security. + +== 1. Running the tests + +To run the tests, do: + +```bash +./gradlew integrationTest +``` + +Or import the project into your IDE and run `OAuth2ResourceServerApplicationTests` from there. + +=== What is it doing? + +By default, the tests are pointing at a mock Authorization Server instance. + +The tests are configured with a set of hard-coded tokens originally obtained from the mock Authorization Server, +and each makes a query to the Resource Server with their corresponding token. + +The Resource Server subsquently verifies with the Authorization Server and authorizes the request, returning the phrase + +```bash +Hello, subject! +``` + +where "subject" is the value of the `sub` field in the JWT returned by the Authorization Server. + +== 2. Running the app + +To run as a stand-alone application, do: + +```bash +./gradlew bootRun +``` + +Or import the project into your IDE and run `OAuth2ResourceServerApplication` from there. + +Once it is up, you can use the following token: + +```bash +export TOKEN=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0IiwiZXhwIjo0NjgzODA1MTI4fQ.ULEPdHG-MK5GlrTQMhgqcyug2brTIZaJIrahUeq9zaiwUSdW83fJ7W1IDd2Z3n4a25JY2uhEcoV95lMfccHR6y_2DLrNvfta22SumY9PEDF2pido54LXG6edIGgarnUbJdR4rpRe_5oRGVa8gDx8FnuZsNv6StSZHAzw5OsuevSTJ1UbJm4UfX3wiahFOQ2OI6G-r5TB2rQNdiPHuNyzG5yznUqRIZ7-GCoMqHMaC-1epKxiX8gYXRROuUYTtcMNa86wh7OVDmvwVmFioRcR58UWBRoO1XQexTtOQq_t8KYsrPZhb9gkyW8x2bAQF-d0J0EJY8JslaH6n4RBaZISww +``` + +And then make this request: + +```bash +curl -H "Authorization: Bearer $TOKEN" localhost:8080 +``` + +Which will respond with the phrase: + +```bash +Hello, subject! +``` + +where `subject` is the value of the `sub` field in the JWT returned by the Authorization Server. + +Or this: + +```bash +export TOKEN=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOiJtZXNzYWdlOnJlYWQiLCJleHAiOjQ2ODM4MDUxNDF9.h-j6FKRFdnTdmAueTZCdep45e6DPwqM68ZQ8doIJ1exi9YxAlbWzOwId6Bd0L5YmCmp63gGQgsBUBLzwnZQ8kLUgUOBEC3UzSWGRqMskCY9_k9pX0iomX6IfF3N0PaYs0WPC4hO1s8wfZQ-6hKQ4KigFi13G9LMLdH58PRMK0pKEvs3gCbHJuEPw-K5ORlpdnleUTQIwINafU57cmK3KocTeknPAM_L716sCuSYGvDl6xUTXO7oPdrXhS_EhxLP6KxrpI1uD4Ea_5OWTh7S0Wx5LLDfU6wBG1DowN20d374zepOIEkR-Jnmr_QlR44vmRqS5ncrF-1R0EGcPX49U6A + +curl -H "Authorization: Bearer $TOKEN" localhost:8080/message +``` + +Will respond with: + +```bash +secret message +``` + +== 2. Testing against other Authorization Servers + +_In order to use this sample, your Authorization Server must support JWTs that either use the "scope" or "scp" attribute._ + +_Additionally, remember that if your authorization server is running locally on port 8080, you'll need to change the sample's port in the `application.yml` by adding something like `server.port: 8082`._ + +To change the sample to point at your Authorization Server, simply find this property in the `application.yml`: + +```yaml +sample.jwk-set-uri: mock://localhost:8081/.well-known/jwks.json +``` + +And change the property to your Authorization Server's JWK set endpoint: + +```yaml +sample.jwk-set-uri: https://dev-123456.oktapreview.com/oauth2/default/v1/keys +``` + +And then you can run the app the same as before: + +```bash +./gradlew bootRun +``` + +Make sure to obtain valid tokens from your Authorization Server in order to play with the sample Resource Server. +To use the `/` endpoint, any valid token from your Authorization Server will do. +To use the `/message` endpoint, the token should have the `message:read` scope. diff --git a/samples/boot/oauth2resourceserver/spring-security-samples-boot-oauth2resourceserver.gradle b/samples/boot/oauth2resourceserver/spring-security-samples-boot-oauth2resourceserver.gradle new file mode 100644 index 00000000000..2135bb0af66 --- /dev/null +++ b/samples/boot/oauth2resourceserver/spring-security-samples-boot-oauth2resourceserver.gradle @@ -0,0 +1,13 @@ +apply plugin: 'io.spring.convention.spring-sample-boot' + +dependencies { + compile project(':spring-security-config') + compile project(':spring-security-oauth2-jose') + compile project(':spring-security-oauth2-resource-server') + + compile 'org.springframework.boot:spring-boot-starter-web' + compile 'com.squareup.okhttp3:mockwebserver' + + testCompile project(':spring-security-test') + testCompile 'org.springframework.boot:spring-boot-starter-test' +} diff --git a/samples/boot/oauth2resourceserver/src/integration-test/java/sample/OAuth2ResourceServerApplicationITests.java b/samples/boot/oauth2resourceserver/src/integration-test/java/sample/OAuth2ResourceServerApplicationITests.java new file mode 100644 index 00000000000..19788620676 --- /dev/null +++ b/samples/boot/oauth2resourceserver/src/integration-test/java/sample/OAuth2ResourceServerApplicationITests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.RequestPostProcessor; + +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Integration tests for {@link OAuth2ResourceServerApplication} + * + * @author Josh Cummings + */ +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +public class OAuth2ResourceServerApplicationITests { + + String noScopesToken = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0IiwiZXhwIjo0NjgzODA1MTI4fQ.ULEPdHG-MK5GlrTQMhgqcyug2brTIZaJIrahUeq9zaiwUSdW83fJ7W1IDd2Z3n4a25JY2uhEcoV95lMfccHR6y_2DLrNvfta22SumY9PEDF2pido54LXG6edIGgarnUbJdR4rpRe_5oRGVa8gDx8FnuZsNv6StSZHAzw5OsuevSTJ1UbJm4UfX3wiahFOQ2OI6G-r5TB2rQNdiPHuNyzG5yznUqRIZ7-GCoMqHMaC-1epKxiX8gYXRROuUYTtcMNa86wh7OVDmvwVmFioRcR58UWBRoO1XQexTtOQq_t8KYsrPZhb9gkyW8x2bAQF-d0J0EJY8JslaH6n4RBaZISww"; + String messageReadToken = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOiJtZXNzYWdlOnJlYWQiLCJleHAiOjQ2ODM4MDUxNDF9.h-j6FKRFdnTdmAueTZCdep45e6DPwqM68ZQ8doIJ1exi9YxAlbWzOwId6Bd0L5YmCmp63gGQgsBUBLzwnZQ8kLUgUOBEC3UzSWGRqMskCY9_k9pX0iomX6IfF3N0PaYs0WPC4hO1s8wfZQ-6hKQ4KigFi13G9LMLdH58PRMK0pKEvs3gCbHJuEPw-K5ORlpdnleUTQIwINafU57cmK3KocTeknPAM_L716sCuSYGvDl6xUTXO7oPdrXhS_EhxLP6KxrpI1uD4Ea_5OWTh7S0Wx5LLDfU6wBG1DowN20d374zepOIEkR-Jnmr_QlR44vmRqS5ncrF-1R0EGcPX49U6A"; + + @Autowired + MockMvc mvc; + + @Test + public void performWhenValidBearerTokenThenAllows() + throws Exception { + + this.mvc.perform(get("/").with(bearerToken(this.noScopesToken))) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("Hello, subject!"))); + } + + // -- tests with scopes + + @Test + public void performWhenValidBearerTokenThenScopedRequestsAlsoWork() + throws Exception { + + this.mvc.perform(get("/message").with(bearerToken(this.messageReadToken))) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("secret message"))); + } + + @Test + public void performWhenInsufficientlyScopedBearerTokenThenDeniesScopedMethodAccess() + throws Exception { + + this.mvc.perform(get("/message").with(bearerToken(this.noScopesToken))) + .andExpect(status().isForbidden()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, + containsString("Bearer error=\"insufficient_scope\""))); + } + + private static class BearerTokenRequestPostProcessor implements RequestPostProcessor { + private String token; + + public BearerTokenRequestPostProcessor(String token) { + this.token = token; + } + + @Override + public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { + request.addHeader("Authorization", "Bearer " + this.token); + return request; + } + } + + private static BearerTokenRequestPostProcessor bearerToken(String token) { + return new BearerTokenRequestPostProcessor(token); + } +} diff --git a/samples/boot/oauth2resourceserver/src/integration-test/resources/application-test.yml b/samples/boot/oauth2resourceserver/src/integration-test/resources/application-test.yml new file mode 100644 index 00000000000..04878b289c8 --- /dev/null +++ b/samples/boot/oauth2resourceserver/src/integration-test/resources/application-test.yml @@ -0,0 +1 @@ +sample.jwk-set-uri: mock://localhost:0/.well-known/jwks.json diff --git a/samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerApplication.java b/samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerApplication.java new file mode 100644 index 00000000000..f9cc432b1b3 --- /dev/null +++ b/samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerApplication.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Josh Cummings + */ +@SpringBootApplication +public class OAuth2ResourceServerApplication { + + public static void main(String[] args) { + SpringApplication.run(OAuth2ResourceServerApplication.class, args); + } +} diff --git a/samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerController.java b/samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerController.java new file mode 100644 index 00000000000..9cb92c21d6a --- /dev/null +++ b/samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerController.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Josh Cummings + */ +@RestController +public class OAuth2ResourceServerController { + + @GetMapping("/") + public String index(@AuthenticationPrincipal Jwt jwt) { + return String.format("Hello, %s!", jwt.getSubject()); + } + + @GetMapping("/message") + public String message() { + return "secret message"; + } +} diff --git a/samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java b/samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java new file mode 100644 index 00000000000..91a44c72239 --- /dev/null +++ b/samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +/** + * @author Josh Cummings + */ +@EnableWebSecurity +public class OAuth2ResourceServerSecurityConfiguration extends WebSecurityConfigurerAdapter { + @Value("${sample.jwk-set-uri}") + String jwkSetUri; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .antMatchers("/message/**").access("hasAuthority('SCOPE_message:read')") + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt() + .jwkSetUri(this.jwkSetUri); + // @formatter:on + } +} diff --git a/samples/boot/oauth2resourceserver/src/main/java/sample/provider/MockProvider.java b/samples/boot/oauth2resourceserver/src/main/java/sample/provider/MockProvider.java new file mode 100644 index 00000000000..b3e16318ece --- /dev/null +++ b/samples/boot/oauth2resourceserver/src/main/java/sample/provider/MockProvider.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.provider; + +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.PreDestroy; + +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +/** + * This is a miminal mock server that serves as a placeholder for a real Authorization Server (AS). + * + * For the sample to work, the AS used must support a JWK endpoint. + * + * For the integration tests to work, the AS used must be able to issue a token + * with the following characteristics: + * + * - The token has the "message:read" scope + * - The token has a "sub" of "subject" + * - The token is signed by a RS256 private key whose public key counterpart is served from the JWK endpoint of the AS. + * + * There is also a test that verifies insufficient scope. In that case, the token should have the following characteristics: + * + * - The token is missing the "message:read" scope + * - The token is signed by a RS256 private key whose public key counterpart is served from the JWK endpoint of the AS. + * + * @author Josh Cummings + */ +public class MockProvider implements EnvironmentPostProcessor { + private MockWebServer server = new MockWebServer(); + + private static final MockResponse JWKS_RESPONSE = response( + "{\"keys\":[{\"p\":\"2p-ViY7DE9ZrdWQb544m0Jp7Cv03YCSljqfim9pD4ALhObX0OrAznOiowTjwBky9JGffMwDBVSfJSD9TSU7aH2sbbfi0bZLMdekKAuimudXwUqPDxrrg0BCyvCYgLmKjbVT3zcdylWSog93CNTxGDPzauu-oc0XPNKCXnaDpNvE\",\"kty\":\"RSA\",\"q\":\"sP_QYavrpBvSJ86uoKVGj2AGl78CSsAtpf1ybSY5TwUlorXSdqapRbY69Y271b0aMLzlleUn9ZTBO1dlKV2_dw_lPADHVia8z3pxL-8sUhIXLsgj4acchMk4c9YX-sFh07xENnyZ-_TXm3llPLuL67HUfBC2eKe800TmCYVWc9U\",\"d\":\"bn1nFxCQT4KLTHqo8mo9HvHD0cRNRNdWcKNnnEQkCF6tKbt-ILRyQGP8O40axLd7CoNVG9c9p_-g4-2kwCtLJNv_STLtwfpCY7VN5o6-ZIpfTjiW6duoPrLWq64Hm_4LOBQTiZfUPcLhsuJRHbWqakj-kV_YbUyC2Ocf_dd8IAQcSrAU2SCcDebhDCWwRUFvaa9V5eq0851S9goaA-AJz-JXyePH6ZFr8JxmWkWxYZ5kdcMD-sm9ZbxE0CaEk32l4fE4hR-L8x2dDtjWA-ahKCZ091z-gV3HWtR2JOjvxoNRjxUo3UxaGiFJHWNIl0EYUJZu1Cb-5wIlEI7wPx5mwQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"qS0OK48M2CIAA6_4Wdw4EbCaAfcTLf5Oy9t5BOF_PFUKqoSpZ6JsT5H0a_4zkjt-oI969v78OTlvBKbmEyKO-KeytzHBAA5CsLmVcz0THrMSg6oXZqu66MPnvWoZN9FEN5TklPOvBFm8Bg1QZ3k-YMVaM--DLvhaYR95_mqaz50\",\"dp\":\"Too2NozLGD1XrXyhabZvy1E0EuaVFj0UHQPDLSpkZ_2g3BK6Art6T0xmE8RYtmqrKIEIdlI3IliAvyvAx_1D7zWTTRaj-xlZyqJFrnXWL7zj8UxT8PkB-r2E-ILZ3NAi1gxIWezlBTZ8M6NfObDFmbTc_3tJkN_raISo8z_ziIE\",\"dq\":\"U0yhSkY5yOsa9YcMoigGVBWSJLpNHtbg5NypjHrPv8OhWbkOSq7WvSstBkFk5AtyFvvfZLMLIkWWxxGzV0t6f1MoxBtttLrYYyCxwihiiGFhLbAdSuZ1wnxcqA9bC7UVECvrQmVTpsMs8UupfHKbQBpZ8OWAqrnuYNNtG4_4Bt0\",\"n\":\"lygtuZj0lJjqOqIWocF8Bb583QDdq-aaFg8PesOp2-EDda6GqCpL-_NZVOflNGX7XIgjsWHcPsQHsV9gWuOzSJ0iEuWvtQ6eGBP5M6m7pccLNZfwUse8Cb4Ngx3XiTlyuqM7pv0LPyppZusfEHVEdeelou7Dy9k0OQ_nJTI3b2E1WBoHC58CJ453lo4gcBm1efURN3LIVc1V9NQY_ESBKVdwqYyoJPEanURLVGRd6cQKn6YrCbbIRHjqAyqOE-z3KmgDJnPriljfR5XhSGyM9eqD9Xpy6zu_MAeMJJfSArp857zLPk-Wf5VP9STAcjyfdBIybMKnwBYr2qHMT675hQ\"}]}", + 200 + ); + + private static final MockResponse NOT_FOUND_RESPONSE = response( + "{ \"message\" : \"This mock authorization server responds to just one request: GET /.well-known/jwks.json.\" }", + 404 + ); + + public MockProvider() throws IOException { + Dispatcher dispatcher = new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) throws InterruptedException { + if ("/.well-known/jwks.json".equals(request.getPath())) { + return JWKS_RESPONSE; + } + + return NOT_FOUND_RESPONSE; + } + }; + + this.server.setDispatcher(dispatcher); + } + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + String uri = environment.getProperty("sample.jwk-set-uri", "mock://localhost:0"); + + if (uri.startsWith("mock://")) { + try { + this.server.start(URI.create(uri).getPort()); + } catch (IOException e) { + throw new IllegalStateException(e); + } + + Map properties = new HashMap<>(); + String url = this.server.url("/.well-known/jwks.json").toString(); + properties.put("sample.jwk-set-uri", url); + + MapPropertySource propertySource = new MapPropertySource("mock", properties); + environment.getPropertySources().addFirst(propertySource); + } + } + + @PreDestroy + public void shutdown() throws IOException { + this.server.shutdown(); + } + + private static MockResponse response(String body, int status) { + return new MockResponse() + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .setResponseCode(status) + .setBody(body); + } +} diff --git a/samples/boot/oauth2resourceserver/src/main/resources/META-INF/spring.factories b/samples/boot/oauth2resourceserver/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..34562aa3b0e --- /dev/null +++ b/samples/boot/oauth2resourceserver/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.env.EnvironmentPostProcessor=sample.provider.MockProvider diff --git a/samples/boot/oauth2resourceserver/src/main/resources/application.yml b/samples/boot/oauth2resourceserver/src/main/resources/application.yml new file mode 100644 index 00000000000..f61da202dfa --- /dev/null +++ b/samples/boot/oauth2resourceserver/src/main/resources/application.yml @@ -0,0 +1 @@ +sample.jwk-set-uri: mock://localhost:8081/.well-known/jwks.json From 02b1c3571bbc1865fee087514f2c05046f70e1f7 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 16 Jul 2018 11:17:08 -0500 Subject: [PATCH 127/226] Rename @TransientAuthentication to @Transient It is quite likely we will need to prevent certain Exceptions from being saved or from triggering a saved request. When we add support for this, we can now leverage @Transient vs creating a new annotation. Issue: gh-5481 --- .../core/{TransientAuthentication.java => Transient.java} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename core/src/main/java/org/springframework/security/core/{TransientAuthentication.java => Transient.java} (100%) diff --git a/core/src/main/java/org/springframework/security/core/TransientAuthentication.java b/core/src/main/java/org/springframework/security/core/Transient.java similarity index 100% rename from core/src/main/java/org/springframework/security/core/TransientAuthentication.java rename to core/src/main/java/org/springframework/security/core/Transient.java From eceae152b8144bf395556679e0a0cabbb5221320 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 16 Jul 2018 11:31:10 -0500 Subject: [PATCH 128/226] Rename @TransientAuthentication to @Transient It is quite likely we will need to prevent certain Exceptions from being saved or from triggering a saved request. When we add support for this, we can now leverage @Transient vs creating a new annotation. Issue: gh-5481 --- ...ionManagementConfigurerTransientAuthenticationTests.java | 4 ++-- ...SessionManagementConfigTransientAuthenticationTests.java | 4 ++-- .../java/org/springframework/security/core/Transient.java | 2 +- .../resource/authentication/JwtAuthenticationToken.java | 4 ++-- .../web/context/HttpSessionSecurityContextRepository.java | 4 ++-- .../context/HttpSessionSecurityContextRepositoryTests.java | 6 +++--- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTransientAuthenticationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTransientAuthenticationTests.java index 673b59ee837..2430ded2d30 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTransientAuthenticationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTransientAuthenticationTests.java @@ -29,7 +29,7 @@ import org.springframework.security.config.test.SpringTestRule; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.TransientAuthentication; +import org.springframework.security.core.Transient; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; @@ -104,7 +104,7 @@ public boolean supports(Class authentication) { } } - @TransientAuthentication + @Transient static class SomeTransientAuthentication extends AbstractAuthenticationToken { SomeTransientAuthentication() { super(null); diff --git a/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests.java b/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests.java index 2bf529bb2db..17d19f274c7 100644 --- a/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests.java +++ b/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests.java @@ -24,7 +24,7 @@ import org.springframework.security.config.test.SpringTestRule; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.TransientAuthentication; +import org.springframework.security.core.Transient; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; @@ -75,7 +75,7 @@ public boolean supports(Class authentication) { } } - @TransientAuthentication + @Transient static class SomeTransientAuthentication extends AbstractAuthenticationToken { SomeTransientAuthentication() { super(null); diff --git a/core/src/main/java/org/springframework/security/core/Transient.java b/core/src/main/java/org/springframework/security/core/Transient.java index 997ab1dcd4c..785bacc5d60 100644 --- a/core/src/main/java/org/springframework/security/core/Transient.java +++ b/core/src/main/java/org/springframework/security/core/Transient.java @@ -34,5 +34,5 @@ @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented -public @interface TransientAuthentication { +public @interface Transient { } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java index 8358125b42a..85ee5ed2a8f 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java @@ -20,7 +20,7 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.SpringSecurityCoreVersion; -import org.springframework.security.core.TransientAuthentication; +import org.springframework.security.core.Transient; import org.springframework.security.oauth2.jwt.Jwt; /** @@ -32,7 +32,7 @@ * @see AbstractOAuth2TokenAuthenticationToken * @see Jwt */ -@TransientAuthentication +@Transient public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; diff --git a/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java b/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java index c6deefe8771..01f84c3063b 100644 --- a/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java +++ b/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java @@ -30,7 +30,7 @@ import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.core.Authentication; -import org.springframework.security.core.TransientAuthentication; +import org.springframework.security.core.Transient; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -445,7 +445,7 @@ private HttpSession createNewSessionIfAllowed(SecurityContext context) { } private boolean isTransientAuthentication(Authentication authentication) { - return AnnotationUtils.getAnnotation(authentication.getClass(), TransientAuthentication.class) != null; + return AnnotationUtils.getAnnotation(authentication.getClass(), Transient.class) != null; } /** diff --git a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java index 4a9b0845cf8..56f4a78d315 100644 --- a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java +++ b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java @@ -37,7 +37,7 @@ import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.core.TransientAuthentication; +import org.springframework.security.core.Transient; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; @@ -674,7 +674,7 @@ public void saveContextWhenTransientAuthenticationWithCustomAnnotationThenSkippe assertThat(session).isNull(); } - @TransientAuthentication + @Transient private static class SomeTransientAuthentication extends AbstractAuthenticationToken { public SomeTransientAuthentication() { super(null); @@ -697,7 +697,7 @@ private static class SomeTransientAuthenticationSubclass extends SomeTransientAu @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) - @TransientAuthentication + @Transient public @interface TestTransientAuthentication { } From 07c4d92ea16c0a3129b498d4f31ceb5cb9e93c92 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 16 Jul 2018 14:19:23 -0600 Subject: [PATCH 129/226] Reliable Error State Tests Some of Resource Server Configurer's tests were relying on specific error messaging from Nimbus and from the JDK, which makes them brittle. These tests now simply confirm that resource server responses contain the correct error state without relying on specific wording outside of our control. --- .../OAuth2ResourceServerConfigurerTests.java | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index 4f58b098964..5d3291b0489 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -25,6 +25,10 @@ import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; +import org.hamcrest.core.AllOf; +import org.hamcrest.core.StringContains; +import org.hamcrest.core.StringEndsWith; +import org.hamcrest.core.StringStartsWith; import org.junit.Rule; import org.junit.Test; @@ -115,7 +119,7 @@ public void getWhenUsingDefaultsWithExpiredBearerTokenThenInvalidToken() this.mvc.perform(get("/").with(bearerToken(token))) .andExpect(status().isUnauthorized()) - .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: Expired JWT")); + .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt")); } @Test @@ -141,8 +145,7 @@ public void getWhenUsingDefaultsWithUnavailableJwkEndpointThenInvalidToken() this.mvc.perform(get("/").with(bearerToken(token))) .andExpect(status().isUnauthorized()) - .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: " + - "Couldn't retrieve remote JWK set: Connection refused (Connection refused)")); + .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt")); } @Test @@ -166,8 +169,7 @@ public void getWhenUsingDefaultsWithMalformedPayloadThenInvalidToken() this.mvc.perform(get("/").with(bearerToken(token))) .andExpect(status().isUnauthorized()) - .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: " + - "Malformed payload")); + .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: Malformed payload")); } @Test @@ -192,8 +194,7 @@ public void getWhenUsingDefaultsWithBearerTokenBeforeNotBeforeThenInvalidToken() this.mvc.perform(get("/").with(bearerToken(token))) .andExpect(status().isUnauthorized()) - .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: " + - "JWT before use time")); + .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt")); } @Test @@ -313,8 +314,7 @@ public void getWhenUsingDefaultsAndAuthorizationServerHasNoMatchingKeyThenInvali this.mvc.perform(get("/") .with(bearerToken(token))) .andExpect(status().isUnauthorized()) - .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: " + - "Signed JWT rejected: Another algorithm expected, or no matching key(s) found")); + .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt")); } @Test @@ -456,7 +456,7 @@ public void postWhenUsingDefaultsWithExpiredBearerTokenAndNoCsrfThenInvalidToken this.mvc.perform(post("/authenticated") .with(bearerToken(token))) .andExpect(status().isUnauthorized()) - .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: Expired JWT")); + .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt")); } // -- Resource Server should not create sessions @@ -785,17 +785,29 @@ private static BearerTokenRequestPostProcessor bearerToken(String token) { } private static ResultMatcher invalidRequestHeader(String message) { - return header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer " + - "error=\"invalid_request\", " + - "error_description=\"" + message + "\", " + - "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\""); + return header().string(HttpHeaders.WWW_AUTHENTICATE, + AllOf.allOf( + new StringStartsWith("Bearer " + + "error=\"invalid_request\", " + + "error_description=\""), + new StringContains(message), + new StringEndsWith(", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"") + ) + ); } private static ResultMatcher invalidTokenHeader(String message) { - return header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer " + - "error=\"invalid_token\", " + - "error_description=\"" + message + "\", " + - "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\""); + return header().string(HttpHeaders.WWW_AUTHENTICATE, + AllOf.allOf( + new StringStartsWith("Bearer " + + "error=\"invalid_token\", " + + "error_description=\""), + new StringContains(message), + new StringEndsWith(", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"") + ) + ); } private static ResultMatcher insufficientScopeHeader(String scope) { From d002814f4f13a55ecb03d9ef5218108ffa67e050 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 16 Jul 2018 14:43:57 -0600 Subject: [PATCH 130/226] Reliable Error State Tests - Nimbus A test against the Nimbus library was relying on specific messaging from Nimbus as well as the JDK, making it brittle. Now, it simply relies on the messaging that we control. Issue: gh-4887 --- .../security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java index e90560cca7f..675445f634f 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java @@ -146,7 +146,7 @@ public void decodeWhenJwkResponseIsMalformedThenReturnsStockException() throws E } @Test - public void decodeWhenJwkEndpointIsUnresponsiveThenReturnsStockException() throws Exception { + public void decodeWhenJwkEndpointIsUnresponsiveThenRetrunsJwtException() throws Exception { try ( MockWebServer server = new MockWebServer() ) { server.enqueue(new MockResponse().setBody(MALFORMED_JWK_SET)); String jwkSetUrl = server.url("/.well-known/jwks.json").toString(); @@ -157,8 +157,7 @@ public void decodeWhenJwkEndpointIsUnresponsiveThenReturnsStockException() throw assertThatCode(() -> decoder.decode(SIGNED_JWT)) .isInstanceOf(JwtException.class) - .hasMessage("An error occurred while attempting to decode the Jwt: " + - "Couldn't retrieve remote JWK set: Connection refused (Connection refused)"); + .hasMessageContaining("An error occurred while attempting to decode the Jwt"); } } } From 3687098ced973cbdab0685f33b48ee6cc2b1b04c Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 16 Jul 2018 16:12:18 -0400 Subject: [PATCH 131/226] Allow configuring a custom OAuth2AuthorizationRequestResolver Fixes gh-5521 --- .../oauth2/client/OAuth2ClientConfigurer.java | 32 +++++++++--- .../oauth2/client/OAuth2LoginConfigurer.java | 33 ++++++++++--- .../client/OAuth2ClientConfigurerTests.java | 31 ++++++++++++ .../client/OAuth2LoginConfigurerTests.java | 49 +++++++++++++++++-- 4 files changed, 130 insertions(+), 15 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java index 5e85ff4bf7d..4d769d29c15 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java @@ -28,6 +28,7 @@ import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.util.Assert; @@ -147,6 +148,7 @@ public AuthorizationEndpointConfig authorizationEndpoint() { */ public class AuthorizationEndpointConfig { private String authorizationRequestBaseUri; + private OAuth2AuthorizationRequestResolver authorizationRequestResolver; private AuthorizationRequestRepository authorizationRequestRepository; private AuthorizationEndpointConfig() { @@ -164,6 +166,18 @@ public AuthorizationEndpointConfig baseUri(String authorizationRequestBaseUri) { return this; } + /** + * Sets the resolver used for resolving {@link OAuth2AuthorizationRequest}'s. + * + * @param authorizationRequestResolver the resolver used for resolving {@link OAuth2AuthorizationRequest}'s + * @return the {@link AuthorizationEndpointConfig} for further configuration + */ + public AuthorizationEndpointConfig authorizationRequestResolver(OAuth2AuthorizationRequestResolver authorizationRequestResolver) { + Assert.notNull(authorizationRequestResolver, "authorizationRequestResolver cannot be null"); + this.authorizationRequestResolver = authorizationRequestResolver; + return this; + } + /** * Sets the repository used for storing {@link OAuth2AuthorizationRequest}'s. * @@ -267,14 +281,20 @@ private void init(B builder, AuthorizationCodeGrantConfigurer authorizationCodeG } private void configure(B builder, AuthorizationCodeGrantConfigurer authorizationCodeGrantConfigurer) throws Exception { - String authorizationRequestBaseUri = authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestBaseUri; - if (authorizationRequestBaseUri == null) { - authorizationRequestBaseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; + OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter; + + if (authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestResolver != null) { + authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter( + authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestResolver); + } else { + String authorizationRequestBaseUri = authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestBaseUri; + if (authorizationRequestBaseUri == null) { + authorizationRequestBaseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; + } + authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter( + OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder), authorizationRequestBaseUri); } - OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter( - OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder), authorizationRequestBaseUri); - if (authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestRepository != null) { authorizationRequestFilter.setAuthorizationRequestRepository( authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestRepository); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java index 55ca29413e4..b8160854bf8 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java @@ -44,6 +44,7 @@ import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; @@ -178,6 +179,7 @@ public AuthorizationEndpointConfig authorizationEndpoint() { */ public class AuthorizationEndpointConfig { private String authorizationRequestBaseUri; + private OAuth2AuthorizationRequestResolver authorizationRequestResolver; private AuthorizationRequestRepository authorizationRequestRepository; private AuthorizationEndpointConfig() { @@ -195,6 +197,19 @@ public AuthorizationEndpointConfig baseUri(String authorizationRequestBaseUri) { return this; } + /** + * Sets the resolver used for resolving {@link OAuth2AuthorizationRequest}'s. + * + * @since 5.1 + * @param authorizationRequestResolver the resolver used for resolving {@link OAuth2AuthorizationRequest}'s + * @return the {@link AuthorizationEndpointConfig} for further configuration + */ + public AuthorizationEndpointConfig authorizationRequestResolver(OAuth2AuthorizationRequestResolver authorizationRequestResolver) { + Assert.notNull(authorizationRequestResolver, "authorizationRequestResolver cannot be null"); + this.authorizationRequestResolver = authorizationRequestResolver; + return this; + } + /** * Sets the repository used for storing {@link OAuth2AuthorizationRequest}'s. * @@ -444,13 +459,19 @@ public void init(B http) throws Exception { @Override public void configure(B http) throws Exception { - String authorizationRequestBaseUri = this.authorizationEndpointConfig.authorizationRequestBaseUri; - if (authorizationRequestBaseUri == null) { - authorizationRequestBaseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; - } + OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter; - OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter( - OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()), authorizationRequestBaseUri); + if (this.authorizationEndpointConfig.authorizationRequestResolver != null) { + authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter( + this.authorizationEndpointConfig.authorizationRequestResolver); + } else { + String authorizationRequestBaseUri = this.authorizationEndpointConfig.authorizationRequestBaseUri; + if (authorizationRequestBaseUri == null) { + authorizationRequestBaseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; + } + authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter( + OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()), authorizationRequestBaseUri); + } if (this.authorizationEndpointConfig.authorizationRequestRepository != null) { authorizationRequestFilter.setAuthorizationRequestRepository( diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java index 919305eed0d..43414b9d4c9 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java @@ -37,7 +37,9 @@ import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; @@ -74,6 +76,8 @@ public class OAuth2ClientConfigurerTests { private static OAuth2AuthorizedClientService authorizedClientService; + private static OAuth2AuthorizationRequestResolver authorizationRequestResolver; + private static OAuth2AccessTokenResponseClient accessTokenResponseClient; private static RequestCache requestCache; @@ -103,6 +107,8 @@ public void setup() { .build(); clientRegistrationRepository = new InMemoryClientRegistrationRepository(this.registration1); authorizedClientService = new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository); + authorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( + clientRegistrationRepository, "/oauth2/authorization"); OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("access-token-1234") .tokenType(OAuth2AccessToken.TokenType.BEARER) @@ -173,6 +179,28 @@ public void configureWhenRequestCacheProvidedAndClientAuthorizationRequiredExcep verify(requestCache).saveRequest(any(HttpServletRequest.class), any(HttpServletResponse.class)); } + // gh-5521 + @Test + public void configureWhenCustomAuthorizationRequestResolverSetThenAuthorizationRequestIncludesCustomParameters() throws Exception { + // Override default resolver + OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver = authorizationRequestResolver; + authorizationRequestResolver = request -> { + OAuth2AuthorizationRequest defaultAuthorizationRequest = defaultAuthorizationRequestResolver.resolve(request); + Map additionalParameters = new HashMap<>(defaultAuthorizationRequest.getAdditionalParameters()); + additionalParameters.put("param1", "value1"); + return OAuth2AuthorizationRequest.from(defaultAuthorizationRequest) + .additionalParameters(additionalParameters) + .build(); + }; + + this.spring.register(OAuth2ClientConfig.class).autowire(); + + MvcResult mvcResult = this.mockMvc.perform(get("/oauth2/authorization/registration-1")) + .andExpect(status().is3xxRedirection()) + .andReturn(); + assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fclient-1¶m1=value1"); + } + @EnableWebSecurity @EnableWebMvc static class OAuth2ClientConfig extends WebSecurityConfigurerAdapter { @@ -188,6 +216,9 @@ protected void configure(HttpSecurity http) throws Exception { .oauth2() .client() .authorizationCodeGrant() + .authorizationEndpoint() + .authorizationRequestResolver(authorizationRequestResolver) + .and() .tokenEndpoint() .accessTokenResponseClient(accessTokenResponseClient); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java index b0dc626f42a..11c41eb90b0 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java @@ -42,7 +42,9 @@ import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; @@ -105,11 +107,9 @@ public class OAuth2LoginConfigurerTests { @Before public void setup() { this.request = new MockHttpServletRequest("GET", ""); + this.request.setServletPath("/login/oauth2/code/google"); this.response = new MockHttpServletResponse(); this.filterChain = new MockFilterChain(); - - this.request.setMethod("GET"); - this.request.setServletPath("/login/oauth2/code/google"); } @After @@ -225,6 +225,20 @@ public void oauth2LoginConfigLoginProcessingUrl() throws Exception { .isInstanceOf(OAuth2UserAuthority.class).hasToString("ROLE_USER"); } + // gh-5521 + @Test + public void oauth2LoginWithCustomAuthorizationRequestParameters() throws Exception { + loadConfig(OAuth2LoginConfigCustomAuthorizationRequestResolver.class); + + String requestUri = "/oauth2/authorization/google"; + this.request = new MockHttpServletRequest("GET", requestUri); + this.request.setServletPath(requestUri); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + + assertThat(this.response.getRedirectedUrl()).matches("https://accounts.google.com/o/oauth2/v2/auth\\?response_type=code&client_id=clientId&scope=openid\\+profile\\+email&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fgoogle&custom-param1=custom-value1"); + } + @Test public void oidcLogin() throws Exception { // setup application context @@ -406,6 +420,35 @@ protected void configure(HttpSecurity http) throws Exception { } } + @EnableWebSecurity + static class OAuth2LoginConfigCustomAuthorizationRequestResolver extends CommonWebSecurityConfigurerAdapter { + private ClientRegistrationRepository clientRegistrationRepository = + new InMemoryClientRegistrationRepository(CLIENT_REGISTRATION); + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .oauth2Login() + .clientRegistrationRepository(this.clientRegistrationRepository) + .authorizationEndpoint() + .authorizationRequestResolver(this.getAuthorizationRequestResolver()); + super.configure(http); + } + + private OAuth2AuthorizationRequestResolver getAuthorizationRequestResolver() { + OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver = + new DefaultOAuth2AuthorizationRequestResolver(this.clientRegistrationRepository, "/oauth2/authorization"); + return request -> { + OAuth2AuthorizationRequest defaultAuthorizationRequest = defaultAuthorizationRequestResolver.resolve(request); + Map additionalParameters = new HashMap<>(defaultAuthorizationRequest.getAdditionalParameters()); + additionalParameters.put("custom-param1", "custom-value1"); + return OAuth2AuthorizationRequest.from(defaultAuthorizationRequest) + .additionalParameters(additionalParameters) + .build(); + }; + } + } + private static abstract class CommonWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { From 27e4751f3b4c17754054f64d033cb697be7b4b4a Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 17 Jul 2018 01:27:39 -0500 Subject: [PATCH 132/226] Add ClientRegistration.Builder.registrationId Fixes: gh-5527 --- .../registration/ClientRegistration.java | 11 ++++++++++ .../registration/ClientRegistrationTests.java | 20 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java index 080dde9d692..bf3f5e198ce 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java @@ -255,6 +255,17 @@ private Builder(String registrationId) { this.registrationId = registrationId; } + /** + * Sets the client identifier. + * + * @param registrationId the registration id + * @return the {@link Builder} + */ + public Builder registrationId(String registrationId) { + this.registrationId = registrationId; + return this; + } + /** * Sets the client identifier. * diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java index a9b1d7a7149..95f23bd833f 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java @@ -351,4 +351,24 @@ public void buildWhenImplicitGrantClientNameIsNullThenThrowIllegalArgumentExcept .clientName(null) .build(); } + + @Test + public void buildWhenOverrideRegistrationIdThenOverridden() { + String overriddenId = "override"; + ClientRegistration registration = ClientRegistration.withRegistrationId(REGISTRATION_ID) + .registrationId(overriddenId) + .clientId(CLIENT_ID) + .clientSecret(CLIENT_SECRET) + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate(REDIRECT_URI) + .scope(SCOPES.toArray(new String[0])) + .authorizationUri(AUTHORIZATION_URI) + .tokenUri(TOKEN_URI) + .jwkSetUri(JWK_SET_URI) + .clientName(CLIENT_NAME) + .build(); + + assertThat(registration.getRegistrationId()).isEqualTo(overriddenId); + } } From a294ae38d518f14cba9c62683c17f96f98c38735 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 16 Jul 2018 13:21:24 -0600 Subject: [PATCH 133/226] Add Bearer Token filter to Security Filters This introduces BearerTokenAuthenticationFilter to SecurityFilters so that it can be used in the various addFilter methods and with the `custom-filter` xml tag. Fixes: gh-5479 --- .../security/config/annotation/web/HttpSecurityBuilder.java | 1 + .../config/annotation/web/builders/FilterComparator.java | 4 +++- .../server/resource/OAuth2ResourceServerConfigurer.java | 3 +-- .../springframework/security/config/http/SecurityFilters.java | 1 + .../springframework/security/config/spring-security-5.1.rnc | 2 +- .../springframework/security/config/spring-security-5.1.xsd | 1 + 6 files changed, 8 insertions(+), 4 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java index 0e4ff7e6f76..bdd1fe911e6 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java @@ -149,6 +149,7 @@ > C removeConfigurer *

    • {@link org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter}
    • *
    • {@link ConcurrentSessionFilter}
    • *
    • {@link DigestAuthenticationFilter}
    • + *
    • {@link org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter}
    • *
    • {@link BasicAuthenticationFilter}
    • *
    • {@link RequestCacheAwareFilter}
    • *
    • {@link SecurityContextHolderAwareRequestFilter}
    • diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java index ebc1a22715a..7c1fb68c1f0 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java @@ -19,7 +19,6 @@ import java.util.Comparator; import java.util.HashMap; import java.util.Map; - import javax.servlet.Filter; import org.springframework.security.web.access.ExceptionTranslationFilter; @@ -108,6 +107,9 @@ final class FilterComparator implements Comparator, Serializable { order += STEP; put(DigestAuthenticationFilter.class, order); order += STEP; + filterToOrder.put( + "org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter", order); + order += STEP; put(BasicAuthenticationFilter.class, order); order += STEP; put(RequestCacheAwareFilter.class, order); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java index faba20fa3e9..de610cfa590 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java @@ -33,7 +33,6 @@ import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver; import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler; -import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; @@ -132,7 +131,7 @@ public void configure(H http) throws Exception { filter.setBearerTokenResolver(bearerTokenResolver); filter = postProcess(filter); - http.addFilterBefore(filter, BasicAuthenticationFilter.class); + http.addFilter(filter); JwtDecoder decoder = this.jwtConfigurer.getJwtDecoder(); diff --git a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java index 1b84ecace48..516cf31cfc8 100644 --- a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java +++ b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java @@ -42,6 +42,7 @@ enum SecurityFilters { LOGIN_PAGE_FILTER, LOGOUT_PAGE_FILTER, DIGEST_AUTH_FILTER, + BEARER_TOKEN_AUTH_FILTER, BASIC_AUTH_FILTER, REQUEST_CACHE_FILTER, SERVLET_API_SUPPORT_FILTER, diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.1.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.rnc index 6f67240121e..f3b75156ad4 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.1.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.rnc @@ -906,4 +906,4 @@ position = ## The explicit position at which the custom-filter should be placed in the chain. Use if you are replacing a standard filter. attribute position {named-security-filter} -named-security-filter = "FIRST" | "CHANNEL_FILTER" | "SECURITY_CONTEXT_FILTER" | "CONCURRENT_SESSION_FILTER" | "WEB_ASYNC_MANAGER_FILTER" | "HEADERS_FILTER" | "CORS_FILTER" | "CSRF_FILTER" | "LOGOUT_FILTER" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_FILTER" | "FORM_LOGIN_FILTER" | "OPENID_FILTER" | "LOGIN_PAGE_FILTER" |"LOGOUT_PAGE_FILTER" | "DIGEST_AUTH_FILTER" | "BASIC_AUTH_FILTER" | "REQUEST_CACHE_FILTER" | "SERVLET_API_SUPPORT_FILTER" | "JAAS_API_SUPPORT_FILTER" | "REMEMBER_ME_FILTER" | "ANONYMOUS_FILTER" | "SESSION_MANAGEMENT_FILTER" | "EXCEPTION_TRANSLATION_FILTER" | "FILTER_SECURITY_INTERCEPTOR" | "SWITCH_USER_FILTER" | "LAST" +named-security-filter = "FIRST" | "CHANNEL_FILTER" | "SECURITY_CONTEXT_FILTER" | "CONCURRENT_SESSION_FILTER" | "WEB_ASYNC_MANAGER_FILTER" | "HEADERS_FILTER" | "CORS_FILTER" | "CSRF_FILTER" | "LOGOUT_FILTER" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_FILTER" | "FORM_LOGIN_FILTER" | "OPENID_FILTER" | "LOGIN_PAGE_FILTER" |"LOGOUT_PAGE_FILTER" | "DIGEST_AUTH_FILTER" | "BEARER_TOKEN_AUTH_FILTER" | "BASIC_AUTH_FILTER" | "REQUEST_CACHE_FILTER" | "SERVLET_API_SUPPORT_FILTER" | "JAAS_API_SUPPORT_FILTER" | "REMEMBER_ME_FILTER" | "ANONYMOUS_FILTER" | "SESSION_MANAGEMENT_FILTER" | "EXCEPTION_TRANSLATION_FILTER" | "FILTER_SECURITY_INTERCEPTOR" | "SWITCH_USER_FILTER" | "LAST" diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.1.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.xsd index acb5e85c84e..6434e496fa2 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.1.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.xsd @@ -2721,6 +2721,7 @@ + From 695f3f7fe8eb01f11f00a2291e291a7eacac0c5b Mon Sep 17 00:00:00 2001 From: "mhyeon.lee" Date: Tue, 17 Jul 2018 00:21:31 +0900 Subject: [PATCH 134/226] Fix DefaultOAuth2AuthorizationRequestResolver baseUrl excludes queryParams To create redirect_uri in DefaultOAuth2AuthorizationRequestResolver, queryParam is included in the current request-based baseUrl. So when binding to the redirectUriTemplate, the wrong type of redirect_uri may be created. Fixes gh-5520 --- ...efaultOAuth2AuthorizationRequestResolver.java | 1 + ...tOAuth2AuthorizationRequestResolverTests.java | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java index 1cdf283d1fc..18a443b6c43 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java @@ -155,6 +155,7 @@ private String expandRedirectUri(HttpServletRequest request, ClientRegistration Map uriVariables = new HashMap<>(); uriVariables.put("registrationId", clientRegistration.getRegistrationId()); String baseUrl = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) + .replaceQuery(null) .replacePath(request.getContextPath()) .build() .toUriString(); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java index 2ff722aa024..b5d548c8477 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java @@ -163,6 +163,22 @@ public void resolveWhenAuthorizationRequestRedirectUriTemplatedThenRedirectUriEx "http://localhost/login/oauth2/code/" + clientRegistration.getRegistrationId()); } + // gh-5520 + @Test + public void resolveWhenAuthorizationRequestRedirectUriTemplatedThenRedirectUriExpandedExcludesQueryString() { + ClientRegistration clientRegistration = this.registration2; + String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + request.setQueryString("foo=bar"); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest.getRedirectUri()).isNotEqualTo( + clientRegistration.getRedirectUriTemplate()); + assertThat(authorizationRequest.getRedirectUri()).isEqualTo( + "http://localhost/login/oauth2/code/" + clientRegistration.getRegistrationId()); + } + @Test public void resolveWhenAuthorizationRequestIncludesPort80ThenExpandedRedirectUriExcludesPort() { ClientRegistration clientRegistration = this.registration1; From ba745eb3a4391743d712f65ff9aa976c062ba107 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 13 Jul 2018 14:34:40 -0600 Subject: [PATCH 135/226] SessionManagementConfigTests groovy->java Issue: gh-4939 --- .../http/SessionManagementConfigTests.groovy | 430 ----------- .../http/SessionManagementConfigTests.java | 667 ++++++++++++++++++ ...-ConcurrencyControlCustomLogoutHandler.xml | 38 + ...nfigTests-ConcurrencyControlExpiredUrl.xml | 43 ++ ...encyControlLogoutAndRememberMeHandlers.xml | 40 ++ ...figTests-ConcurrencyControlMaxSessions.xml | 37 + ...ts-ConcurrencyControlRememberMeHandler.xml | 39 + ...ConcurrencyControlSessionRegistryAlias.xml | 35 + ...s-ConcurrencyControlSessionRegistryRef.xml | 41 ++ ...agementConfigTests-CreateSessionAlways.xml | 34 + ...entConfigTests-CreateSessionIfRequired.xml | 35 + ...nagementConfigTests-CreateSessionNever.xml | 35 + ...mentConfigTests-CreateSessionStateless.xml | 35 + ...tConfigTests-NoSessionManagementFilter.xml | 37 + .../SessionManagementConfigTests-Sec1208.xml | 39 + .../SessionManagementConfigTests-Sec2137.xml | 38 + ...Tests-SessionAuthenticationStrategyRef.xml | 39 + ...essionFixationProtectionMigrateSession.xml | 36 + ...figTests-SessionFixationProtectionNone.xml | 36 + ...ionProtectionNoneWithInvalidSessionUrl.xml | 36 + 20 files changed, 1340 insertions(+), 430 deletions(-) delete mode 100644 config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy create mode 100644 config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTests.java create mode 100644 config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlCustomLogoutHandler.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlExpiredUrl.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlLogoutAndRememberMeHandlers.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlMaxSessions.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlRememberMeHandler.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlSessionRegistryAlias.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlSessionRegistryRef.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionAlways.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionIfRequired.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionNever.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionStateless.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-NoSessionManagementFilter.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-Sec1208.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-Sec2137.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionAuthenticationStrategyRef.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionMigrateSession.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionNone.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionNoneWithInvalidSessionUrl.xml diff --git a/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy deleted file mode 100644 index 15cf40a8319..00000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy +++ /dev/null @@ -1,430 +0,0 @@ -/* - * Copyright 2002-2013 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.config.http - -import org.springframework.mock.web.MockFilterChain -import org.springframework.mock.web.MockHttpServletRequest -import org.springframework.mock.web.MockHttpServletResponse -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken -import org.springframework.security.core.Authentication -import org.springframework.security.core.authority.AuthorityUtils -import org.springframework.security.core.context.SecurityContext -import org.springframework.security.core.context.SecurityContextHolder -import org.springframework.security.core.session.SessionRegistry -import org.springframework.security.core.session.SessionRegistryImpl -import org.springframework.security.core.userdetails.User -import org.springframework.security.util.FieldUtils -import org.springframework.security.web.FilterChainProxy -import org.springframework.security.web.authentication.RememberMeServices -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter -import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler -import org.springframework.security.web.authentication.logout.LogoutFilter -import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler -import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter -import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy -import org.springframework.security.web.context.NullSecurityContextRepository -import org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper -import org.springframework.security.web.context.SecurityContextPersistenceFilter -import org.springframework.security.web.savedrequest.RequestCacheAwareFilter -import org.springframework.security.web.session.ConcurrentSessionFilter -import org.springframework.security.web.session.SessionManagementFilter - -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse - -import static org.junit.Assert.assertSame -import static org.mockito.Matchers.any -import static org.mockito.Mockito.verify - -/** - * Tests session-related functionality for the <http> namespace element and <session-management> - * - * @author Luke Taylor - * @author Rob Winch - */ -class SessionManagementConfigTests extends AbstractHttpConfigTests { - - def settingCreateSessionToAlwaysSetsFilterPropertiesCorrectly() { - httpCreateSession('always') { } - createAppContext(); - - def filter = getFilter(SecurityContextPersistenceFilter.class); - - expect: - filter.forceEagerSessionCreation - filter.repo.allowSessionCreation - filter.repo.disableUrlRewriting - } - - def settingCreateSessionToNeverSetsFilterPropertiesCorrectly() { - httpCreateSession('never') { } - createAppContext(); - - def filter = getFilter(SecurityContextPersistenceFilter.class); - - expect: - !filter.forceEagerSessionCreation - !filter.repo.allowSessionCreation - } - - def settingCreateSessionToStatelessSetsFilterPropertiesCorrectly() { - httpCreateSession('stateless') { } - createAppContext(); - - def filter = getFilter(SecurityContextPersistenceFilter.class); - - expect: - !filter.forceEagerSessionCreation - filter.repo instanceof NullSecurityContextRepository - getFilter(SessionManagementFilter.class) == null - getFilter(RequestCacheAwareFilter.class) == null - } - - def settingCreateSessionToIfRequiredDoesntCreateASessionForPublicInvocation() { - httpCreateSession('ifRequired') { } - createAppContext(); - - def filter = getFilter(SecurityContextPersistenceFilter.class); - - expect: - !filter.forceEagerSessionCreation - filter.repo.allowSessionCreation - } - - def 'SEC-1208: Session is not created when rejecting user due to max sessions exceeded'() { - setup: - httpCreateSession('never') { - 'session-management'() { - 'concurrency-control'('max-sessions':1,'error-if-maximum-exceeded':'true') - } - csrf(disabled:true) - } - createAppContext() - SessionRegistry registry = appContext.getBean(SessionRegistry) - registry.registerNewSession("1", new User("user","password",AuthorityUtils.createAuthorityList("ROLE_USER"))) - MockHttpServletRequest request = new MockHttpServletRequest("GET", "") - MockHttpServletResponse response = new MockHttpServletResponse() - String credentials = "user:password" - request.addHeader("Authorization", "Basic " + credentials.bytes.encodeBase64()) - when: "exceed max authentication attempts" - appContext.getBean(FilterChainProxy).doFilter(request, response, new MockFilterChain()) - then: "no new session is created" - request.getSession(false) == null - response.status == HttpServletResponse.SC_UNAUTHORIZED - } - - def 'SEC-2137: disable session fixation and enable concurrency control'() { - setup: "context where session fixation is disabled and concurrency control is enabled" - httpAutoConfig { - 'session-management'('session-fixation-protection':'none') { - 'concurrency-control'('max-sessions':'1','error-if-maximum-exceeded':'true') - } - } - createAppContext() - MockHttpServletRequest request = new MockHttpServletRequest("GET", "") - MockHttpServletResponse response = new MockHttpServletResponse() - String originalSessionId = request.session.id - String credentials = "user:password" - request.addHeader("Authorization", "Basic " + credentials.bytes.encodeBase64()) - when: "authenticate" - appContext.getBean(FilterChainProxy).doFilter(request, response, new MockFilterChain()) - then: "session invalidate is not called" - request.session.id == originalSessionId - } - - def httpCreateSession(String create, Closure c) { - xml.http(['auto-config': 'true', 'create-session': create], c) - } - - def concurrentSessionSupportAddsFilterAndExpectedBeans() { - when: - httpAutoConfig { - 'session-management'() { - 'concurrency-control'('session-registry-alias':'sr', 'expired-url': '/expired') - } - csrf(disabled:true) - } - createAppContext(); - List filters = getFilters("/someurl"); - def concurrentSessionFilter = filters.get(1) - - then: - concurrentSessionFilter instanceof ConcurrentSessionFilter - concurrentSessionFilter.sessionInformationExpiredStrategy.destinationUrl == '/expired' - appContext.getBean("sr") != null - getFilter(SessionManagementFilter.class) != null - sessionRegistryIsValid(); - - concurrentSessionFilter.handlers.logoutHandlers.size() == 1 - def logoutHandler = concurrentSessionFilter.handlers.logoutHandlers[0] - logoutHandler instanceof SecurityContextLogoutHandler - logoutHandler.invalidateHttpSession - - } - - def 'concurrency-control adds custom logout handlers'() { - when: 'Custom logout and remember-me' - httpAutoConfig { - 'session-management'() { - 'concurrency-control'() - } - 'logout'('invalidate-session': false, 'delete-cookies': 'testCookie') - 'remember-me'() - csrf(disabled:true) - } - createAppContext() - - List filters = getFilters("/someurl") - ConcurrentSessionFilter concurrentSessionFilter = filters.get(1) - def logoutHandlers = concurrentSessionFilter.handlers.logoutHandlers - - then: 'ConcurrentSessionFilter contains the customized LogoutHandlers' - logoutHandlers.size() == 3 - def securityCtxlogoutHandler = logoutHandlers.find { it instanceof SecurityContextLogoutHandler } - securityCtxlogoutHandler.invalidateHttpSession == false - def cookieClearingLogoutHandler = logoutHandlers.find { it instanceof CookieClearingLogoutHandler } - cookieClearingLogoutHandler.cookiesToClear == ['testCookie'] - def remembermeLogoutHandler = logoutHandlers.find { it instanceof RememberMeServices } - remembermeLogoutHandler == getFilter(RememberMeAuthenticationFilter.class).rememberMeServices - } - - def 'concurrency-control with remember-me and no LogoutFilter contains SecurityContextLogoutHandler and RememberMeServices as LogoutHandlers'() { - when: 'RememberMe and No LogoutFilter' - xml.http(['entry-point-ref': 'entryPoint'], { - 'session-management'() { - 'concurrency-control'() - } - 'remember-me'() - csrf(disabled:true) - }) - bean('entryPoint', 'org.springframework.security.web.authentication.Http403ForbiddenEntryPoint') - createAppContext() - - List filters = getFilters("/someurl") - ConcurrentSessionFilter concurrentSessionFilter = filters.get(1) - def logoutHandlers = concurrentSessionFilter.handlers.logoutHandlers - - then: 'SecurityContextLogoutHandler and RememberMeServices are in ConcurrentSessionFilter logoutHandlers' - !filters.find { it instanceof LogoutFilter } - logoutHandlers.size() == 2 - def securityCtxlogoutHandler = logoutHandlers.find { it instanceof SecurityContextLogoutHandler } - securityCtxlogoutHandler.invalidateHttpSession == true - logoutHandlers.find { it instanceof RememberMeServices } == getFilter(RememberMeAuthenticationFilter).rememberMeServices - } - - def 'concurrency-control with no remember-me or LogoutFilter contains SecurityContextLogoutHandler as LogoutHandlers'() { - when: 'No Logout Filter or RememberMe' - xml.http(['entry-point-ref': 'entryPoint'], { - 'session-management'() { - 'concurrency-control'() - } - }) - bean('entryPoint', 'org.springframework.security.web.authentication.Http403ForbiddenEntryPoint') - createAppContext() - - List filters = getFilters("/someurl") - ConcurrentSessionFilter concurrentSessionFilter = filters.get(1) - def logoutHandlers = concurrentSessionFilter.handlers.logoutHandlers - - then: 'Only SecurityContextLogoutHandler is found in ConcurrentSessionFilter logoutHandlers' - !filters.find { it instanceof LogoutFilter } - logoutHandlers.size() == 1 - def securityCtxlogoutHandler = logoutHandlers.find { it instanceof SecurityContextLogoutHandler } - securityCtxlogoutHandler.invalidateHttpSession == true - } - - def 'SEC-2057: ConcurrentSessionFilter is after SecurityContextPersistenceFilter'() { - httpAutoConfig { - 'session-management'() { - 'concurrency-control'() - } - } - createAppContext() - List filters = getFilters("/someurl") - - expect: - filters.get(0) instanceof SecurityContextPersistenceFilter - filters.get(1) instanceof ConcurrentSessionFilter - } - - def 'concurrency-control handles default expired-url as null'() { - httpAutoConfig { - 'session-management'() { - 'concurrency-control'('session-registry-alias':'sr') - } - } - createAppContext(); - List filters = getFilters("/someurl"); - - expect: - filters.get(1).sessionInformationExpiredStrategy.class.name == 'org.springframework.security.web.session.ConcurrentSessionFilter$ResponseBodySessionInformationExpiredStrategy' - } - - def externalSessionStrategyIsSupported() { - setup: - httpAutoConfig { - 'session-management'('session-authentication-strategy-ref':'ss') - csrf(disabled:true) - } - mockBean(SessionAuthenticationStrategy,'ss') - createAppContext() - - MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); - request.getSession(); - request.servletPath = "/login" - request.setMethod("POST"); - request.setParameter("username", "user"); - request.setParameter("password", "password"); - - SessionAuthenticationStrategy sessionAuthStrategy = appContext.getBean('ss',SessionAuthenticationStrategy) - FilterChainProxy springSecurityFilterChain = appContext.getBean(FilterChainProxy) - when: - springSecurityFilterChain.doFilter(request,new MockHttpServletResponse(), new MockFilterChain()) - then: "CustomSessionAuthenticationStrategy has seen the request (although REQUEST is a wrapped request)" - verify(sessionAuthStrategy).onAuthentication(any(Authentication), any(HttpServletRequest), any(HttpServletResponse)) - } - - def externalSessionRegistryBeanIsConfiguredCorrectly() { - httpAutoConfig { - 'session-management'() { - 'concurrency-control'('session-registry-ref':'sr') - } - csrf(disabled:true) - } - bean('sr', SessionRegistryImpl.class.name) - createAppContext(); - - expect: - sessionRegistryIsValid(); - } - - def sessionRegistryIsValid() { - Object sessionRegistry = appContext.getBean("sr"); - Object sessionRegistryFromConcurrencyFilter = FieldUtils.getFieldValue( - getFilter(ConcurrentSessionFilter.class), "sessionRegistry"); - Object sessionRegistryFromFormLoginFilter = FieldUtils.getFieldValue(getFilter(UsernamePasswordAuthenticationFilter),"sessionStrategy").delegateStrategies[0].sessionRegistry - Object sessionRegistryFromMgmtFilter = FieldUtils.getFieldValue(getFilter(SessionManagementFilter),"sessionAuthenticationStrategy").delegateStrategies[0].sessionRegistry - - assertSame(sessionRegistry, sessionRegistryFromConcurrencyFilter); - assertSame(sessionRegistry, sessionRegistryFromMgmtFilter); - // SEC-1143 - assertSame(sessionRegistry, sessionRegistryFromFormLoginFilter); - true; - } - - def concurrentSessionMaxSessionsIsCorrectlyConfigured() { - setup: - httpAutoConfig { - 'session-management'('session-authentication-error-url':'/max-exceeded') { - 'concurrency-control'('max-sessions': '2', 'error-if-maximum-exceeded':'true') - } - } - createAppContext(); - - def seshFilter = getFilter(SessionManagementFilter.class); - def auth = new UsernamePasswordAuthenticationToken("bob", "pass"); - SecurityContextHolder.getContext().setAuthentication(auth); - MockHttpServletResponse mockResponse = new MockHttpServletResponse(); - def response = new SaveContextOnUpdateOrErrorResponseWrapper(mockResponse, false) { - protected void saveContext(SecurityContext context) { - } - }; - when: "First session is established" - seshFilter.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()); - then: "ok" - mockResponse.redirectedUrl == null - when: "Second session is established" - seshFilter.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()); - then: "ok" - mockResponse.redirectedUrl == null - when: "Third session is established" - seshFilter.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()); - then: "Rejected" - mockResponse.redirectedUrl == "/max-exceeded"; - } - - def disablingSessionProtectionRemovesSessionManagementFilterIfNoInvalidSessionUrlSet() { - httpAutoConfig { - 'session-management'('session-fixation-protection': 'none') - csrf(disabled:true) - } - createAppContext() - - expect: - !(getFilters("/someurl").find { it instanceof SessionManagementFilter}) - } - - def 'session-fixation-protection=none'() { - setup: - MockHttpServletRequest request = new MockHttpServletRequest(method:'POST') - request.session.id = '123' - request.setParameter('username', 'user') - request.setParameter('password', 'password') - request.servletPath = '/login' - - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - httpAutoConfig { - 'session-management'('session-fixation-protection': 'none') - csrf(disabled:true) - } - createAppContext() - request.session.id = '123' - - when: - springSecurityFilterChain.doFilter(request,response, chain) - - then: - request.session.id == '123' - } - - def 'session-fixation-protection=migrateSession'() { - setup: - MockHttpServletRequest request = new MockHttpServletRequest(method:'POST') - request.setParameter('username', 'user') - request.setParameter('password', 'password') - request.servletPath = '/login' - - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - httpAutoConfig { - 'session-management'('session-fixation-protection': 'migrateSession') - csrf(disabled:true) - } - createAppContext() - String originalId = request.session.id - - when: - springSecurityFilterChain.doFilter(request,response, chain) - - then: - request.session.id != originalId - } - - def disablingSessionProtectionRetainsSessionManagementFilterInvalidSessionUrlSet() { - httpAutoConfig { - 'session-management'('session-fixation-protection': 'none', 'invalid-session-url': '/timeoutUrl') - csrf(disabled:true) - } - createAppContext() - def filter = getFilters("/someurl")[11] - - expect: - filter instanceof SessionManagementFilter - filter.invalidSessionStrategy.destinationUrl == '/timeoutUrl' - } - -} diff --git a/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTests.java b/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTests.java new file mode 100644 index 00000000000..0ff6d2edf1f --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTests.java @@ -0,0 +1,667 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.http; + +import java.io.IOException; +import java.security.Principal; +import java.util.List; +import javax.servlet.Filter; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +import org.apache.http.HttpStatus; +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.session.SessionRegistry; +import org.springframework.security.util.FieldUtils; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.authentication.RememberMeServices; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.security.web.authentication.session.SessionAuthenticationException; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; +import org.springframework.security.web.session.ConcurrentSessionFilter; +import org.springframework.security.web.session.SessionManagementFilter; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests session-related functionality for the <http> namespace element and <session-management> + * + * @author Luke Taylor + * @author Rob Winch + * @author Josh Cummings + */ +public class SessionManagementConfigTests { + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/SessionManagementConfigTests"; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + MockMvc mvc; + + @Test + public void requestWhenCreateSessionAlwaysThenAlwaysCreatesSession() + throws Exception { + this.spring.configLocations(this.xml("CreateSessionAlways")).autowire(); + + MockHttpServletRequest request = get("/").buildRequest(this.servletContext()); + MockHttpServletResponse response = request(request, this.spring.getContext()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK); + assertThat(request.getSession(false)).isNotNull(); + } + + @Test + public void requestWhenCreateSessionIsSetToNeverThenDoesNotCreateSessionOnLoginChallenge() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionNever")).autowire(); + + MockHttpServletRequest request = get("/auth").buildRequest(this.servletContext()); + MockHttpServletResponse response = request(request, this.spring.getContext()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY); + assertThat(request.getSession(false)).isNull(); + } + + @Test + public void requestWhenCreateSessionIsSetToNeverThenDoesNotCreateSessionOnLogin() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionNever")).autowire(); + + MockHttpServletRequest request = post("/login") + .param("username", "user") + .param("password", "password") + .buildRequest(this.servletContext()); + request = csrf().postProcessRequest(request); + MockHttpServletResponse response = request(request, this.spring.getContext()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY); + assertThat(request.getSession(false)).isNull(); + } + + @Test + public void requestWhenCreateSessionIsSetToNeverThenUsesExistingSession() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionNever")).autowire(); + + MockHttpServletRequest request = post("/login") + .param("username", "user") + .param("password", "password") + .buildRequest(this.servletContext()); + request = csrf().postProcessRequest(request); + MockHttpSession session = new MockHttpSession(); + request.setSession(session); + MockHttpServletResponse response = request(request, this.spring.getContext()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY); + assertThat(request.getSession(false)).isNotNull(); + assertThat(request.getSession(false).getAttribute(SPRING_SECURITY_CONTEXT_KEY)) + .isNotNull(); + } + + @Test + public void requestWhenCreateSessionIsSetToStatelessThenDoesNotCreateSessionOnLoginChallenge() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionStateless")).autowire(); + + this.mvc.perform(get("/auth")) + .andExpect(status().isFound()) + .andExpect(session().exists(false)); + } + + @Test + public void requestWhenCreateSessionIsSetToStatelessThenDoesNotCreateSessionOnLogin() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionStateless")).autowire(); + + + this.mvc.perform(post("/login") + .param("username", "user") + .param("password", "password") + .with(csrf())) + .andExpect(status().isFound()) + .andExpect(session().exists(false)); + } + + @Test + public void requestWhenCreateSessionIsSetToStatelessThenIgnoresExistingSession() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionStateless")).autowire(); + + MvcResult result = + this.mvc.perform(post("/login") + .param("username", "user") + .param("password", "password") + .session(new MockHttpSession()) + .with(csrf())) + .andExpect(status().isFound()) + .andExpect(session()) + .andReturn(); + + assertThat(result.getRequest().getSession(false).getAttribute(SPRING_SECURITY_CONTEXT_KEY)) + .isNull(); + } + + @Test + public void requestWhenCreateSessionIsSetToIfRequiredThenDoesNotCreateSessionOnPublicInvocation() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionIfRequired")).autowire(); + + ServletContext servletContext = this.mvc.getDispatcherServlet().getServletContext(); + MockHttpServletRequest request = get("/").buildRequest(servletContext); + MockHttpServletResponse response = request(request, this.spring.getContext()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK); + assertThat(request.getSession(false)).isNull(); + } + + @Test + public void requestWhenCreateSessionIsSetToIfRequiredThenCreatesSessionOnLoginChallenge() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionIfRequired")).autowire(); + + ServletContext servletContext = this.mvc.getDispatcherServlet().getServletContext(); + MockHttpServletRequest request = get("/auth").buildRequest(servletContext); + MockHttpServletResponse response = request(request, this.spring.getContext()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY); + assertThat(request.getSession(false)).isNotNull(); + } + + @Test + public void requestWhenCreateSessionIsSetToIfRequiredThenCreatesSessionOnLogin() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionIfRequired")).autowire(); + + ServletContext servletContext = this.mvc.getDispatcherServlet().getServletContext(); + MockHttpServletRequest request = post("/login") + .param("username", "user") + .param("password", "password") + .buildRequest(servletContext); + request = csrf().postProcessRequest(request); + MockHttpServletResponse response = request(request, this.spring.getContext()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY); + assertThat(request.getSession(false)).isNotNull(); + } + + /** + * SEC-1208 + */ + @Test + public void requestWhenRejectingUserBasedOnMaxSessionsExceededThenDoesNotCreateSession() + throws Exception { + + this.spring.configLocations(this.xml("Sec1208")).autowire(); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()) + .andExpect(session()); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(status().isUnauthorized()) + .andExpect(session().exists(false)); + } + + /** + * SEC-2137 + */ + @Test + public void requestWhenSessionFixationProtectionDisabledAndConcurrencyControlEnabledThenSessionNotInvalidated() + throws Exception { + + this.spring.configLocations(this.xml("Sec2137")).autowire(); + + MockHttpSession session = new MockHttpSession(); + this.mvc.perform(get("/auth") + .session(session) + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()) + .andExpect(session().id(session.getId())); + } + + @Test + public void autowireWhenExportingSessionRegistryBeanThenAvailableForWiring() { + this.spring.configLocations(this.xml("ConcurrencyControlSessionRegistryAlias")).autowire(); + + this.sessionRegistryIsValid(); + } + + @Test + public void requestWhenExpiredUrlIsSetThenInvalidatesSessionAndRedirects() + throws Exception { + + this.spring.configLocations(this.xml("ConcurrencyControlExpiredUrl")).autowire(); + + this.mvc.perform(get("/auth") + .session(this.expiredSession()) + .with(httpBasic("user", "password"))) + .andExpect(redirectedUrl("/expired")) + .andExpect(session().exists(false)); + } + + @Test + public void requestWhenConcurrencyControlAndCustomLogoutHandlersAreSetThenAllAreInvokedWhenSessionExpires() + throws Exception { + + this.spring.configLocations(this.xml("ConcurrencyControlLogoutAndRememberMeHandlers")).autowire(); + + this.mvc.perform(get("/auth") + .session(this.expiredSession()) + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()) + .andExpect(cookie().maxAge("testCookie", 0)) + .andExpect(cookie().exists("rememberMeCookie")) + .andExpect(session().valid(true)); + } + + @Test + public void requestWhenConcurrencyControlAndRememberMeAreSetThenInvokedWhenSessionExpires() + throws Exception { + + this.spring.configLocations(this.xml("ConcurrencyControlRememberMeHandler")).autowire(); + + this.mvc.perform(get("/auth") + .session(this.expiredSession()) + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()) + .andExpect(cookie().exists("rememberMeCookie")) + .andExpect(session().exists(false)); + } + + /** + * SEC-2057 + */ + @Test + public void autowireWhenConcurrencyControlIsSetThenLogoutHandlersGetAuthenticationObject() + throws Exception { + + this.spring.configLocations(this.xml("ConcurrencyControlCustomLogoutHandler")).autowire(); + + MvcResult result = + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(session()) + .andReturn(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); + + SessionRegistry sessionRegistry = this.spring.getContext().getBean(SessionRegistry.class); + sessionRegistry.getSessionInformation(session.getId()).expireNow(); + + this.mvc.perform(get("/auth") + .session(session)) + .andExpect(header().string("X-Username", "user")); + } + + @Test + public void requestWhenConcurrencyControlIsSetThenDefaultsToResponseBodyExpirationResponse() + throws Exception { + + this.spring.configLocations(this.xml("ConcurrencyControlSessionRegistryAlias")).autowire(); + + this.mvc.perform(get("/auth") + .session(this.expiredSession()) + .with(httpBasic("user", "password"))) + .andExpect(content().string("This session has been expired (possibly due to multiple concurrent " + + "logins being attempted as the same user).")); + } + + @Test + public void requestWhenCustomSessionAuthenticationStrategyThenInvokesOnAuthentication() + throws Exception { + + this.spring.configLocations(this.xml("SessionAuthenticationStrategyRef")).autowire(); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(status().isIAmATeapot()); + } + + @Test + public void autowireWhenSessionRegistryRefIsSetThenAvailableForWiring() { + this.spring.configLocations(this.xml("ConcurrencyControlSessionRegistryRef")).autowire(); + + this.sessionRegistryIsValid(); + } + + @Test + public void requestWhenMaxSessionsIsSetThenErrorsWhenExceeded() + throws Exception { + + this.spring.configLocations(this.xml("ConcurrencyControlMaxSessions")).autowire(); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(redirectedUrl("/max-exceeded")); + } + + @Test + public void autowireWhenSessionFixationProtectionIsNoneAndCsrfDisabledThenSessionManagementFilterIsNotWired() { + + this.spring.configLocations(this.xml("NoSessionManagementFilter")).autowire(); + + assertThat(this.getFilter(SessionManagementFilter.class)).isNull(); + } + + @Test + public void requestWhenSessionFixationProtectionIsNoneThenSessionNotInvalidated() + throws Exception { + + this.spring.configLocations(this.xml("SessionFixationProtectionNone")).autowire(); + + MockHttpSession session = new MockHttpSession(); + String sessionId = session.getId(); + + this.mvc.perform(get("/auth") + .session(session) + .with(httpBasic("user", "password"))) + .andExpect(session().id(sessionId)); + } + + @Test + public void requestWhenSessionFixationProtectionIsMigrateSessionThenSessionIsReplaced() + throws Exception { + + this.spring.configLocations(this.xml("SessionFixationProtectionMigrateSession")).autowire(); + + MockHttpSession session = new MockHttpSession(); + String sessionId = session.getId(); + + MvcResult result = + this.mvc.perform(get("/auth") + .session(session) + .with(httpBasic("user", "password"))) + .andExpect(session()) + .andReturn(); + + assertThat(result.getRequest().getSession(false).getId()).isNotEqualTo(sessionId); + } + + @Test + public void requestWhenSessionFixationProtectionIsNoneAndInvalidSessionUrlIsSetThenStillRedirectsOnInvalidSession() + throws Exception { + + this.spring.configLocations(this.xml("SessionFixationProtectionNoneWithInvalidSessionUrl")).autowire(); + + this.mvc.perform(get("/auth") + .with(request -> { + request.setRequestedSessionId("1"); + request.setRequestedSessionIdValid(false); + return request; + })) + .andExpect(redirectedUrl("/timeoutUrl")); + } + + static class TeapotSessionAuthenticationStrategy implements SessionAuthenticationStrategy { + + @Override + public void onAuthentication( + Authentication authentication, + HttpServletRequest request, + HttpServletResponse response) throws SessionAuthenticationException { + + response.setStatus(org.springframework.http.HttpStatus.I_AM_A_TEAPOT.value()); + } + } + + static class CustomRememberMeServices implements RememberMeServices, LogoutHandler { + @Override + public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) { + return null; + } + + @Override + public void loginFail(HttpServletRequest request, HttpServletResponse response) { + + } + + @Override + public void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { + + } + + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + response.addHeader("X-Username", authentication.getName()); + } + } + + @RestController + static class BasicController { + @GetMapping("/") + public String ok() { + return "ok"; + } + + @GetMapping("/auth") + public String auth(Principal principal) { + return principal.getName(); + } + } + + private void sessionRegistryIsValid() { + SessionRegistry sessionRegistry = this.spring.getContext() + .getBean("sessionRegistry", SessionRegistry.class); + + assertThat(sessionRegistry).isNotNull(); + + assertThat(this.getFilter(ConcurrentSessionFilter.class)) + .returns(sessionRegistry, this::extractSessionRegistry); + assertThat(this.getFilter(UsernamePasswordAuthenticationFilter.class)) + .returns(sessionRegistry, this::extractSessionRegistry); + // SEC-1143 + assertThat(this.getFilter(SessionManagementFilter.class)) + .returns(sessionRegistry, this::extractSessionRegistry); + } + + private SessionRegistry extractSessionRegistry(ConcurrentSessionFilter filter) { + return getFieldValue(filter, "sessionRegistry"); + } + + private SessionRegistry extractSessionRegistry(UsernamePasswordAuthenticationFilter filter) { + SessionAuthenticationStrategy strategy = getFieldValue(filter, "sessionStrategy"); + List strategies = getFieldValue(strategy, "delegateStrategies"); + return getFieldValue(strategies.get(0), "sessionRegistry"); + } + + private SessionRegistry extractSessionRegistry(SessionManagementFilter filter) { + SessionAuthenticationStrategy strategy = getFieldValue(filter, "sessionAuthenticationStrategy"); + List strategies = getFieldValue(strategy, "delegateStrategies"); + return getFieldValue(strategies.get(0), "sessionRegistry"); + } + + private T getFieldValue(Object target, String fieldName) { + try { + return (T) FieldUtils.getFieldValue(target, fieldName); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static SessionResultMatcher session() { + return new SessionResultMatcher(); + } + + private static class SessionResultMatcher implements ResultMatcher { + private String id; + private Boolean valid; + private Boolean exists = true; + + public ResultMatcher exists(boolean exists) { + this.exists = exists; + return this; + } + + public ResultMatcher valid(boolean valid) { + this.valid = valid; + return this.exists(true); + } + + public ResultMatcher id(String id) { + this.id = id; + return this.exists(true); + } + + @Override + public void match(MvcResult result) { + if (!this.exists) { + assertThat(result.getRequest().getSession(false)).isNull(); + return; + } + + assertThat(result.getRequest().getSession(false)).isNotNull(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); + + if (this.valid != null) { + if (this.valid) { + assertThat(session.isInvalid()).isFalse(); + } else { + assertThat(session.isInvalid()).isTrue(); + } + } + + if (this.id != null) { + assertThat(session.getId()).isEqualTo(this.id); + } + } + } + + private static MockHttpServletResponse request( + MockHttpServletRequest request, + ApplicationContext context) + throws IOException, ServletException { + + MockHttpServletResponse response = new MockHttpServletResponse(); + + FilterChainProxy proxy = context.getBean(FilterChainProxy.class); + + proxy.doFilter( + request, + new EncodeUrlDenyingHttpServletResponseWrapper(response), + (req, resp) -> {}); + + return response; + } + + private static class EncodeUrlDenyingHttpServletResponseWrapper + extends HttpServletResponseWrapper { + + public EncodeUrlDenyingHttpServletResponseWrapper(HttpServletResponse response) { + super(response); + } + + @Override + public String encodeURL(String url) { + throw new RuntimeException("Unexpected invocation of encodeURL"); + } + + @Override + public String encodeRedirectURL(String url) { + throw new RuntimeException("Unexpected invocation of encodeURL"); + } + + @Override + public String encodeUrl(String url) { + throw new RuntimeException("Unexpected invocation of encodeURL"); + } + + @Override + public String encodeRedirectUrl(String url) { + throw new RuntimeException("Unexpected invocation of encodeURL"); + } + } + + private MockHttpSession expiredSession() { + MockHttpSession session = new MockHttpSession(); + SessionRegistry sessionRegistry = this.spring.getContext().getBean(SessionRegistry.class); + sessionRegistry.registerNewSession(session.getId(), "user"); + sessionRegistry.getSessionInformation(session.getId()).expireNow(); + return session; + } + + private T getFilter(Class filterClass) { + return (T) getFilters().stream() + .filter(filterClass::isInstance) + .findFirst() + .orElse(null); + } + + private List getFilters() { + FilterChainProxy proxy = this.spring.getContext().getBean(FilterChainProxy.class); + + return proxy.getFilters("/"); + } + + private ServletContext servletContext() { + WebApplicationContext context = (WebApplicationContext) this.spring.getContext(); + return context.getServletContext(); + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlCustomLogoutHandler.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlCustomLogoutHandler.xml new file mode 100644 index 00000000000..a95022eeac6 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlCustomLogoutHandler.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlExpiredUrl.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlExpiredUrl.xml new file mode 100644 index 00000000000..38141dfc7f0 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlExpiredUrl.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlLogoutAndRememberMeHandlers.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlLogoutAndRememberMeHandlers.xml new file mode 100644 index 00000000000..6f75ce1d2f6 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlLogoutAndRememberMeHandlers.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlMaxSessions.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlMaxSessions.xml new file mode 100644 index 00000000000..f16c2da2f87 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlMaxSessions.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlRememberMeHandler.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlRememberMeHandler.xml new file mode 100644 index 00000000000..eec4a40f9b1 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlRememberMeHandler.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlSessionRegistryAlias.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlSessionRegistryAlias.xml new file mode 100644 index 00000000000..13baba1949e --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlSessionRegistryAlias.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlSessionRegistryRef.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlSessionRegistryRef.xml new file mode 100644 index 00000000000..057f5985c0c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlSessionRegistryRef.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionAlways.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionAlways.xml new file mode 100644 index 00000000000..049c3c47508 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionAlways.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionIfRequired.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionIfRequired.xml new file mode 100644 index 00000000000..47a770d00e8 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionIfRequired.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionNever.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionNever.xml new file mode 100644 index 00000000000..0edce4d62ab --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionNever.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionStateless.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionStateless.xml new file mode 100644 index 00000000000..b252e1f8f2c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionStateless.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-NoSessionManagementFilter.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-NoSessionManagementFilter.xml new file mode 100644 index 00000000000..84944d46753 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-NoSessionManagementFilter.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-Sec1208.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-Sec1208.xml new file mode 100644 index 00000000000..afdedc05200 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-Sec1208.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-Sec2137.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-Sec2137.xml new file mode 100644 index 00000000000..10fd3e2d5b1 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-Sec2137.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionAuthenticationStrategyRef.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionAuthenticationStrategyRef.xml new file mode 100644 index 00000000000..04c2d7a0395 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionAuthenticationStrategyRef.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionMigrateSession.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionMigrateSession.xml new file mode 100644 index 00000000000..e0cb0476823 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionMigrateSession.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionNone.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionNone.xml new file mode 100644 index 00000000000..217daddcb7f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionNone.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionNoneWithInvalidSessionUrl.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionNoneWithInvalidSessionUrl.xml new file mode 100644 index 00000000000..807cea0710d --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionNoneWithInvalidSessionUrl.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + From 2ecfa67b0c1a424583824e875c14c7a79449d3a0 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 17 Jul 2018 22:13:33 -0500 Subject: [PATCH 136/226] Cache Control disabled for 304 Fixes: gh-5534 --- .../web/header/writers/CacheControlHeadersWriter.java | 3 ++- .../header/CacheControlServerHttpHeadersWriter.java | 4 ++++ .../writers/CacheControlHeadersWriterTests.java | 11 +++++++++++ .../CacheControlServerHttpHeadersWriterTests.java | 11 +++++++++++ 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/web/src/main/java/org/springframework/security/web/header/writers/CacheControlHeadersWriter.java b/web/src/main/java/org/springframework/security/web/header/writers/CacheControlHeadersWriter.java index 9355ae6b987..9500e38a7ca 100644 --- a/web/src/main/java/org/springframework/security/web/header/writers/CacheControlHeadersWriter.java +++ b/web/src/main/java/org/springframework/security/web/header/writers/CacheControlHeadersWriter.java @@ -22,6 +22,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; import org.springframework.security.web.header.Header; import org.springframework.security.web.header.HeaderWriter; import org.springframework.util.ReflectionUtils; @@ -59,7 +60,7 @@ public CacheControlHeadersWriter() { @Override public void writeHeaders(HttpServletRequest request, HttpServletResponse response) { if (hasHeader(response, CACHE_CONTROL) || hasHeader(response, EXPIRES) - || hasHeader(response, PRAGMA)) { + || hasHeader(response, PRAGMA) || response.getStatus() == HttpStatus.NOT_MODIFIED.value()) { return; } this.delegate.writeHeaders(request, response); diff --git a/web/src/main/java/org/springframework/security/web/server/header/CacheControlServerHttpHeadersWriter.java b/web/src/main/java/org/springframework/security/web/server/header/CacheControlServerHttpHeadersWriter.java index ac6b0949800..aae78795db0 100644 --- a/web/src/main/java/org/springframework/security/web/server/header/CacheControlServerHttpHeadersWriter.java +++ b/web/src/main/java/org/springframework/security/web/server/header/CacheControlServerHttpHeadersWriter.java @@ -16,6 +16,7 @@ package org.springframework.security.web.server.header; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @@ -61,6 +62,9 @@ public class CacheControlServerHttpHeadersWriter implements ServerHttpHeadersWri @Override public Mono writeHttpHeaders(ServerWebExchange exchange) { + if (exchange.getResponse().getStatusCode() == HttpStatus.NOT_MODIFIED) { + return Mono.empty(); + } return CACHE_HEADERS.writeHttpHeaders(exchange); } diff --git a/web/src/test/java/org/springframework/security/web/header/writers/CacheControlHeadersWriterTests.java b/web/src/test/java/org/springframework/security/web/header/writers/CacheControlHeadersWriterTests.java index a4e1b1bb42c..f774d3b3f08 100644 --- a/web/src/test/java/org/springframework/security/web/header/writers/CacheControlHeadersWriterTests.java +++ b/web/src/test/java/org/springframework/security/web/header/writers/CacheControlHeadersWriterTests.java @@ -23,6 +23,7 @@ import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest; import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.util.ReflectionUtils; @@ -124,4 +125,14 @@ public void writeHeadersDisabledIfExpires() { assertThat(this.response.getHeaderValue("Cache-Control")).isNull(); assertThat(this.response.getHeaderValue("Pragma")).isNull(); } + + @Test + // gh-5534 + public void writeHeadersDisabledIfNotModified() { + this.response.setStatus(HttpStatus.NOT_MODIFIED.value()); + + this.writer.writeHeaders(this.request, this.response); + + assertThat(this.response.getHeaderNames()).isEmpty(); + } } diff --git a/web/src/test/java/org/springframework/security/web/server/header/CacheControlServerHttpHeadersWriterTests.java b/web/src/test/java/org/springframework/security/web/server/header/CacheControlServerHttpHeadersWriterTests.java index 07248475da5..57f1d3c5727 100644 --- a/web/src/test/java/org/springframework/security/web/server/header/CacheControlServerHttpHeadersWriterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/header/CacheControlServerHttpHeadersWriterTests.java @@ -19,6 +19,7 @@ import org.junit.Test; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.web.server.ServerWebExchange; @@ -83,4 +84,14 @@ public void writeHeadersWhenExpiresThenNoCacheControlHeaders() { assertThat(headers.get(HttpHeaders.EXPIRES)).containsOnly(expires); } + @Test + // gh-5534 + public void writeHeadersWhenNotModifiedThenNoCacheControlHeaders() { + exchange.getResponse().setStatusCode(HttpStatus.NOT_MODIFIED); + + writer.writeHttpHeaders(exchange); + + assertThat(headers).isEmpty(); + } + } From 826e298c2fc78d14f6b7ebf88c6ac89fc0b6ff8d Mon Sep 17 00:00:00 2001 From: "mhyeon.lee" Date: Thu, 12 Jul 2018 17:46:22 +0900 Subject: [PATCH 137/226] Polish Javadoc to remove warning --- .../AbstractConfiguredSecurityBuilder.java | 14 +++++++------- .../annotation/SecurityConfigurerAdapter.java | 2 +- .../builders/AuthenticationManagerBuilder.java | 2 +- .../LdapAuthenticationProviderConfigurer.java | 8 ++++---- .../JdbcUserDetailsManagerConfigurer.java | 4 ++-- .../UserDetailsManagerConfigurer.java | 6 +++--- .../AbstractDaoAuthenticationConfigurer.java | 2 +- .../GlobalMethodSecurityConfiguration.java | 16 ++++++++-------- .../annotation/web/builders/HttpSecurity.java | 10 +++++----- .../annotation/web/builders/WebSecurity.java | 4 ++-- .../configuration/WebSecurityConfiguration.java | 2 +- .../WebSecurityConfigurerAdapter.java | 6 +++--- .../AbstractAuthenticationFilterConfigurer.java | 4 ++-- .../configurers/ChannelSecurityConfigurer.java | 4 ++-- .../web/configurers/HeadersConfigurer.java | 2 +- .../web/configurers/PortMapperConfigurer.java | 2 +- .../configurers/UrlAuthorizationConfigurer.java | 4 ++-- .../openid/OpenIDLoginConfigurer.java | 2 +- ...iveUserDetailsServiceResourceFactoryBean.java | 1 - ...essageBrokerSecurityBeanDefinitionParser.java | 2 +- .../server/SecurityMockServerConfigurers.java | 2 +- .../request/SecurityMockMvcRequestBuilders.java | 2 +- 22 files changed, 50 insertions(+), 51 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java index 29d64aee426..1e71774a867 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java +++ b/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java @@ -122,7 +122,7 @@ public O getOrBuild() { * invokes {@link SecurityConfigurerAdapter#setBuilder(SecurityBuilder)}. * * @param configurer - * @return + * @return the {@link SecurityConfigurerAdapter} for further customizations * @throws Exception */ @SuppressWarnings("unchecked") @@ -140,7 +140,7 @@ public > C apply(C configurer) * are not considered. * * @param configurer - * @return + * @return the {@link SecurityConfigurerAdapter} for further customizations * @throws Exception */ public > C apply(C configurer) throws Exception { @@ -172,7 +172,7 @@ public C getSharedObject(Class sharedType) { /** * Gets the shared objects - * @return + * @return the shared Objects */ public Map, Object> getSharedObjects() { return Collections.unmodifiableMap(this.sharedObjects); @@ -214,7 +214,7 @@ private > void add(C configurer) throws Excep * List if not found. Note that object hierarchies are not considered. * * @param clazz the {@link SecurityConfigurer} class to look for - * @return + * @return a list of {@link SecurityConfigurer}s for further customization */ @SuppressWarnings("unchecked") public > List getConfigurers(Class clazz) { @@ -230,7 +230,7 @@ public > List getConfigurers(Class claz * List if not found. Note that object hierarchies are not considered. * * @param clazz the {@link SecurityConfigurer} class to look for - * @return + * @return a list of {@link SecurityConfigurer}s for further customization */ @SuppressWarnings("unchecked") public > List removeConfigurers(Class clazz) { @@ -246,7 +246,7 @@ public > List removeConfigurers(Class c * found. Note that object hierarchies are not considered. * * @param clazz - * @return + * @return the {@link SecurityConfigurer} for further customizations */ @SuppressWarnings("unchecked") public > C getConfigurer(Class clazz) { @@ -359,7 +359,7 @@ protected void beforeConfigure() throws Exception { /** * Subclasses must implement this method to build the object that is being returned. * - * @return + * @return the Object to be buit or null if the implementation allows it */ protected abstract O performBuild() throws Exception; diff --git a/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurerAdapter.java b/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurerAdapter.java index b36d406a402..9e519149ed4 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurerAdapter.java +++ b/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurerAdapter.java @@ -51,7 +51,7 @@ public void configure(B builder) throws Exception { * Return the {@link SecurityBuilder} when done using the {@link SecurityConfigurer}. * This is useful for method chaining. * - * @return + * @return the {@link SecurityBuilder} for further customizations */ public B and() { return getBuilder(); diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java index 52f23fd3eb6..aaffc81009f 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java @@ -258,7 +258,7 @@ protected ProviderManager performBuild() throws Exception { * default configuration in the {@link SecurityConfigurer#configure(SecurityBuilder)} * method. * - * @return + * @return true, if {@link AuthenticationManagerBuilder} is configured, otherwise false */ public boolean isConfigured() { return !authenticationProviders.isEmpty() || parentAuthenticationManager != null; diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java index 4b9725abdb1..fde27dad55f 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java @@ -302,7 +302,7 @@ public LdapAuthenticationProviderConfigurer userDetailsContextMapper( /** * Specifies the attribute name which contains the role name. Default is "cn". * @param groupRoleAttribute the attribute name that maps a group to a role. - * @return + * @return the {@link LdapAuthenticationProviderConfigurer} for further customizations */ public LdapAuthenticationProviderConfigurer groupRoleAttribute( String groupRoleAttribute) { @@ -384,11 +384,11 @@ public void configure(B builder) throws Exception { */ public final class PasswordCompareConfigurer { - /** + /**Us * Allows specifying the {@link PasswordEncoder} to use. The default is * {@link org.springframework.security.crypto.password.NoOpPasswordEncoder}. * @param passwordEncoder the {@link PasswordEncoder} to use - * @return the {@link PasswordEncoder} to use + * @return the {@link PasswordCompareConfigurer} for further customizations */ public PasswordCompareConfigurer passwordEncoder(PasswordEncoder passwordEncoder) { LdapAuthenticationProviderConfigurer.this.passwordEncoder = passwordEncoder; @@ -602,7 +602,7 @@ private BaseLdapPathContextSource getContextSource() throws Exception { } /** - * @return + * @return the {@link PasswordCompareConfigurer} for further customizations */ public PasswordCompareConfigurer passwordCompare() { return new PasswordCompareConfigurer().passwordAttribute("password") diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/JdbcUserDetailsManagerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/JdbcUserDetailsManagerConfigurer.java index 3f5ea1f9695..7d2ae974023 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/JdbcUserDetailsManagerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/JdbcUserDetailsManagerConfigurer.java @@ -63,7 +63,7 @@ public JdbcUserDetailsManagerConfigurer() { * Populates the {@link DataSource} to be used. This is the only required attribute. * * @param dataSource the {@link DataSource} to be used. Cannot be null. - * @return + * @return The {@link JdbcUserDetailsManagerConfigurer} used for additional customizations * @throws Exception */ public JdbcUserDetailsManagerConfigurer dataSource(DataSource dataSource) @@ -142,7 +142,7 @@ public JdbcUserDetailsManagerConfigurer groupAuthoritiesByUsername(String que * storage (default is ""). * * @param rolePrefix - * @return + * @return The {@link JdbcUserDetailsManagerConfigurer} used for additional customizations * @throws Exception */ public JdbcUserDetailsManagerConfigurer rolePrefix(String rolePrefix) diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/UserDetailsManagerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/UserDetailsManagerConfigurer.java index 6dfd328c951..961a0b3cf96 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/UserDetailsManagerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/UserDetailsManagerConfigurer.java @@ -69,7 +69,7 @@ protected void initUserDetailsService() throws Exception { * method can be invoked multiple times to add multiple users. * * @param userDetails the user to add. Cannot be null. - * @return + * @return the {@link UserDetailsBuilder} for further customizations */ @SuppressWarnings("unchecked") public final C withUser(UserDetails userDetails) { @@ -82,7 +82,7 @@ public final C withUser(UserDetails userDetails) { * method can be invoked multiple times to add multiple users. * * @param userBuilder the user to add. Cannot be null. - * @return + * @return the {@link UserDetailsBuilder} for further customizations */ @SuppressWarnings("unchecked") public final C withUser(User.UserBuilder userBuilder) { @@ -95,7 +95,7 @@ public final C withUser(User.UserBuilder userBuilder) { * method can be invoked multiple times to add multiple users. * * @param username the username for the user being added. Cannot be null. - * @return + * @return the {@link UserDetailsBuilder} for further customizations */ @SuppressWarnings("unchecked") public final UserDetailsBuilder withUser(String username) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java index 52cf21c17cf..e58f6ce06ab 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java @@ -69,7 +69,7 @@ public C withObjectPostProcessor(ObjectPostProcessor objectPostProcessor) { * {@link DaoAuthenticationProvider}. The default is to use plain text. * * @param passwordEncoder The {@link PasswordEncoder} to use. - * @return + * @return the {@link AbstractDaoAuthenticationConfigurer} for further customizations */ @SuppressWarnings("unchecked") public C passwordEncoder(PasswordEncoder passwordEncoder) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java index a2aa8ac794a..83e0e8e35c3 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java @@ -118,7 +118,7 @@ public T postProcess(T object) { * {@link MethodInterceptor}. *

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

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

    * - * @return + * @return the {@link MethodSecurityExpressionHandler} to use */ protected MethodSecurityExpressionHandler createExpressionHandler() { return defaultMethodExpressionHandler; @@ -307,7 +307,7 @@ protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() { * {@link #configure(AuthenticationManagerBuilder)} was not overridden, then an * {@link AuthenticationManager} is attempted to be autowired by type. * - * @return + * @return the {@link AuthenticationManager} to use */ protected AuthenticationManager authenticationManager() throws Exception { if (authenticationManager == null) { @@ -346,7 +346,7 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { * {@link #customMethodSecurityMetadataSource()} and the attributes on * {@link EnableGlobalMethodSecurity}. * - * @return + * @return the {@link MethodSecurityMetadataSource} */ @Bean public MethodSecurityMetadataSource methodSecurityMetadataSource() { @@ -379,7 +379,7 @@ public MethodSecurityMetadataSource methodSecurityMetadataSource() { * Creates the {@link PreInvocationAuthorizationAdvice} to be used. The default is * {@link ExpressionBasedPreInvocationAdvice}. * - * @return + * @return the {@link PreInvocationAuthorizationAdvice} */ @Bean public PreInvocationAuthorizationAdvice preInvocationAuthorizationAdvice() { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index e46c5202e47..1d41bdd3fbe 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -327,7 +327,7 @@ public OpenIDLoginConfigurer openidLogin() throws Exception { * } * * - * @return + * @return the {@link HeadersConfigurer} for further customizations * @throws Exception * @see HeadersConfigurer */ @@ -643,7 +643,7 @@ public RememberMeConfigurer rememberMe() throws Exception { * * @see #requestMatcher(RequestMatcher) * - * @return + * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customizations * @throws Exception */ public ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry authorizeRequests() @@ -763,7 +763,7 @@ public CsrfConfigurer csrf() throws Exception { * } * * - * @return + * @return the {@link LogoutConfigurer} for further customizations * @throws Exception */ public LogoutConfigurer logout() throws Exception { @@ -826,7 +826,7 @@ public LogoutConfigurer logout() throws Exception { * } * * - * @return + * @return the {@link AnonymousConfigurer} for further customizations * @throws Exception */ public AnonymousConfigurer anonymous() throws Exception { @@ -890,7 +890,7 @@ public AnonymousConfigurer anonymous() throws Exception { * * @see FormLoginConfigurer#loginPage(String) * - * @return + * @return the {@link FormLoginConfigurer} for further customizations * @throws Exception */ public FormLoginConfigurer formLogin() throws Exception { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java index 4f9d94f3ac0..85e6c1fb503 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java @@ -235,7 +235,7 @@ public WebSecurity expressionHandler( /** * Gets the {@link SecurityExpressionHandler} to be used. - * @return + * @return the {@link SecurityExpressionHandler} for further customizations */ public SecurityExpressionHandler getExpressionHandler() { return expressionHandler; @@ -243,7 +243,7 @@ public SecurityExpressionHandler getExpressionHandler() { /** * Gets the {@link WebInvocationPrivilegeEvaluator} to be used. - * @return + * @return the {@link WebInvocationPrivilegeEvaluator} for further customizations */ public WebInvocationPrivilegeEvaluator getPrivilegeEvaluator() { if (privilegeEvaluator != null) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java index f3caec0faa7..4e0f8a3f3ae 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java @@ -88,7 +88,7 @@ public SecurityExpressionHandler webSecurityExpressionHandler( /** * Creates the Spring Security Filter Chain - * @return + * @return the {@link Filter} that represents the security filter chain * @throws Exception */ @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java index d8c14ff9ccb..0377e53a321 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java @@ -257,7 +257,7 @@ public AuthenticationManager authenticationManagerBean() throws Exception { * {@link AuthenticationManagerBuilder} that was passed in. Otherwise, autowire the * {@link AuthenticationManager} by type. * - * @return + * @return the {@link AuthenticationManager} to use * @throws Exception */ protected AuthenticationManager authenticationManager() throws Exception { @@ -291,7 +291,7 @@ protected AuthenticationManager authenticationManager() throws Exception { * * To change the instance returned, developers should change * {@link #userDetailsService()} instead - * @return + * @return the {@link UserDetailsService} * @throws Exception * @see #userDetailsService() */ @@ -308,7 +308,7 @@ public UserDetailsService userDetailsServiceBean() throws Exception { * {@link ApplicationContext}. Developers should override this method when changing * the instance of {@link #userDetailsServiceBean()}. * - * @return + * @return the {@link UserDetailsService} to use */ protected UserDetailsService userDetailsService() { AuthenticationManagerBuilder globalAuthBuilder = context diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java index 0bd4c1fc611..32f861983dc 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java @@ -181,7 +181,7 @@ public final T successHandler(AuthenticationSuccessHandler successHandler) { /** * Equivalent of invoking permitAll(true) - * @return + * @return the {@link FormLoginConfigurer} for additional customization */ public final T permitAll() { return permitAll(true); @@ -364,7 +364,7 @@ protected final String getLoginProcessingUrl() { /** * Gets the URL to send users to if authentication fails * - * @return + * @return the URL to send users if authentication fails (e.g. "/login?error"). */ protected final String getFailureUrl() { return failureUrl; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java index db0a23a7ac9..80eb1d2fda9 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java @@ -188,7 +188,7 @@ public ChannelRequestMatcherRegistry withObjectPostProcessor( * Sets the {@link ChannelProcessor} instances to use in * {@link ChannelDecisionManagerImpl} * @param channelProcessors - * @return + * @return the {@link ChannelSecurityConfigurer} for further customizations */ public ChannelRequestMatcherRegistry channelProcessors( List channelProcessors) { @@ -200,7 +200,7 @@ public ChannelRequestMatcherRegistry channelProcessors( * Return the {@link SecurityBuilder} when done using the * {@link SecurityConfigurer}. This is useful for method chaining. * - * @return + * @return the type of {@link HttpSecurityBuilder} that is being configured */ public H and() { return ChannelSecurityConfigurer.this.and(); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java index 7af9c9c441c..992529ab57a 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java @@ -442,7 +442,7 @@ public HeadersConfigurer deny() { * application. *

    * - * @return + * @return the {@link HeadersConfigurer} for additional customization. */ public HeadersConfigurer sameOrigin() { writer = new XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PortMapperConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PortMapperConfigurer.java index 262bb1436c7..0a64f0e5ab8 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PortMapperConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PortMapperConfigurer.java @@ -45,7 +45,7 @@ public PortMapperConfigurer() { /** * Allows specifying the {@link PortMapper} instance. * @param portMapper - * @return + * @return the {@link PortMapperConfigurer} for further customizations */ public PortMapperConfigurer portMapper(PortMapper portMapper) { this.portMapper = portMapper; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java index edd17b07119..88ac8eb547d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java @@ -99,7 +99,7 @@ public UrlAuthorizationConfigurer(ApplicationContext context) { * The StandardInterceptUrlRegistry is what users will interact with after applying * the {@link UrlAuthorizationConfigurer}. * - * @return + * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customizations */ public StandardInterceptUrlRegistry getRegistry() { return REGISTRY; @@ -198,7 +198,7 @@ FilterInvocationSecurityMetadataSource createMetadataSource(H http) { * provided {@link ConfigAttribute} instances * @param configAttributes the {@link ConfigAttribute} instances that should be mapped * by the {@link RequestMatcher} instances - * @return the {@link UrlAuthorizationConfigurer} for further customizations + * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customizations */ private StandardInterceptUrlRegistry addMapping( Iterable requestMatchers, diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java index a1c8ae02253..e29ccdee920 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java @@ -479,7 +479,7 @@ public AttributeConfigurer required(boolean required) { /** * The OpenID attribute type. * @param type - * @return + * @return the {@link AttributeConfigurer} for further customizations */ public AttributeConfigurer type(String type) { this.type = type; diff --git a/config/src/main/java/org/springframework/security/config/core/userdetails/ReactiveUserDetailsServiceResourceFactoryBean.java b/config/src/main/java/org/springframework/security/config/core/userdetails/ReactiveUserDetailsServiceResourceFactoryBean.java index e932cc17c15..2f74d105a3d 100644 --- a/config/src/main/java/org/springframework/security/config/core/userdetails/ReactiveUserDetailsServiceResourceFactoryBean.java +++ b/config/src/main/java/org/springframework/security/config/core/userdetails/ReactiveUserDetailsServiceResourceFactoryBean.java @@ -57,7 +57,6 @@ public void setResourceLoader(ResourceLoader resourceLoader) { * Sets the location of a Resource that is a Properties file in the format defined in {@link UserDetailsResourceFactoryBean}. * * @param resourceLocation the location of the properties file that contains the users (i.e. "classpath:users.properties") - * @return the UserDetailsResourceFactoryBean */ public void setResourceLocation(String resourceLocation) { this.userDetails.setResourceLocation(resourceLocation); diff --git a/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java index afa29975af6..8165cca7e35 100644 --- a/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java @@ -110,7 +110,7 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements /** * @param element * @param parserContext - * @return + * @return the {@link BeanDefinition} */ public BeanDefinition parse(Element element, ParserContext parserContext) { BeanDefinitionRegistry registry = parserContext.getRegistry(); diff --git a/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java b/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java index 4e4951e74d8..2d2d73276a7 100644 --- a/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java +++ b/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java @@ -139,7 +139,7 @@ private CsrfMutator() {} } /** - * Updates the WebServerExchange using {@code {@link SecurityMockServerConfigurers#mockUser(UserDetails)}. Defaults to use a + * Updates the WebServerExchange using {@code {@link SecurityMockServerConfigurers#mockUser(UserDetails)}}. Defaults to use a * password of "password" and granted authorities of "ROLE_USER". */ public static class UserExchangeMutator implements WebTestClientConfigurer, MockServerConfigurer { diff --git a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestBuilders.java b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestBuilders.java index 62e54519968..b2a924ccc9c 100644 --- a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestBuilders.java +++ b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestBuilders.java @@ -139,7 +139,7 @@ public MockHttpServletRequest buildRequest(ServletContext servletContext) { * Specifies the URL to POST to. Default is "/login" * * @param loginProcessingUrl the URL to POST to. Default is "/login" - * @return + * @return the {@link FormLoginRequestBuilder} for additional customizations */ public FormLoginRequestBuilder loginProcessingUrl(String loginProcessingUrl) { this.loginProcessingUrl = loginProcessingUrl; From 94e7bbcad5742ad09ff2c492ca283282db9518c1 Mon Sep 17 00:00:00 2001 From: "mhyeon.lee" Date: Tue, 10 Jul 2018 23:40:42 +0900 Subject: [PATCH 138/226] Fix OAuth2 ClientRegistration scope can be null Allows scope of OAuth2 ClientRegistration to be null. - The scope setting in the RFC document is defined as Optional. https://tools.ietf.org/html/rfc6749#section-4.1.1 > scope: OPTIONAL. > The scope of the access request as described by Section 3.3. - When the client omits the scope parameter, validation is determined by the authorization server. https://tools.ietf.org/html/rfc6749#section-3.3 > If the client omits the scope parameter when requesting authorization, the authorization server MUST either process the request using a pre-defined default value or fail the request indicating an invalid scope. The authorization server SHOULD document its scope requirements and default value (if defined). Fixes gh-5494 --- .../registration/ClientRegistration.java | 4 +-- .../registration/ClientRegistrationTests.java | 27 +++++++++++++++---- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java index bf3f5e198ce..bf390f8b94f 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java @@ -463,10 +463,9 @@ private void validateAuthorizationCodeGrantType() { Assert.hasText(this.clientSecret, "clientSecret cannot be empty"); Assert.notNull(this.clientAuthenticationMethod, "clientAuthenticationMethod cannot be null"); Assert.hasText(this.redirectUriTemplate, "redirectUriTemplate cannot be empty"); - Assert.notEmpty(this.scopes, "scopes cannot be empty"); Assert.hasText(this.authorizationUri, "authorizationUri cannot be empty"); Assert.hasText(this.tokenUri, "tokenUri cannot be empty"); - if (this.scopes.contains(OidcScopes.OPENID)) { + if (this.scopes != null && this.scopes.contains(OidcScopes.OPENID)) { // OIDC Clients need to verify/validate the ID Token Assert.hasText(this.jwkSetUri, "jwkSetUri cannot be empty"); } @@ -479,7 +478,6 @@ private void validateImplicitGrantType() { Assert.hasText(this.registrationId, "registrationId cannot be empty"); Assert.hasText(this.clientId, "clientId cannot be empty"); Assert.hasText(this.redirectUriTemplate, "redirectUriTemplate cannot be empty"); - Assert.notEmpty(this.scopes, "scopes cannot be empty"); Assert.hasText(this.authorizationUri, "authorizationUri cannot be empty"); Assert.hasText(this.clientName, "clientName cannot be empty"); } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java index 95f23bd833f..0a9c096950b 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -165,8 +165,9 @@ public void buildWhenAuthorizationCodeGrantRedirectUriIsNullThenThrowIllegalArgu .build(); } - @Test(expected = IllegalArgumentException.class) - public void buildWhenAuthorizationCodeGrantScopeIsNullThenThrowIllegalArgumentException() { + // gh-5494 + @Test + public void buildWhenAuthorizationCodeGrantScopeIsNullThenScopeNotRequired() { ClientRegistration.withRegistrationId(REGISTRATION_ID) .clientId(CLIENT_ID) .clientSecret(CLIENT_SECRET) @@ -260,6 +261,21 @@ public void buildWhenAuthorizationCodeGrantScopeDoesNotContainOpenidThenJwkSetUr .build(); } + // gh-5494 + @Test + public void buildWhenAuthorizationCodeGrantScopeIsNullThenJwkSetUriNotRequired() { + ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .clientSecret(CLIENT_SECRET) + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate(REDIRECT_URI) + .authorizationUri(AUTHORIZATION_URI) + .tokenUri(TOKEN_URI) + .clientName(CLIENT_NAME) + .build(); + } + @Test public void buildWhenImplicitGrantAllAttributesProvidedThenAllAttributesAreSet() { ClientRegistration registration = ClientRegistration.withRegistrationId(REGISTRATION_ID) @@ -316,8 +332,9 @@ public void buildWhenImplicitGrantRedirectUriIsNullThenThrowIllegalArgumentExcep .build(); } - @Test(expected = IllegalArgumentException.class) - public void buildWhenImplicitGrantScopeIsNullThenThrowIllegalArgumentException() { + // gh-5494 + @Test + public void buildWhenImplicitGrantScopeIsNullThenScopeNotRequired() { ClientRegistration.withRegistrationId(REGISTRATION_ID) .clientId(CLIENT_ID) .authorizationGrantType(AuthorizationGrantType.IMPLICIT) From a635330015be498d2fa30f60f2957f9f2d41d973 Mon Sep 17 00:00:00 2001 From: Jonathan Chen Date: Wed, 18 Jul 2018 09:07:34 +1200 Subject: [PATCH 139/226] Include email in user information attributes from Facebook Fixes gh-5532 --- .../security/config/oauth2/client/CommonOAuth2Provider.java | 2 +- .../config/oauth2/client/CommonOAuth2ProviderTests.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/oauth2/client/CommonOAuth2Provider.java b/config/src/main/java/org/springframework/security/config/oauth2/client/CommonOAuth2Provider.java index 526a05cc5fb..a98aff17261 100644 --- a/config/src/main/java/org/springframework/security/config/oauth2/client/CommonOAuth2Provider.java +++ b/config/src/main/java/org/springframework/security/config/oauth2/client/CommonOAuth2Provider.java @@ -73,7 +73,7 @@ public Builder getBuilder(String registrationId) { builder.scope("public_profile", "email"); builder.authorizationUri("https://www.facebook.com/v2.8/dialog/oauth"); builder.tokenUri("https://graph.facebook.com/v2.8/oauth/access_token"); - builder.userInfoUri("https://graph.facebook.com/me"); + builder.userInfoUri("https://graph.facebook.com/me?fields=id,name,email"); builder.userNameAttributeName("id"); builder.clientName("Facebook"); return builder; diff --git a/config/src/test/java/org/springframework/security/config/oauth2/client/CommonOAuth2ProviderTests.java b/config/src/test/java/org/springframework/security/config/oauth2/client/CommonOAuth2ProviderTests.java index 2e093fe94cd..6ff20bcac31 100644 --- a/config/src/test/java/org/springframework/security/config/oauth2/client/CommonOAuth2ProviderTests.java +++ b/config/src/test/java/org/springframework/security/config/oauth2/client/CommonOAuth2ProviderTests.java @@ -89,7 +89,7 @@ public void getBuilderWhenFacebookShouldHaveFacebookSettings() throws Exception assertThat(providerDetails.getTokenUri()) .isEqualTo("https://graph.facebook.com/v2.8/oauth/access_token"); assertThat(providerDetails.getUserInfoEndpoint().getUri()) - .isEqualTo("https://graph.facebook.com/me"); + .isEqualTo("https://graph.facebook.com/me?fields=id,name,email"); assertThat(providerDetails.getUserInfoEndpoint().getUserNameAttributeName()) .isEqualTo("id"); assertThat(providerDetails.getJwkSetUri()).isNull(); From 5f2b0fa0432cd448c868e5ec96e9f7c93e7cfe65 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 18 Jul 2018 19:32:36 -0500 Subject: [PATCH 140/226] Default Log In Pages Use HTTPS for CSS Fixes: gh-5539 --- .../DefaultLoginPageConfigurerTests.groovy | 12 ++++++------ .../http/FormLoginBeanDefinitionParserTests.java | 8 ++++---- .../ui/DefaultLoginPageGeneratingFilter.java | 2 +- .../web/server/ui/LoginPageGeneratingWebFilter.java | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy index 32b7188b0f3..3364d9b104f 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy @@ -62,7 +62,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { Please sign in - +
    @@ -104,7 +104,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { Please sign in - +
    @@ -151,7 +151,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { Please sign in - +
    @@ -254,7 +254,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { Please sign in - +
    @@ -305,7 +305,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { Please sign in - +
    @@ -349,7 +349,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { Please sign in - +
    diff --git a/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java index 16ea49c54e2..e13c68b9ea9 100644 --- a/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java @@ -64,7 +64,7 @@ public void getLoginWhenAutoConfigThenShowsDefaultLoginPage() + " \n" + " Please sign in\n" + " \n" - + " \n" + + " \n" + " \n" + " \n" + "
    \n" @@ -110,7 +110,7 @@ public void getLoginWhenConfiguredWithCustomAttributesThenLoginPageReflects() + " \n" + " Please sign in\n" + " \n" - + " \n" + + " \n" + " \n" + " \n" + "
    \n" @@ -147,7 +147,7 @@ public void getLoginWhenConfiguredForOpenIdThenLoginPageReflects() + " \n" + " Please sign in\n" + " \n" - + " \n" + + " \n" + " \n" + " \n" + "
    \n" @@ -190,7 +190,7 @@ public void getLoginWhenConfiguredForOpenIdWithCustomAttributesThenLoginPageRefl + " \n" + " Please sign in\n" + " \n" - + " \n" + + " \n" + " \n" + " \n" + "
    \n" diff --git a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java index 783fe6fb019..153b527c35a 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java @@ -231,7 +231,7 @@ private String generateLoginPageHtml(HttpServletRequest request, boolean loginEr + " \n" + " Please sign in\n" + " \n" - + " \n" + + " \n" + " \n" + " \n" + "
    \n"); diff --git a/web/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java b/web/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java index 7bf0cfaa35b..8e920af54ae 100644 --- a/web/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java +++ b/web/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java @@ -101,7 +101,7 @@ private byte[] createPage(ServerWebExchange exchange, String csrfTokenHtmlInput) + " \n" + " Please sign in\n" + " \n" - + " \n" + + " \n" + " \n" + " \n" + "
    \n" From 56b0f6497db7aad4fe2c4b43fa0e63927ad376f6 Mon Sep 17 00:00:00 2001 From: Eric Hudon Date: Wed, 18 Jul 2018 10:19:21 -0400 Subject: [PATCH 141/226] Fix a missing "throws Exception" for configure(AuthenticationManagerBuilder auth) The actual method signature look this this: ```java protected void configure(AuthenticationManagerBuilder auth) throws Exception ``` This PR aims at aligning the javadoc for this annotation with the actual method signature. --- .../config/annotation/web/configuration/EnableWebSecurity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java index 940d685eafe..8fc51dc26cc 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java @@ -52,7 +52,7 @@ * } * * @Override - * protected void configure(AuthenticationManagerBuilder auth) { + * protected void configure(AuthenticationManagerBuilder auth) throws Exception { * auth * // enable in memory based authentication with a user named "user" and "admin" * .inMemoryAuthentication().withUser("user").password("password").roles("USER") From 2e8b371f9f49213a3a1be785cde6e3f833b1f3d1 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 14 Jun 2018 22:14:11 +0800 Subject: [PATCH 142/226] Fixed document error --- .../docs/asciidoc/_includes/authorization/expression-based.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manual/src/docs/asciidoc/_includes/authorization/expression-based.adoc b/docs/manual/src/docs/asciidoc/_includes/authorization/expression-based.adoc index 85608114696..aac5044dd1d 100644 --- a/docs/manual/src/docs/asciidoc/_includes/authorization/expression-based.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/authorization/expression-based.adoc @@ -35,7 +35,7 @@ This can be customized by modifying the `defaultRolePrefix` on `DefaultWebSecuri | Returns `true` if the current principal has the specified authority. | `hasAnyAuthority([authority1,authority2])` -| Returns `true` if the current principal has any of the supplied roles (given as a comma-separated list of strings) +| Returns `true` if the current principal has any of the supplied authorities (given as a comma-separated list of strings) | `principal` | Allows direct access to the principal object representing the current user From aae7c2a1cdc56509613c234267d54ac96cc85046 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 19 Jul 2018 14:44:21 -0600 Subject: [PATCH 143/226] MultiHttpBlockConfigTests groovy->java Note that originally there were five tests in the groovy test, however the last one, multipleAuthenticationManagersWorks, turned out to be a duplicate after creating the test requestWhenUsingMutuallyExclusiveHttpElementsThenIsRoutedAccordingly As such, the new file contains just four tests. Issue: gh-4939 --- .../http/MultiHttpBlockConfigTests.groovy | 133 ------------------ .../http/MultiHttpBlockConfigTests.java | 116 +++++++++++++++ ...pBlockConfigTests-DistinctHttpElements.xml | 36 +++++ ...BlockConfigTests-IdenticalHttpElements.xml | 34 +++++ ...Tests-IdenticallyPatternedHttpElements.xml | 34 +++++ .../MultiHttpBlockConfigTests-Sec1937.xml | 50 +++++++ 6 files changed, 270 insertions(+), 133 deletions(-) delete mode 100644 config/src/test/groovy/org/springframework/security/config/http/MultiHttpBlockConfigTests.groovy create mode 100644 config/src/test/java/org/springframework/security/config/http/MultiHttpBlockConfigTests.java create mode 100644 config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-DistinctHttpElements.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-IdenticalHttpElements.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-IdenticallyPatternedHttpElements.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-Sec1937.xml diff --git a/config/src/test/groovy/org/springframework/security/config/http/MultiHttpBlockConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/MultiHttpBlockConfigTests.groovy deleted file mode 100644 index 6d54e6b384b..00000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/MultiHttpBlockConfigTests.groovy +++ /dev/null @@ -1,133 +0,0 @@ -package org.springframework.security.config.http - -import static org.mockito.Mockito.* - -import org.powermock.api.mockito.internal.verification.VerifyNoMoreInteractions; -import org.springframework.beans.factory.parsing.BeanDefinitionParsingException -import org.springframework.mock.web.MockFilterChain; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.config.BeanIds -import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.web.FilterChainProxy -import org.junit.Assert -import org.springframework.beans.factory.BeanCreationException -import org.springframework.security.web.SecurityFilterChain - -/** - * Tests scenarios with multiple <http> elements. - * - * @author Luke Taylor - */ -class MultiHttpBlockConfigTests extends AbstractHttpConfigTests { - - def multipleHttpElementsAreSupported () { - when: "Two elements are used" - xml.http(pattern: '/stateless/**', 'create-session': 'stateless') { - 'http-basic'() - } - xml.http(pattern: '/stateful/**') { - 'form-login'() - } - createAppContext() - FilterChainProxy fcp = appContext.getBean(BeanIds.FILTER_CHAIN_PROXY) - def filterChains = fcp.getFilterChains(); - - then: - filterChains.size() == 2 - filterChains[0].requestMatcher.pattern == '/stateless/**' - } - - def duplicateHttpElementsAreRejected () { - when: "Two elements are used" - xml.http('create-session': 'stateless') { - 'http-basic'() - } - xml.http() { - 'form-login'() - } - createAppContext() - then: - BeanCreationException e = thrown() - e.cause instanceof IllegalArgumentException - } - - def duplicatePatternsAreRejected () { - when: "Two elements with the same pattern are used" - xml.http(pattern: '/stateless/**', 'create-session': 'stateless') { - 'http-basic'() - } - xml.http(pattern: '/stateless/**') { - 'form-login'() - } - createAppContext() - then: - BeanCreationException e = thrown() - e.cause instanceof IllegalArgumentException - } - - - def 'SEC-1937: http@authentication-manager-ref and multi authentication-mananager'() { - setup: - xml.http('authentication-manager-ref' : 'authManager', 'pattern' : '/first/**') { - 'form-login'('login-processing-url': '/first/login') - csrf(disabled:true) - } - xml.http('authentication-manager-ref' : 'authManager2') { - 'form-login'() - csrf(disabled:true) - } - mockBean(UserDetailsService,'uds') - mockBean(UserDetailsService,'uds2') - createAppContext(""" - - - - - - -""") - UserDetailsService uds = appContext.getBean('uds') - UserDetailsService uds2 = appContext.getBean('uds2') - when: - MockHttpServletRequest request = new MockHttpServletRequest("GET", "") - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - request.servletPath = "/first/login" - request.requestURI = "/first/login" - request.method = 'POST' - springSecurityFilterChain.doFilter(request,response,chain) - then: - verify(uds).loadUserByUsername(anyString()) || true - verifyZeroInteractions(uds2) || true - when: - MockHttpServletRequest request2 = new MockHttpServletRequest("GET", "") - MockHttpServletResponse response2 = new MockHttpServletResponse() - MockFilterChain chain2 = new MockFilterChain() - request2.servletPath = "/login" - request2.requestURI = "/login" - request2.method = 'POST' - springSecurityFilterChain.doFilter(request2,response2,chain2) - then: - verify(uds2).loadUserByUsername(anyString()) || true - verifyNoMoreInteractions(uds) || true - } - - def multipleAuthenticationManagersWorks () { - xml.http(name: 'basic', pattern: '/basic/**', ) { - 'http-basic'() - } - xml.http(pattern: '/form/**') { - 'form-login'() - } - createAppContext() - FilterChainProxy fcp = appContext.getBean(BeanIds.FILTER_CHAIN_PROXY) - SecurityFilterChain basicChain = fcp.filterChains[0]; - - expect: - Assert.assertSame (basicChain, appContext.getBean('basic')) - } -} diff --git a/config/src/test/java/org/springframework/security/config/http/MultiHttpBlockConfigTests.java b/config/src/test/java/org/springframework/security/config/http/MultiHttpBlockConfigTests.java new file mode 100644 index 00000000000..5704431f15c --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/MultiHttpBlockConfigTests.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.http; + +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.stereotype.Controller; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.web.bind.annotation.GetMapping; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests scenarios with multiple <http> elements. + * + * @author Luke Taylor + */ +public class MultiHttpBlockConfigTests { + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/MultiHttpBlockConfigTests"; + + @Autowired + MockMvc mvc; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Test + public void requestWhenUsingMutuallyExclusiveHttpElementsThenIsRoutedAccordingly() + throws Exception { + + this.spring.configLocations(this.xml("DistinctHttpElements")).autowire(); + + this.mvc.perform(MockMvcRequestBuilders.get("/first") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(post("/second/login") + .param("username", "user") + .param("password", "password") + .with(csrf())) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/")); + } + + @Test + public void configureWhenUsingDuplicateHttpElementsThenThrowsWiringException() { + assertThatCode(() -> this.spring.configLocations(this.xml("IdenticalHttpElements")).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasCauseInstanceOf(IllegalArgumentException.class); + } + + @Test + public void configureWhenUsingIndenticallyPatternedHttpElementsThenThrowsWiringException() { + assertThatCode(() -> this.spring.configLocations(this.xml("IdenticallyPatternedHttpElements")).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasCauseInstanceOf(IllegalArgumentException.class); + } + + /** + * SEC-1937 + */ + @Test + public void requestWhenTargettingAuthenticationManagersToCorrespondingHttpElementsThenAuthenticationProceeds() + throws Exception { + + this.spring.configLocations(this.xml("Sec1937")).autowire(); + + this.mvc.perform(get("/first") + .with(httpBasic("first", "password")) + .with(csrf())) + .andExpect(status().isOk()); + + this.mvc.perform(post("/second/login") + .param("username", "second") + .param("password", "password") + .with(csrf())) + .andExpect(redirectedUrl("/")); + } + + @Controller + static class BasicController { + @GetMapping("/first") + public String first() { + return "ok"; + } + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-DistinctHttpElements.xml b/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-DistinctHttpElements.xml new file mode 100644 index 00000000000..be89f7d9771 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-DistinctHttpElements.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-IdenticalHttpElements.xml b/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-IdenticalHttpElements.xml new file mode 100644 index 00000000000..ba1c05489c6 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-IdenticalHttpElements.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-IdenticallyPatternedHttpElements.xml b/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-IdenticallyPatternedHttpElements.xml new file mode 100644 index 00000000000..e027a52dff2 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-IdenticallyPatternedHttpElements.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-Sec1937.xml b/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-Sec1937.xml new file mode 100644 index 00000000000..fbe6d7d5724 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-Sec1937.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From d7f58d01b4d8efc3bdba04b65deee9ba87fbf8b5 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Thu, 19 Jul 2018 16:46:13 -0400 Subject: [PATCH 144/226] Use OAuth2AuthorizedClientRepository in filters and resolver Fixes gh-5544 --- .../OAuth2ClientConfiguration.java | 16 +++---- .../oauth2/client/OAuth2ClientConfigurer.java | 24 +++++++--- .../client/OAuth2ClientConfigurerUtils.java | 37 ++++++++++++--- .../oauth2/client/OAuth2LoginConfigurer.java | 24 ++++++++-- .../OAuth2ClientConfigurationTests.java | 47 +++++++++++-------- .../client/OAuth2ClientConfigurerTests.java | 18 +++++-- .../web/OAuth2LoginAuthenticationFilter.java | 40 +++++++++++----- .../OAuth2LoginAuthenticationFilterTests.java | 40 ++++++++++------ ...uthorizationCodeGrantApplicationTests.java | 25 ++++++++-- .../java/sample/config/SecurityConfig.java | 8 ++++ .../samples/OAuth2LoginApplicationTests.java | 14 ++++-- .../java/sample/OAuth2LoginApplication.java | 9 ++++ 12 files changed, 215 insertions(+), 87 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java index aa538d3445d..982f660f6e9 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java @@ -20,8 +20,7 @@ import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.method.annotation.OAuth2AuthorizedClientArgumentResolver; import org.springframework.util.ClassUtils; import org.springframework.web.method.support.HandlerMethodArgumentResolver; @@ -58,22 +57,21 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { @Configuration static class OAuth2ClientWebMvcSecurityConfiguration implements WebMvcConfigurer { - private OAuth2AuthorizedClientService authorizedClientService; + private OAuth2AuthorizedClientRepository authorizedClientRepository; @Override public void addArgumentResolvers(List argumentResolvers) { - if (this.authorizedClientService != null) { + if (this.authorizedClientRepository != null) { OAuth2AuthorizedClientArgumentResolver authorizedClientArgumentResolver = - new OAuth2AuthorizedClientArgumentResolver( - new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(this.authorizedClientService)); + new OAuth2AuthorizedClientArgumentResolver(this.authorizedClientRepository); argumentResolvers.add(authorizedClientArgumentResolver); } } @Autowired(required = false) - public void setAuthorizedClientService(List authorizedClientServices) { - if (authorizedClientServices.size() == 1) { - this.authorizedClientService = authorizedClientServices.get(0); + public void setAuthorizedClientRepository(List authorizedClientRepositories) { + if (authorizedClientRepositories.size() == 1) { + this.authorizedClientRepository = authorizedClientRepositories.get(0); } } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java index 4d769d29c15..60b81499d0e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java @@ -29,6 +29,7 @@ import org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.util.Assert; @@ -63,7 +64,7 @@ * *
      *
    • {@link ClientRegistrationRepository} (required)
    • - *
    • {@link OAuth2AuthorizedClientService} (optional)
    • + *
    • {@link OAuth2AuthorizedClientRepository} (optional)
    • *
    * *

    Shared Objects Used

    @@ -72,7 +73,7 @@ * *
      *
    • {@link ClientRegistrationRepository}
    • - *
    • {@link OAuth2AuthorizedClientService}
    • + *
    • {@link OAuth2AuthorizedClientRepository}
    • *
    * * @author Joe Grandja @@ -80,7 +81,7 @@ * @see OAuth2AuthorizationRequestRedirectFilter * @see OAuth2AuthorizationCodeGrantFilter * @see ClientRegistrationRepository - * @see OAuth2AuthorizedClientService + * @see OAuth2AuthorizedClientRepository * @see AbstractHttpConfigurer */ public final class OAuth2ClientConfigurer> extends @@ -100,6 +101,18 @@ public OAuth2ClientConfigurer clientRegistrationRepository(ClientRegistration return this; } + /** + * Sets the repository for authorized client(s). + * + * @param authorizedClientRepository the authorized client repository + * @return the {@link OAuth2ClientConfigurer} for further configuration + */ + public OAuth2ClientConfigurer authorizedClientRepository(OAuth2AuthorizedClientRepository authorizedClientRepository) { + Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null"); + this.getBuilder().setSharedObject(OAuth2AuthorizedClientRepository.class, authorizedClientRepository); + return this; + } + /** * Sets the service for authorized client(s). * @@ -108,7 +121,7 @@ public OAuth2ClientConfigurer clientRegistrationRepository(ClientRegistration */ public OAuth2ClientConfigurer authorizedClientService(OAuth2AuthorizedClientService authorizedClientService) { Assert.notNull(authorizedClientService, "authorizedClientService cannot be null"); - this.getBuilder().setSharedObject(OAuth2AuthorizedClientService.class, authorizedClientService); + this.authorizedClientRepository(new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService)); return this; } @@ -309,8 +322,7 @@ private void configure(B builder, AuthorizationCodeGrantConfigurer authorization OAuth2AuthorizationCodeGrantFilter authorizationCodeGrantFilter = new OAuth2AuthorizationCodeGrantFilter( OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder), - new AuthenticatedPrincipalOAuth2AuthorizedClientRepository( - OAuth2ClientConfigurerUtils.getAuthorizedClientService(builder)), + OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(builder), authenticationManager); if (authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestRepository != null) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java index bb62ef24221..1e16a73da0c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java @@ -24,6 +24,8 @@ import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.util.StringUtils; import java.util.Map; @@ -61,14 +63,35 @@ private static > ClientRegistrationRepository g return clientRegistrationRepositoryMap.values().iterator().next(); } - static > OAuth2AuthorizedClientService getAuthorizedClientService(B builder) { - OAuth2AuthorizedClientService authorizedClientService = builder.getSharedObject(OAuth2AuthorizedClientService.class); - if (authorizedClientService == null) { - authorizedClientService = getAuthorizedClientServiceBean(builder); - if (authorizedClientService == null) { - authorizedClientService = new InMemoryOAuth2AuthorizedClientService(getClientRegistrationRepository(builder)); + static > OAuth2AuthorizedClientRepository getAuthorizedClientRepository(B builder) { + OAuth2AuthorizedClientRepository authorizedClientRepository = builder.getSharedObject(OAuth2AuthorizedClientRepository.class); + if (authorizedClientRepository == null) { + authorizedClientRepository = getAuthorizedClientRepositoryBean(builder); + if (authorizedClientRepository == null) { + authorizedClientRepository = new AuthenticatedPrincipalOAuth2AuthorizedClientRepository( + getAuthorizedClientService((builder))); } - builder.setSharedObject(OAuth2AuthorizedClientService.class, authorizedClientService); + builder.setSharedObject(OAuth2AuthorizedClientRepository.class, authorizedClientRepository); + } + return authorizedClientRepository; + } + + private static > OAuth2AuthorizedClientRepository getAuthorizedClientRepositoryBean(B builder) { + Map authorizedClientRepositoryMap = BeanFactoryUtils.beansOfTypeIncludingAncestors( + builder.getSharedObject(ApplicationContext.class), OAuth2AuthorizedClientRepository.class); + if (authorizedClientRepositoryMap.size() > 1) { + throw new NoUniqueBeanDefinitionException(OAuth2AuthorizedClientRepository.class, authorizedClientRepositoryMap.size(), + "Expected single matching bean of type '" + OAuth2AuthorizedClientRepository.class.getName() + "' but found " + + authorizedClientRepositoryMap.size() + ": " + StringUtils.collectionToCommaDelimitedString(authorizedClientRepositoryMap.keySet())); + } + return (!authorizedClientRepositoryMap.isEmpty() ? authorizedClientRepositoryMap.values().iterator().next() : null); + } + + + private static > OAuth2AuthorizedClientService getAuthorizedClientService(B builder) { + OAuth2AuthorizedClientService authorizedClientService = getAuthorizedClientServiceBean(builder); + if (authorizedClientService == null) { + authorizedClientService = new InMemoryOAuth2AuthorizedClientService(getClientRegistrationRepository(builder)); } return authorizedClientService; } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java index b8160854bf8..eeeb5a738c9 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java @@ -42,9 +42,11 @@ import org.springframework.security.oauth2.client.userinfo.DelegatingOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; @@ -92,7 +94,7 @@ * *
      *
    • {@link ClientRegistrationRepository} (required)
    • - *
    • {@link OAuth2AuthorizedClientService} (optional)
    • + *
    • {@link OAuth2AuthorizedClientRepository} (optional)
    • *
    • {@link GrantedAuthoritiesMapper} (optional)
    • *
    * @@ -102,7 +104,7 @@ * *
      *
    • {@link ClientRegistrationRepository}
    • - *
    • {@link OAuth2AuthorizedClientService}
    • + *
    • {@link OAuth2AuthorizedClientRepository}
    • *
    • {@link GrantedAuthoritiesMapper}
    • *
    • {@link DefaultLoginPageGeneratingFilter} - if {@link #loginPage(String)} is not configured * and {@code DefaultLoginPageGeneratingFilter} is available, than a default login page will be made available
    • @@ -115,6 +117,7 @@ * @see OAuth2AuthorizationRequestRedirectFilter * @see OAuth2LoginAuthenticationFilter * @see ClientRegistrationRepository + * @see OAuth2AuthorizedClientRepository * @see AbstractAuthenticationFilterConfigurer */ public final class OAuth2LoginConfigurer> extends @@ -139,6 +142,19 @@ public OAuth2LoginConfigurer clientRegistrationRepository(ClientRegistrationR return this; } + /** + * Sets the repository for authorized client(s). + * + * @since 5.1 + * @param authorizedClientRepository the authorized client repository + * @return the {@link OAuth2LoginConfigurer} for further configuration + */ + public OAuth2LoginConfigurer authorizedClientRepository(OAuth2AuthorizedClientRepository authorizedClientRepository) { + Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null"); + this.getBuilder().setSharedObject(OAuth2AuthorizedClientRepository.class, authorizedClientRepository); + return this; + } + /** * Sets the service for authorized client(s). * @@ -147,7 +163,7 @@ public OAuth2LoginConfigurer clientRegistrationRepository(ClientRegistrationR */ public OAuth2LoginConfigurer authorizedClientService(OAuth2AuthorizedClientService authorizedClientService) { Assert.notNull(authorizedClientService, "authorizedClientService cannot be null"); - this.getBuilder().setSharedObject(OAuth2AuthorizedClientService.class, authorizedClientService); + this.authorizedClientRepository(new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService)); return this; } @@ -400,7 +416,7 @@ public void init(B http) throws Exception { OAuth2LoginAuthenticationFilter authenticationFilter = new OAuth2LoginAuthenticationFilter( OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()), - OAuth2ClientConfigurerUtils.getAuthorizedClientService(this.getBuilder()), + OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(this.getBuilder()), this.loginProcessingUrl); this.setAuthenticationFilter(authenticationFilter); super.loginProcessingUrl(this.loginProcessingUrl); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java index 86a73476945..c2098cd86be 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java @@ -21,22 +21,27 @@ import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; +import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.test.SpringTestRule; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.test.web.servlet.MockMvc; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import javax.servlet.http.HttpServletRequest; + import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -57,18 +62,20 @@ public class OAuth2ClientConfigurationTests { public void requestWhenAuthorizedClientFoundThenMethodArgumentResolved() throws Exception { String clientRegistrationId = "client1"; String principalName = "user1"; + TestingAuthenticationToken authentication = new TestingAuthenticationToken(principalName, "password"); - OAuth2AuthorizedClientService authorizedClientService = mock(OAuth2AuthorizedClientService.class); + OAuth2AuthorizedClientRepository authorizedClientRepository = mock(OAuth2AuthorizedClientRepository.class); OAuth2AuthorizedClient authorizedClient = mock(OAuth2AuthorizedClient.class); - when(authorizedClientService.loadAuthorizedClient(clientRegistrationId, principalName)).thenReturn(authorizedClient); + when(authorizedClientRepository.loadAuthorizedClient( + eq(clientRegistrationId), eq(authentication), any(HttpServletRequest.class))).thenReturn(authorizedClient); OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class); when(authorizedClient.getAccessToken()).thenReturn(accessToken); - OAuth2AuthorizedClientArgumentResolverConfig.AUTHORIZED_CLIENT_SERVICE = authorizedClientService; + OAuth2AuthorizedClientArgumentResolverConfig.AUTHORIZED_CLIENT_REPOSITORY = authorizedClientRepository; this.spring.register(OAuth2AuthorizedClientArgumentResolverConfig.class).autowire(); - this.mockMvc.perform(get("/authorized-client").with(user(principalName))) + this.mockMvc.perform(get("/authorized-client").with(authentication(authentication))) .andExpect(status().isOk()) .andExpect(content().string("resolved")); } @@ -76,7 +83,7 @@ public void requestWhenAuthorizedClientFoundThenMethodArgumentResolved() throws @EnableWebMvc @EnableWebSecurity static class OAuth2AuthorizedClientArgumentResolverConfig extends WebSecurityConfigurerAdapter { - static OAuth2AuthorizedClientService AUTHORIZED_CLIENT_SERVICE; + static OAuth2AuthorizedClientRepository AUTHORIZED_CLIENT_REPOSITORY; @Override protected void configure(HttpSecurity http) throws Exception { @@ -92,23 +99,23 @@ public String authorizedClient(@RegisteredOAuth2AuthorizedClient("client1") OAut } @Bean - public OAuth2AuthorizedClientService authorizedClientService() { - return AUTHORIZED_CLIENT_SERVICE; + public OAuth2AuthorizedClientRepository authorizedClientRepository() { + return AUTHORIZED_CLIENT_REPOSITORY; } } // gh-5321 @Test - public void loadContextWhenOAuth2AuthorizedClientServiceRegisteredTwiceThenThrowNoUniqueBeanDefinitionException() { - assertThatThrownBy(() -> this.spring.register(OAuth2AuthorizedClientServiceRegisteredTwiceConfig.class).autowire()) + public void loadContextWhenOAuth2AuthorizedClientRepositoryRegisteredTwiceThenThrowNoUniqueBeanDefinitionException() { + assertThatThrownBy(() -> this.spring.register(OAuth2AuthorizedClientRepositoryRegisteredTwiceConfig.class).autowire()) .hasRootCauseInstanceOf(NoUniqueBeanDefinitionException.class) - .hasMessageContaining("Expected single matching bean of type '" + OAuth2AuthorizedClientService.class.getName() + - "' but found 2: authorizedClientService1,authorizedClientService2"); + .hasMessageContaining("Expected single matching bean of type '" + OAuth2AuthorizedClientRepository.class.getName() + + "' but found 2: authorizedClientRepository1,authorizedClientRepository2"); } @EnableWebMvc @EnableWebSecurity - static class OAuth2AuthorizedClientServiceRegisteredTwiceConfig extends WebSecurityConfigurerAdapter { + static class OAuth2AuthorizedClientRepositoryRegisteredTwiceConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { @@ -127,13 +134,13 @@ public ClientRegistrationRepository clientRegistrationRepository() { } @Bean - public OAuth2AuthorizedClientService authorizedClientService1() { - return mock(OAuth2AuthorizedClientService.class); + public OAuth2AuthorizedClientRepository authorizedClientRepository1() { + return mock(OAuth2AuthorizedClientRepository.class); } @Bean - public OAuth2AuthorizedClientService authorizedClientService2() { - return mock(OAuth2AuthorizedClientService.class); + public OAuth2AuthorizedClientRepository authorizedClientRepository2() { + return mock(OAuth2AuthorizedClientRepository.class); } } @@ -194,8 +201,8 @@ public ClientRegistrationRepository clientRegistrationRepository2() { } @Bean - public OAuth2AuthorizedClientService authorizedClientService() { - return mock(OAuth2AuthorizedClientService.class); + public OAuth2AuthorizedClientRepository authorizedClientRepository() { + return mock(OAuth2AuthorizedClientRepository.class); } } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java index 43414b9d4c9..795b5bc2339 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java @@ -23,6 +23,7 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @@ -36,10 +37,12 @@ import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; @@ -61,6 +64,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; @@ -76,6 +80,8 @@ public class OAuth2ClientConfigurerTests { private static OAuth2AuthorizedClientService authorizedClientService; + private static OAuth2AuthorizedClientRepository authorizedClientRepository; + private static OAuth2AuthorizationRequestResolver authorizationRequestResolver; private static OAuth2AccessTokenResponseClient accessTokenResponseClient; @@ -107,6 +113,7 @@ public void setup() { .build(); clientRegistrationRepository = new InMemoryClientRegistrationRepository(this.registration1); authorizedClientService = new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository); + authorizedClientRepository = new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService); authorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( clientRegistrationRepository, "/oauth2/authorization"); @@ -153,17 +160,18 @@ public void configureWhenAuthorizationCodeResponseSuccessThenAuthorizedClientSav MockHttpSession session = (MockHttpSession) request.getSession(); String principalName = "user1"; + TestingAuthenticationToken authentication = new TestingAuthenticationToken(principalName, "password"); this.mockMvc.perform(get("/client-1") .param(OAuth2ParameterNames.CODE, "code") .param(OAuth2ParameterNames.STATE, "state") - .with(user(principalName)) + .with(authentication(authentication)) .session(session)) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl("http://localhost/client-1")); - OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient( - this.registration1.getRegistrationId(), principalName); + OAuth2AuthorizedClient authorizedClient = authorizedClientRepository.loadAuthorizedClient( + this.registration1.getRegistrationId(), authentication, request); assertThat(authorizedClient).isNotNull(); } @@ -229,8 +237,8 @@ public ClientRegistrationRepository clientRegistrationRepository() { } @Bean - public OAuth2AuthorizedClientService authorizedClientService() { - return authorizedClientService; + public OAuth2AuthorizedClientRepository authorizedClientRepository() { + return authorizedClientRepository; } @RestController diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java index d9a3d9f9bc1..18e4cb7b275 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,12 +15,6 @@ */ package org.springframework.security.oauth2.client.web; -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -43,6 +37,11 @@ import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + /** * An implementation of an {@link AbstractAuthenticationProcessingFilter} for OAuth 2.0 Login. * @@ -68,7 +67,7 @@ * *
    • * Upon a successful authentication, an {@link OAuth2AuthenticationToken} is created (representing the End-User {@code Principal}) - * and associated to the {@link OAuth2AuthorizedClient Authorized Client} using the {@link OAuth2AuthorizedClientService}. + * and associated to the {@link OAuth2AuthorizedClient Authorized Client} using the {@link OAuth2AuthorizedClientRepository}. *
    • *
    • * Finally, the {@link OAuth2AuthenticationToken} is returned and ultimately stored @@ -88,7 +87,7 @@ * @see OAuth2AuthorizationRequestRedirectFilter * @see ClientRegistrationRepository * @see OAuth2AuthorizedClient - * @see OAuth2AuthorizedClientService + * @see OAuth2AuthorizedClientRepository * @see Section 4.1 Authorization Code Grant * @see Section 4.1.2 Authorization Response */ @@ -100,7 +99,7 @@ public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProce private static final String AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE = "authorization_request_not_found"; private static final String CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE = "client_registration_not_found"; private ClientRegistrationRepository clientRegistrationRepository; - private OAuth2AuthorizedClientService authorizedClientService; + private OAuth2AuthorizedClientRepository authorizedClientRepository; private AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository(); @@ -125,11 +124,26 @@ public OAuth2LoginAuthenticationFilter(ClientRegistrationRepository clientRegist public OAuth2LoginAuthenticationFilter(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientService authorizedClientService, String filterProcessesUrl) { + this(clientRegistrationRepository, + new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService), filterProcessesUrl); + } + + /** + * Constructs an {@code OAuth2LoginAuthenticationFilter} using the provided parameters. + * + * @since 5.1 + * @param clientRegistrationRepository the repository of client registrations + * @param authorizedClientRepository the authorized client repository + * @param filterProcessesUrl the {@code URI} where this {@code Filter} will process the authentication requests + */ + public OAuth2LoginAuthenticationFilter(ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository, + String filterProcessesUrl) { super(filterProcessesUrl); Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); - Assert.notNull(authorizedClientService, "authorizedClientService cannot be null"); + Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null"); this.clientRegistrationRepository = clientRegistrationRepository; - this.authorizedClientService = authorizedClientService; + this.authorizedClientRepository = authorizedClientRepository; } @Override @@ -176,7 +190,7 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ authenticationResult.getAccessToken(), authenticationResult.getRefreshToken()); - this.authorizedClientService.saveAuthorizedClient(authorizedClient, oauth2Authentication); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response); return oauth2Authentication; } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilterTests.java index 9892851d57e..1546a5b396d 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilterTests.java @@ -70,10 +70,12 @@ public class OAuth2LoginAuthenticationFilterTests { private ClientRegistration registration2; private String principalName1 = "principal-1"; private ClientRegistrationRepository clientRegistrationRepository; + private OAuth2AuthorizedClientRepository authorizedClientRepository; private OAuth2AuthorizedClientService authorizedClientService; private AuthorizationRequestRepository authorizationRequestRepository; private AuthenticationFailureHandler failureHandler; private AuthenticationManager authenticationManager; + private OAuth2LoginAuthenticationToken loginAuthentication; private OAuth2LoginAuthenticationFilter filter; @Before @@ -107,11 +109,12 @@ public void setUp() { this.clientRegistrationRepository = new InMemoryClientRegistrationRepository( this.registration1, this.registration2); this.authorizedClientService = new InMemoryOAuth2AuthorizedClientService(this.clientRegistrationRepository); + this.authorizedClientRepository = new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(this.authorizedClientService); this.authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository(); this.failureHandler = mock(AuthenticationFailureHandler.class); this.authenticationManager = mock(AuthenticationManager.class); - this.filter = spy(new OAuth2LoginAuthenticationFilter( - this.clientRegistrationRepository, this.authorizedClientService)); + this.filter = spy(new OAuth2LoginAuthenticationFilter(this.clientRegistrationRepository, + this.authorizedClientRepository, OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI)); this.filter.setAuthorizationRequestRepository(this.authorizationRequestRepository); this.filter.setAuthenticationFailureHandler(this.failureHandler); this.filter.setAuthenticationManager(this.authenticationManager); @@ -129,9 +132,16 @@ public void constructorWhenAuthorizedClientServiceIsNullThenThrowIllegalArgument .isInstanceOf(IllegalArgumentException.class); } + @Test + public void constructorWhenAuthorizedClientRepositoryIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2LoginAuthenticationFilter(this.clientRegistrationRepository, + (OAuth2AuthorizedClientRepository) null, OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI)) + .isInstanceOf(IllegalArgumentException.class); + } + @Test public void constructorWhenFilterProcessesUrlIsNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2LoginAuthenticationFilter(this.clientRegistrationRepository, this.authorizedClientService, null)) + assertThatThrownBy(() -> new OAuth2LoginAuthenticationFilter(this.clientRegistrationRepository, this.authorizedClientRepository, null)) .isInstanceOf(IllegalArgumentException.class); } @@ -276,8 +286,8 @@ public void doFilterWhenAuthorizationResponseValidThenAuthorizedClientSaved() th this.filter.doFilter(request, response, filterChain); - OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient( - this.registration1.getRegistrationId(), this.principalName1); + OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient( + this.registration1.getRegistrationId(), this.loginAuthentication, request); assertThat(authorizedClient).isNotNull(); assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1); assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principalName1); @@ -289,7 +299,7 @@ public void doFilterWhenAuthorizationResponseValidThenAuthorizedClientSaved() th public void doFilterWhenCustomFilterProcessesUrlThenFilterProcesses() throws Exception { String filterProcessesUrl = "/login/oauth2/custom/*"; this.filter = spy(new OAuth2LoginAuthenticationFilter( - this.clientRegistrationRepository, this.authorizedClientService, filterProcessesUrl)); + this.clientRegistrationRepository, this.authorizedClientRepository, filterProcessesUrl)); this.filter.setAuthenticationManager(this.authenticationManager); String requestUri = "/login/oauth2/custom/" + this.registration2.getRegistrationId(); @@ -324,13 +334,15 @@ private void setUpAuthorizationRequest(HttpServletRequest request, HttpServletRe private void setUpAuthenticationResult(ClientRegistration registration) { OAuth2User user = mock(OAuth2User.class); when(user.getName()).thenReturn(this.principalName1); - OAuth2LoginAuthenticationToken loginAuthentication = mock(OAuth2LoginAuthenticationToken.class); - when(loginAuthentication.getPrincipal()).thenReturn(user); - when(loginAuthentication.getAuthorities()).thenReturn(AuthorityUtils.createAuthorityList("ROLE_USER")); - when(loginAuthentication.getClientRegistration()).thenReturn(registration); - when(loginAuthentication.getAuthorizationExchange()).thenReturn(mock(OAuth2AuthorizationExchange.class)); - when(loginAuthentication.getAccessToken()).thenReturn(mock(OAuth2AccessToken.class)); - when(loginAuthentication.getRefreshToken()).thenReturn(mock(OAuth2RefreshToken.class)); - when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn(loginAuthentication); + this.loginAuthentication = mock(OAuth2LoginAuthenticationToken.class); + when(this.loginAuthentication.getPrincipal()).thenReturn(user); + when(this.loginAuthentication.getName()).thenReturn(this.principalName1); + when(this.loginAuthentication.getAuthorities()).thenReturn(AuthorityUtils.createAuthorityList("ROLE_USER")); + when(this.loginAuthentication.getClientRegistration()).thenReturn(registration); + when(this.loginAuthentication.getAuthorizationExchange()).thenReturn(mock(OAuth2AuthorizationExchange.class)); + when(this.loginAuthentication.getAccessToken()).thenReturn(mock(OAuth2AccessToken.class)); + when(this.loginAuthentication.getRefreshToken()).thenReturn(mock(OAuth2RefreshToken.class)); + when(this.loginAuthentication.isAuthenticated()).thenReturn(true); + when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn(this.loginAuthentication); } } diff --git a/samples/boot/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java b/samples/boot/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java index b078b6aacb3..d94e4b0470c 100644 --- a/samples/boot/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java +++ b/samples/boot/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java @@ -22,24 +22,29 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Import; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; @@ -57,6 +62,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; @@ -78,7 +84,7 @@ public class OAuth2AuthorizationCodeGrantApplicationTests { private ClientRegistrationRepository clientRegistrationRepository; @Autowired - private OAuth2AuthorizedClientService authorizedClientService; + private OAuth2AuthorizedClientRepository authorizedClientRepository; @Autowired private MockMvc mockMvc; @@ -116,18 +122,19 @@ public void requestWhenClientGrantedAuthorizationThenAuthorizedClientSaved() thr MockHttpSession session = (MockHttpSession) request.getSession(); String principalName = "user"; + TestingAuthenticationToken authentication = new TestingAuthenticationToken(principalName, "password"); // Authorization Response this.mockMvc.perform(get("/github-repos") .param(OAuth2ParameterNames.CODE, "code") .param(OAuth2ParameterNames.STATE, "state") - .with(user(principalName)) + .with(authentication(authentication)) .session(session)) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl("http://localhost/github-repos")); - OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient( - registration.getRegistrationId(), principalName); + OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient( + registration.getRegistrationId(), authentication, request); assertThat(authorizedClient).isNotNull(); } @@ -164,5 +171,15 @@ private OAuth2AccessTokenResponseClient acc @ComponentScan(basePackages = "sample.web") @Import(WebClientConfig.class) public static class SpringBootApplicationTestConfig { + + @Bean + public OAuth2AuthorizedClientService authorizedClientService(ClientRegistrationRepository clientRegistrationRepository) { + return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository); + } + + @Bean + public OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) { + return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService); + } } } diff --git a/samples/boot/authcodegrant/src/main/java/sample/config/SecurityConfig.java b/samples/boot/authcodegrant/src/main/java/sample/config/SecurityConfig.java index e755450b2d8..3a47ac24c26 100644 --- a/samples/boot/authcodegrant/src/main/java/sample/config/SecurityConfig.java +++ b/samples/boot/authcodegrant/src/main/java/sample/config/SecurityConfig.java @@ -22,6 +22,9 @@ import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.provisioning.InMemoryUserDetailsManager; /** @@ -43,6 +46,11 @@ protected void configure(HttpSecurity http) throws Exception { .authorizationCodeGrant(); } + @Bean + public OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) { + return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService); + } + @Bean public UserDetailsService userDetailsService() { UserDetails userDetails = User.withDefaultPasswordEncoder() diff --git a/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java b/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java index 68fb179c61a..5bb0e81539a 100644 --- a/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java +++ b/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,9 @@ import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; @@ -403,12 +405,14 @@ private OAuth2UserService mockUserService() { @ComponentScan(basePackages = "sample.web") public static class SpringBootApplicationTestConfig { - @Autowired - private ClientRegistrationRepository clientRegistrationRepository; + @Bean + public OAuth2AuthorizedClientService authorizedClientService(ClientRegistrationRepository clientRegistrationRepository) { + return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository); + } @Bean - public OAuth2AuthorizedClientService authorizedClientService() { - return new InMemoryOAuth2AuthorizedClientService(this.clientRegistrationRepository); + public OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) { + return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService); } } } diff --git a/samples/boot/oauth2login/src/main/java/sample/OAuth2LoginApplication.java b/samples/boot/oauth2login/src/main/java/sample/OAuth2LoginApplication.java index 14b247827f3..1682cfa0356 100644 --- a/samples/boot/oauth2login/src/main/java/sample/OAuth2LoginApplication.java +++ b/samples/boot/oauth2login/src/main/java/sample/OAuth2LoginApplication.java @@ -17,6 +17,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; /** * @author Joe Grandja @@ -27,4 +31,9 @@ public class OAuth2LoginApplication { public static void main(String[] args) { SpringApplication.run(OAuth2LoginApplication.class, args); } + + @Bean + public OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) { + return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService); + } } From 71fde4fddeabebf40ad962a0c362898f087e01bb Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Fri, 20 Jul 2018 09:41:56 -0400 Subject: [PATCH 145/226] Use context.getBean() for ClientRegistrationRepository Fixes gh-5538 --- .../oauth2/client/OAuth2ClientConfigurerUtils.java | 12 +----------- .../OAuth2ClientConfigurationTests.java | 3 +-- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java index 1e16a73da0c..140ad70c321 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java @@ -16,7 +16,6 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.client; import org.springframework.beans.factory.BeanFactoryUtils; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; @@ -51,16 +50,7 @@ static > ClientRegistrationRepository getClient } private static > ClientRegistrationRepository getClientRegistrationRepositoryBean(B builder) { - Map clientRegistrationRepositoryMap = BeanFactoryUtils.beansOfTypeIncludingAncestors( - builder.getSharedObject(ApplicationContext.class), ClientRegistrationRepository.class); - if (clientRegistrationRepositoryMap.isEmpty()) { - throw new NoSuchBeanDefinitionException(ClientRegistrationRepository.class); - } else if (clientRegistrationRepositoryMap.size() > 1) { - throw new NoUniqueBeanDefinitionException(ClientRegistrationRepository.class, clientRegistrationRepositoryMap.size(), - "Expected single matching bean of type '" + ClientRegistrationRepository.class.getName() + "' but found " + - clientRegistrationRepositoryMap.size() + ": " + StringUtils.collectionToCommaDelimitedString(clientRegistrationRepositoryMap.keySet())); - } - return clientRegistrationRepositoryMap.values().iterator().next(); + return builder.getSharedObject(ApplicationContext.class).getBean(ClientRegistrationRepository.class); } static > OAuth2AuthorizedClientRepository getAuthorizedClientRepository(B builder) { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java index c2098cd86be..524f4c57d86 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java @@ -171,8 +171,7 @@ protected void configure(HttpSecurity http) throws Exception { public void loadContextWhenClientRegistrationRepositoryRegisteredTwiceThenThrowNoUniqueBeanDefinitionException() { assertThatThrownBy(() -> this.spring.register(ClientRegistrationRepositoryRegisteredTwiceConfig.class).autowire()) .hasRootCauseInstanceOf(NoUniqueBeanDefinitionException.class) - .hasMessageContaining("Expected single matching bean of type '" + ClientRegistrationRepository.class.getName() + - "' but found 2: clientRegistrationRepository1,clientRegistrationRepository2"); + .hasMessageContaining("expected single matching bean but found 2: clientRegistrationRepository1,clientRegistrationRepository2"); } @EnableWebMvc From 827109c2db2b62d0e93a4106c99de96dea082f65 Mon Sep 17 00:00:00 2001 From: "mhyeon.lee" Date: Wed, 11 Jul 2018 23:52:00 +0900 Subject: [PATCH 146/226] Add AuthenticationMethod type This section defines three methods of sending bearer access tokens in resource requests to resource servers. Clients MUST NOT use more than one method to transmit the token in each request. RFC6750 Section 2 Authenticated Requests https://tools.ietf.org/html/rfc6750#section-2 Add AuthenticationMethod in ClientRegistration UserInfoEndpoint. Add AuthenticationMethod for OAuth2UserService to get User. To support the use of the POST method. https://tools.ietf.org/html/rfc6750#section-2.2 gh-5500 --- .../registration/ClientRegistration.java | 26 ++++++ .../DefaultReactiveOAuth2UserService.java | 21 ++++- .../NimbusUserInfoResponseClient.java | 6 +- .../oidc/userinfo/OidcUserServiceTests.java | 74 ++++++++++++++++- .../registration/ClientRegistrationTests.java | 23 ++++++ .../DefaultOAuth2UserServiceTests.java | 80 ++++++++++++++++++- ...DefaultReactiveOAuth2UserServiceTests.java | 50 ++++++++++++ .../oauth2/core/AuthenticationMethod.java | 72 +++++++++++++++++ .../core/AuthenticationMethodTests.java | 48 +++++++++++ 9 files changed, 394 insertions(+), 6 deletions(-) create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthenticationMethod.java create mode 100644 oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthenticationMethodTests.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java index bf390f8b94f..4b9bb1e24b2 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java @@ -15,6 +15,7 @@ */ package org.springframework.security.oauth2.client.registration; +import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.oidc.OidcScopes; @@ -197,6 +198,7 @@ public String getJwkSetUri() { */ public class UserInfoEndpoint { private String uri; + private AuthenticationMethod authenticationMethod = AuthenticationMethod.HEADER; private String userNameAttributeName; private UserInfoEndpoint() { @@ -211,6 +213,16 @@ public String getUri() { return this.uri; } + /** + * Returns the authentication method for the user info endpoint. + * + * @since 5.1 + * @return the {@link AuthenticationMethod} for the user info endpoint. + */ + public AuthenticationMethod getAuthenticationMethod() { + return this.authenticationMethod; + } + /** * Returns the attribute name used to access the user's name from the user info response. * @@ -247,6 +259,7 @@ public static class Builder { private String authorizationUri; private String tokenUri; private String userInfoUri; + private AuthenticationMethod userInfoAuthenticationMethod = AuthenticationMethod.HEADER; private String userNameAttributeName; private String jwkSetUri; private String clientName; @@ -383,6 +396,18 @@ public Builder userInfoUri(String userInfoUri) { return this; } + /** + * Sets the authentication method for the user info endpoint. + * + * @since 5.1 + * @param userInfoAuthenticationMethod the authentication method for the user info endpoint + * @return the {@link Builder} + */ + public Builder userInfoAuthenticationMethod(AuthenticationMethod userInfoAuthenticationMethod) { + this.userInfoAuthenticationMethod = userInfoAuthenticationMethod; + return this; + } + /** * Sets the attribute name used to access the user's name from the user info response. * @@ -446,6 +471,7 @@ private ClientRegistration create() { providerDetails.authorizationUri = this.authorizationUri; providerDetails.tokenUri = this.tokenUri; providerDetails.userInfoEndpoint.uri = this.userInfoUri; + providerDetails.userInfoEndpoint.authenticationMethod = this.userInfoAuthenticationMethod; providerDetails.userInfoEndpoint.userNameAttributeName = this.userNameAttributeName; providerDetails.jwkSetUri = this.jwkSetUri; clientRegistration.providerDetails = providerDetails; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java index 1f9bc10c147..533400c391d 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java @@ -26,8 +26,10 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; @@ -99,9 +101,22 @@ public Mono loadUser(OAuth2UserRequest userRequest) ParameterizedTypeReference> typeReference = new ParameterizedTypeReference>() { }; - Mono> userAttributes = this.webClient.get() - .uri(userInfoUri) - .headers(bearerToken(userRequest.getAccessToken().getTokenValue())) + AuthenticationMethod authenticationMethod = userRequest.getClientRegistration().getProviderDetails() + .getUserInfoEndpoint().getAuthenticationMethod(); + WebClient.RequestHeadersSpec requestHeadersSpec; + if (AuthenticationMethod.FORM.equals(authenticationMethod)) { + requestHeadersSpec = this.webClient.post() + .uri(userInfoUri) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .syncBody("access_token=" + userRequest.getAccessToken().getTokenValue()); + } else { + requestHeadersSpec = this.webClient.get() + .uri(userInfoUri) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .headers(bearerToken(userRequest.getAccessToken().getTokenValue())); + } + Mono> userAttributes = requestHeadersSpec .retrieve() .onStatus(s -> s != HttpStatus.OK, response -> { return parse(response).map(userInfoErrorResponse -> { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/NimbusUserInfoResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/NimbusUserInfoResponseClient.java index 52929a04a73..cbbe7597882 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/NimbusUserInfoResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/NimbusUserInfoResponseClient.java @@ -32,6 +32,7 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; @@ -79,8 +80,11 @@ private ClientHttpResponse getUserInfoResponse(ClientRegistration clientRegistra OAuth2AccessToken oauth2AccessToken) throws OAuth2AuthenticationException { URI userInfoUri = URI.create(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri()); BearerAccessToken accessToken = new BearerAccessToken(oauth2AccessToken.getTokenValue()); + AuthenticationMethod authenticationMethod = clientRegistration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod(); + HTTPRequest.Method httpMethod = AuthenticationMethod.FORM.equals(authenticationMethod) + ? HTTPRequest.Method.POST : HTTPRequest.Method.GET; - UserInfoRequest userInfoRequest = new UserInfoRequest(userInfoUri, accessToken); + UserInfoRequest userInfoRequest = new UserInfoRequest(userInfoUri, httpMethod, accessToken); HTTPRequest httpRequest = userInfoRequest.toHTTPRequest(); httpRequest.setAccept(MediaType.APPLICATION_JSON_VALUE); httpRequest.setConnectTimeout(30000); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java index 6164022292e..1586292af63 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java @@ -17,6 +17,8 @@ import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -26,9 +28,11 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; @@ -56,7 +60,7 @@ * * @author Joe Grandja */ -@PowerMockIgnore("okhttp3.*") +@PowerMockIgnore({"okhttp3.*", "okio.Buffer"}) @PrepareForTest(ClientRegistration.class) @RunWith(PowerMockRunner.class) public class OidcUserServiceTests { @@ -79,6 +83,7 @@ public void setUp() throws Exception { when(this.providerDetails.getUserInfoEndpoint()).thenReturn(this.userInfoEndpoint); when(this.clientRegistration.getAuthorizationGrantType()).thenReturn(AuthorizationGrantType.AUTHORIZATION_CODE); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn(StandardClaimNames.SUB); this.accessToken = mock(OAuth2AccessToken.class); @@ -354,4 +359,71 @@ public void loadUserWhenUserInfoSuccessResponseThenAcceptHeaderJson() throws Exc assertThat(server.takeRequest(1, TimeUnit.SECONDS).getHeader(HttpHeaders.ACCEPT)) .isEqualTo(MediaType.APPLICATION_JSON_VALUE); } + + // gh-5500 + @Test + public void loadUserWhenAuthenticationMethodHeaderSuccessResponseThenHttpMethodGet() throws Exception { + MockWebServer server = new MockWebServer(); + + String userInfoResponse = "{\n" + + " \"sub\": \"subject1\",\n" + + " \"name\": \"first last\",\n" + + " \"given_name\": \"first\",\n" + + " \"family_name\": \"last\",\n" + + " \"preferred_username\": \"user1\",\n" + + " \"email\": \"user1@example.com\"\n" + + "}\n"; + server.enqueue(new MockResponse() + .setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .setBody(userInfoResponse)); + + server.start(); + + String userInfoUri = server.url("/user").toString(); + + when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); + when(this.accessToken.getTokenValue()).thenReturn("access-token"); + + this.userService.loadUser(new OidcUserRequest(this.clientRegistration, this.accessToken, this.idToken)); + server.shutdown(); + RecordedRequest request = server.takeRequest(); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET.name()); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(request.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + this.accessToken.getTokenValue()); + } + + // gh-5500 + @Test + public void loadUserWhenAuthenticationMethodFormSuccessResponseThenHttpMethodPost() throws Exception { + MockWebServer server = new MockWebServer(); + + String userInfoResponse = "{\n" + + " \"sub\": \"subject1\",\n" + + " \"name\": \"first last\",\n" + + " \"given_name\": \"first\",\n" + + " \"family_name\": \"last\",\n" + + " \"preferred_username\": \"user1\",\n" + + " \"email\": \"user1@example.com\"\n" + + "}\n"; + server.enqueue(new MockResponse() + .setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .setBody(userInfoResponse)); + + server.start(); + + String userInfoUri = server.url("/user").toString(); + + when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.FORM); + when(this.accessToken.getTokenValue()).thenReturn("access-token"); + + this.userService.loadUser(new OidcUserRequest(this.clientRegistration, this.accessToken, this.idToken)); + server.shutdown(); + RecordedRequest request = server.takeRequest(); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST.name()); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(request.getHeader(HttpHeaders.CONTENT_TYPE)).contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE); + assertThat(request.getBody().readUtf8()).isEqualTo("access_token=" + this.accessToken.getTokenValue()); + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java index 0a9c096950b..93a30b0505d 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.client.registration; import org.junit.Test; +import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; @@ -52,6 +53,7 @@ public void buildWhenAuthorizationGrantTypeIsNullThenThrowIllegalArgumentExcepti .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -68,6 +70,7 @@ public void buildWhenAuthorizationCodeGrantAllAttributesProvidedThenAllAttribute .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -81,6 +84,7 @@ public void buildWhenAuthorizationCodeGrantAllAttributesProvidedThenAllAttribute assertThat(registration.getScopes()).isEqualTo(SCOPES); assertThat(registration.getProviderDetails().getAuthorizationUri()).isEqualTo(AUTHORIZATION_URI); assertThat(registration.getProviderDetails().getTokenUri()).isEqualTo(TOKEN_URI); + assertThat(registration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod()).isEqualTo(AuthenticationMethod.FORM); assertThat(registration.getProviderDetails().getJwkSetUri()).isEqualTo(JWK_SET_URI); assertThat(registration.getClientName()).isEqualTo(CLIENT_NAME); } @@ -96,6 +100,7 @@ public void buildWhenAuthorizationCodeGrantRegistrationIdIsNullThenThrowIllegalA .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -112,6 +117,7 @@ public void buildWhenAuthorizationCodeGrantClientIdIsNullThenThrowIllegalArgumen .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -128,6 +134,7 @@ public void buildWhenAuthorizationCodeGrantClientSecretIsNullThenThrowIllegalArg .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -144,6 +151,7 @@ public void buildWhenAuthorizationCodeGrantClientAuthenticationMethodIsNullThenT .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -160,6 +168,7 @@ public void buildWhenAuthorizationCodeGrantRedirectUriIsNullThenThrowIllegalArgu .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -177,6 +186,7 @@ public void buildWhenAuthorizationCodeGrantScopeIsNullThenScopeNotRequired() { .scope((String[]) null) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -193,6 +203,7 @@ public void buildWhenAuthorizationCodeGrantAuthorizationUriIsNullThenThrowIllega .scope(SCOPES.toArray(new String[0])) .authorizationUri(null) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -209,6 +220,7 @@ public void buildWhenAuthorizationCodeGrantTokenUriIsNullThenThrowIllegalArgumen .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(null) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -225,6 +237,7 @@ public void buildWhenAuthorizationCodeGrantJwkSetUriIsNullThenThrowIllegalArgume .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(null) .clientName(CLIENT_NAME) .build(); @@ -241,6 +254,7 @@ public void buildWhenAuthorizationCodeGrantClientNameIsNullThenThrowIllegalArgum .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(null) .build(); @@ -256,6 +270,7 @@ public void buildWhenAuthorizationCodeGrantScopeDoesNotContainOpenidThenJwkSetUr .redirectUriTemplate(REDIRECT_URI) .scope("scope1") .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .tokenUri(TOKEN_URI) .clientName(CLIENT_NAME) .build(); @@ -284,6 +299,7 @@ public void buildWhenImplicitGrantAllAttributesProvidedThenAllAttributesAreSet() .redirectUriTemplate(REDIRECT_URI) .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .clientName(CLIENT_NAME) .build(); @@ -293,6 +309,7 @@ public void buildWhenImplicitGrantAllAttributesProvidedThenAllAttributesAreSet() assertThat(registration.getRedirectUriTemplate()).isEqualTo(REDIRECT_URI); assertThat(registration.getScopes()).isEqualTo(SCOPES); assertThat(registration.getProviderDetails().getAuthorizationUri()).isEqualTo(AUTHORIZATION_URI); + assertThat(registration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod()).isEqualTo(AuthenticationMethod.FORM); assertThat(registration.getClientName()).isEqualTo(CLIENT_NAME); } @@ -304,6 +321,7 @@ public void buildWhenImplicitGrantRegistrationIdIsNullThenThrowIllegalArgumentEx .redirectUriTemplate(REDIRECT_URI) .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .clientName(CLIENT_NAME) .build(); } @@ -316,6 +334,7 @@ public void buildWhenImplicitGrantClientIdIsNullThenThrowIllegalArgumentExceptio .redirectUriTemplate(REDIRECT_URI) .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .clientName(CLIENT_NAME) .build(); } @@ -328,6 +347,7 @@ public void buildWhenImplicitGrantRedirectUriIsNullThenThrowIllegalArgumentExcep .redirectUriTemplate(null) .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .clientName(CLIENT_NAME) .build(); } @@ -341,6 +361,7 @@ public void buildWhenImplicitGrantScopeIsNullThenScopeNotRequired() { .redirectUriTemplate(REDIRECT_URI) .scope((String[]) null) .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .clientName(CLIENT_NAME) .build(); } @@ -353,6 +374,7 @@ public void buildWhenImplicitGrantAuthorizationUriIsNullThenThrowIllegalArgument .redirectUriTemplate(REDIRECT_URI) .scope(SCOPES.toArray(new String[0])) .authorizationUri(null) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .clientName(CLIENT_NAME) .build(); } @@ -365,6 +387,7 @@ public void buildWhenImplicitGrantClientNameIsNullThenThrowIllegalArgumentExcept .redirectUriTemplate(REDIRECT_URI) .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .clientName(null) .build(); } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserServiceTests.java index a85a99f2155..99ca960c2ee 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserServiceTests.java @@ -17,6 +17,8 @@ import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -26,9 +28,11 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.user.OAuth2User; @@ -46,7 +50,7 @@ * * @author Joe Grandja */ -@PowerMockIgnore("okhttp3.*") +@PowerMockIgnore({"okhttp3.*", "okio.Buffer"}) @PrepareForTest(ClientRegistration.class) @RunWith(PowerMockRunner.class) public class DefaultOAuth2UserServiceTests { @@ -115,6 +119,7 @@ public void loadUserWhenUserInfoSuccessResponseThenReturnUser() throws Exception String userInfoUri = server.url("/user").toString(); when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn("user-name"); when(this.accessToken.getTokenValue()).thenReturn("access-token"); @@ -162,6 +167,7 @@ public void loadUserWhenUserInfoSuccessResponseInvalidThenThrowOAuth2Authenticat String userInfoUri = server.url("/user").toString(); when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn("user-name"); when(this.accessToken.getTokenValue()).thenReturn("access-token"); @@ -184,6 +190,7 @@ public void loadUserWhenUserInfoErrorResponseThenThrowOAuth2AuthenticationExcept String userInfoUri = server.url("/user").toString(); when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn("user-name"); when(this.accessToken.getTokenValue()).thenReturn("access-token"); @@ -201,6 +208,7 @@ public void loadUserWhenUserInfoUriInvalidThenThrowAuthenticationServiceExceptio String userInfoUri = "http://invalid-provider.com/user"; when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn("user-name"); when(this.accessToken.getTokenValue()).thenReturn("access-token"); @@ -229,6 +237,7 @@ public void loadUserWhenUserInfoSuccessResponseThenAcceptHeaderJson() throws Exc String userInfoUri = server.url("/user").toString(); when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn("user-name"); when(this.accessToken.getTokenValue()).thenReturn("access-token"); @@ -237,4 +246,73 @@ public void loadUserWhenUserInfoSuccessResponseThenAcceptHeaderJson() throws Exc assertThat(server.takeRequest(1, TimeUnit.SECONDS).getHeader(HttpHeaders.ACCEPT)) .isEqualTo(MediaType.APPLICATION_JSON_VALUE); } + + // gh-5500 + @Test + public void loadUserWhenAuthenticationMethodHeaderSuccessResponseThenHttpMethodGet() throws Exception { + MockWebServer server = new MockWebServer(); + + String userInfoResponse = "{\n" + + " \"user-name\": \"user1\",\n" + + " \"first-name\": \"first\",\n" + + " \"last-name\": \"last\",\n" + + " \"middle-name\": \"middle\",\n" + + " \"address\": \"address\",\n" + + " \"email\": \"user1@example.com\"\n" + + "}\n"; + server.enqueue(new MockResponse() + .setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .setBody(userInfoResponse)); + + server.start(); + + String userInfoUri = server.url("/user").toString(); + + when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); + when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn("user-name"); + when(this.accessToken.getTokenValue()).thenReturn("access-token"); + + this.userService.loadUser(new OAuth2UserRequest(this.clientRegistration, this.accessToken)); + server.shutdown(); + RecordedRequest request = server.takeRequest(); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET.name()); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(request.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + this.accessToken.getTokenValue()); + } + + // gh-5500 + @Test + public void loadUserWhenAuthenticationMethodFormSuccessResponseThenHttpMethodPost() throws Exception { + MockWebServer server = new MockWebServer(); + + String userInfoResponse = "{\n" + + " \"user-name\": \"user1\",\n" + + " \"first-name\": \"first\",\n" + + " \"last-name\": \"last\",\n" + + " \"middle-name\": \"middle\",\n" + + " \"address\": \"address\",\n" + + " \"email\": \"user1@example.com\"\n" + + "}\n"; + server.enqueue(new MockResponse() + .setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .setBody(userInfoResponse)); + + server.start(); + + String userInfoUri = server.url("/user").toString(); + + when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.FORM); + when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn("user-name"); + when(this.accessToken.getTokenValue()).thenReturn("access-token"); + + this.userService.loadUser(new OAuth2UserRequest(this.clientRegistration, this.accessToken)); + server.shutdown(); + RecordedRequest request = server.takeRequest(); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST.name()); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(request.getHeader(HttpHeaders.CONTENT_TYPE)).contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE); + assertThat(request.getBody().readUtf8()).isEqualTo("access_token=" + this.accessToken.getTokenValue()); + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserServiceTests.java index 70090f3d896..be57a92114a 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserServiceTests.java @@ -22,15 +22,19 @@ import org.junit.Before; import org.junit.Test; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; + +import okhttp3.mockwebserver.RecordedRequest; import reactor.test.StepVerifier; import java.time.Duration; @@ -67,6 +71,7 @@ public void setup() throws Exception { .authorizationUri("https://github.com/login/oauth/authorize") .tokenUri("https://github.com/login/oauth/access_token") .userInfoUri(userInfoUri) + .userInfoAuthenticationMethod(AuthenticationMethod.HEADER) .userNameAttributeName("user-name") .clientName("GitHub") .clientId("clientId") @@ -140,6 +145,51 @@ public void loadUserWhenUserInfoSuccessResponseThenReturnUser() throws Exception assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes()); } + // gh-5500 + @Test + public void loadUserWhenAuthenticationMethodHeaderSuccessResponseThenHttpMethodGet() throws Exception { + this.clientRegistration.userInfoAuthenticationMethod(AuthenticationMethod.HEADER); + String userInfoResponse = "{\n" + + " \"user-name\": \"user1\",\n" + + " \"first-name\": \"first\",\n" + + " \"last-name\": \"last\",\n" + + " \"middle-name\": \"middle\",\n" + + " \"address\": \"address\",\n" + + " \"email\": \"user1@example.com\"\n" + + "}\n"; + enqueueApplicationJsonBody(userInfoResponse); + + this.userService.loadUser(oauth2UserRequest()).block(); + + RecordedRequest request = this.server.takeRequest(); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET.name()); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(request.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + this.accessToken.getTokenValue()); + } + + // gh-5500 + @Test + public void loadUserWhenAuthenticationMethodFormSuccessResponseThenHttpMethodPost() throws Exception { + this.clientRegistration.userInfoAuthenticationMethod( AuthenticationMethod.FORM); + String userInfoResponse = "{\n" + + " \"user-name\": \"user1\",\n" + + " \"first-name\": \"first\",\n" + + " \"last-name\": \"last\",\n" + + " \"middle-name\": \"middle\",\n" + + " \"address\": \"address\",\n" + + " \"email\": \"user1@example.com\"\n" + + "}\n"; + enqueueApplicationJsonBody(userInfoResponse); + + this.userService.loadUser(oauth2UserRequest()).block(); + + RecordedRequest request = this.server.takeRequest(); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST.name()); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(request.getHeader(HttpHeaders.CONTENT_TYPE)).contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE); + assertThat(request.getBody().readUtf8()).isEqualTo("access_token=" + this.accessToken.getTokenValue()); + } + @Test public void loadUserWhenUserInfoSuccessResponseInvalidThenThrowOAuth2AuthenticationException() throws Exception { String userInfoResponse = "{\n" + diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthenticationMethod.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthenticationMethod.java new file mode 100644 index 00000000000..e74ee7ef98c --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthenticationMethod.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.core; + +import java.io.Serializable; + +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.util.Assert; + +/** + * The authentication method used when sending bearer access tokens in resource requests to resource servers. + * + * @author MyeongHyeon Lee + * @since 5.1 + * @see Section 2 Authenticated Requests + */ +public final class AuthenticationMethod implements Serializable { + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + public static final AuthenticationMethod HEADER = new AuthenticationMethod("header"); + public static final AuthenticationMethod FORM = new AuthenticationMethod("form"); + public static final AuthenticationMethod QUERY = new AuthenticationMethod("query"); + private final String value; + + /** + * Constructs an {@code AuthenticationMethod} using the provided value. + * + * @param value the value of the authentication method type + */ + public AuthenticationMethod(String value) { + Assert.hasText(value, "value cannot be empty"); + this.value = value; + } + + /** + * Returns the value of the authentication method type. + * + * @return the value of the authentication method type + */ + public String getValue() { + return this.value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + AuthenticationMethod that = (AuthenticationMethod) obj; + return this.getValue().equals(that.getValue()); + } + + @Override + public int hashCode() { + return this.getValue().hashCode(); + } +} diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthenticationMethodTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthenticationMethodTests.java new file mode 100644 index 00000000000..4c541cf1506 --- /dev/null +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthenticationMethodTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.core; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Test; + +/** + * Tests for {@link AuthenticationMethod}. + * + * @author MyeongHyeon Lee + */ +public class AuthenticationMethodTests { + + @Test + public void constructorWhenValueIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new AuthenticationMethod(null)).hasMessage("value cannot be empty"); + } + + @Test + public void getValueWhenHeaderAuthenticationTypeThenReturnHeader() { + assertThat(AuthenticationMethod.HEADER.getValue()).isEqualTo("header"); + } + + @Test + public void getValueWhenFormAuthenticationTypeThenReturnForm() { + assertThat(AuthenticationMethod.FORM.getValue()).isEqualTo("form"); + } + + @Test + public void getValueWhenFormAuthenticationTypeThenReturnQuery() { + assertThat(AuthenticationMethod.QUERY.getValue()).isEqualTo("query"); + } +} From 8758abb7931c1c3a71678b742c06f0a12470910e Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 16 Jul 2018 17:42:29 -0600 Subject: [PATCH 147/226] User-Specified JwtDecoder This exposes JwtConfigurer#decoder as well as makes the configurer look in the application context for a bean of type JwtDecoder. Fixes: gh-5519 --- .../OAuth2ResourceServerConfigurer.java | 66 ++++-- .../OAuth2ResourceServerConfigurerTests.java | 194 +++++++++++++++++- 2 files changed, 238 insertions(+), 22 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java index de610cfa590..ae3db1588b5 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java @@ -18,8 +18,8 @@ import javax.servlet.http.HttpServletRequest; +import org.springframework.context.ApplicationContext; import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; @@ -51,7 +51,19 @@ *
    * *

    - * When using {@link #jwt()}, a Jwk Set Uri must be supplied via {@link JwtConfigurer#jwkSetUri} + * When using {@link #jwt()}, either + * + *

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

    Security Filters

    * @@ -77,10 +89,6 @@ *
  • {@link AuthenticationManager}
  • * * - * If {@link #jwt()} isn't supplied, then the {@link BearerTokenAuthenticationFilter} is still added, but without - * any OAuth 2.0 {@link AuthenticationProvider}s. This is useful if needing to switch out Spring Security's Jwt support - * for a custom one. - * * @author Josh Cummings * @since 5.1 * @see BearerTokenAuthenticationFilter @@ -100,9 +108,14 @@ public final class OAuth2ResourceServerConfigurer decoder(JwtDecoder decoder) { + this.decoder = decoder; + return OAuth2ResourceServerConfigurer.this; + } public OAuth2ResourceServerConfigurer jwkSetUri(String uri) { this.decoder = new NimbusJwtDecoderJwkSupport(uri); return OAuth2ResourceServerConfigurer.this; } - private JwtDecoder getJwtDecoder() { + JwtDecoder getJwtDecoder() { + if ( this.decoder == null ) { + return this.context.getBean(JwtDecoder.class); + } + return this.decoder; } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index 5d3291b0489..6b89689ad96 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -20,6 +20,9 @@ import java.io.FileReader; import java.io.IOException; import java.lang.reflect.Field; +import java.time.Instant; +import java.util.Collections; +import java.util.Map; import java.util.stream.Collectors; import javax.annotation.PreDestroy; @@ -34,9 +37,11 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; @@ -55,6 +60,11 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.test.web.servlet.MockMvc; @@ -68,9 +78,13 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.support.GenericWebApplicationContext; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -86,8 +100,14 @@ * @author Josh Cummings */ public class OAuth2ResourceServerConfigurerTests { + private static final String JWT_TOKEN = "token"; + private static final String JWT_SUBJECT = "mock-test-subject"; + private static final Map JWT_HEADERS = Collections.singletonMap("alg", JwsAlgorithms.RS256); + private static final Map JWT_CLAIMS = Collections.singletonMap(JwtClaimNames.SUB, JWT_SUBJECT); + private static final Jwt JWT = new Jwt(JWT_TOKEN, Instant.MIN, Instant.MAX, JWT_HEADERS, JWT_CLAIMS); + private static final String JWK_SET_URI = "https://mock.org"; - @Autowired + @Autowired(required = false) MockMvc mvc; @Autowired(required = false) @@ -506,6 +526,130 @@ public void requestWhenSessionManagementConfiguredThenUserConfigurationOverrides assertThat(result.getRequest().getSession(false)).isNotNull(); } + // -- custom jwt decoder + + @Test + public void requestWhenCustomJwtDecoderWiredOnDslThenUsed() + throws Exception { + + this.spring.register(CustomJwtDecoderOnDsl.class, BasicController.class).autowire(); + + CustomJwtDecoderOnDsl config = this.spring.getContext().getBean(CustomJwtDecoderOnDsl.class); + JwtDecoder decoder = config.decoder(); + + when(decoder.decode(anyString())).thenReturn(JWT); + + this.mvc.perform(get("/authenticated") + .with(bearerToken(JWT_TOKEN))) + .andExpect(status().isOk()) + .andExpect(content().string(JWT_SUBJECT)); + } + + @Test + public void requestWhenCustomJwtDecoderExposedAsBeanThenUsed() + throws Exception { + + this.spring.register(CustomJwtDecoderAsBean.class, BasicController.class).autowire(); + + JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class); + + when(decoder.decode(anyString())).thenReturn(JWT); + + this.mvc.perform(get("/authenticated") + .with(bearerToken(JWT_TOKEN))) + .andExpect(status().isOk()) + .andExpect(content().string(JWT_SUBJECT)); + } + + @Test + public void getJwtDecoderWhenConfiguredWithDecoderAndJwkSetUriThenLastOneWins() { + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = + new OAuth2ResourceServerConfigurer().new JwtConfigurer(null); + + JwtDecoder decoder = mock(JwtDecoder.class); + + jwtConfigurer.jwkSetUri(JWK_SET_URI); + jwtConfigurer.decoder(decoder); + + assertThat(jwtConfigurer.getJwtDecoder()).isEqualTo(decoder); + + jwtConfigurer = + new OAuth2ResourceServerConfigurer().new JwtConfigurer(null); + + jwtConfigurer.decoder(decoder); + jwtConfigurer.jwkSetUri(JWK_SET_URI); + + assertThat(jwtConfigurer.getJwtDecoder()).isInstanceOf(NimbusJwtDecoderJwkSupport.class); + + } + + @Test + public void getJwtDecoderWhenConflictingJwtDecodersThenTheDslWiredOneTakesPrecedence() { + + JwtDecoder decoderBean = mock(JwtDecoder.class); + JwtDecoder decoder = mock(JwtDecoder.class); + + ApplicationContext context = mock(ApplicationContext.class); + when(context.getBean(JwtDecoder.class)).thenReturn(decoderBean); + + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = + new OAuth2ResourceServerConfigurer().new JwtConfigurer(context); + jwtConfigurer.decoder(decoder); + + assertThat(jwtConfigurer.getJwtDecoder()).isEqualTo(decoder); + } + + @Test + public void getJwtDecoderWhenContextHasBeanAndUserConfiguresJwkSetUriThenJwkSetUriTakesPrecedence() { + + JwtDecoder decoder = mock(JwtDecoder.class); + ApplicationContext context = mock(ApplicationContext.class); + when(context.getBean(JwtDecoder.class)).thenReturn(decoder); + + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = + new OAuth2ResourceServerConfigurer().new JwtConfigurer(context); + + jwtConfigurer.jwkSetUri(JWK_SET_URI); + + assertThat(jwtConfigurer.getJwtDecoder()).isNotEqualTo(decoder); + assertThat(jwtConfigurer.getJwtDecoder()).isInstanceOf(NimbusJwtDecoderJwkSupport.class); + } + + @Test + public void getJwtDecoderWhenTwoJwtDecoderBeansAndAnotherWiredOnDslThenDslWiredOneTakesPrecedence() { + + JwtDecoder decoderBean = mock(JwtDecoder.class); + JwtDecoder decoder = mock(JwtDecoder.class); + + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean("decoderOne", JwtDecoder.class, () -> decoderBean); + context.registerBean("decoderTwo", JwtDecoder.class, () -> decoderBean); + this.spring.context(context).autowire(); + + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = + new OAuth2ResourceServerConfigurer().new JwtConfigurer(context); + jwtConfigurer.decoder(decoder); + + assertThat(jwtConfigurer.getJwtDecoder()).isEqualTo(decoder); + } + + @Test + public void getJwtDecoderWhenTwoJwtDecoderBeansThenThrowsException() { + + JwtDecoder decoder = mock(JwtDecoder.class); + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean("decoderOne", JwtDecoder.class, () -> decoder); + context.registerBean("decoderTwo", JwtDecoder.class, () -> decoder); + + this.spring.context(context).autowire(); + + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = + new OAuth2ResourceServerConfigurer().new JwtConfigurer(context); + + assertThatCode(() -> jwtConfigurer.getJwtDecoder()) + .isInstanceOf(NoUniqueBeanDefinitionException.class); + } + // -- In combination with other authentication providers @Test @@ -534,7 +678,7 @@ public void configuredWhenMissingJwtAuthenticationProviderThenWiringException() assertThatCode(() -> this.spring.register(JwtlessConfig.class).autowire()) .isInstanceOf(BeanCreationException.class) - .hasMessageContaining("no instance of JwtDecoder"); + .hasMessageContaining("no Jwt configuration was found"); } @Test @@ -542,7 +686,7 @@ public void configureWhenMissingJwkSetUriThenWiringException() { assertThatCode(() -> this.spring.register(JwtHalfConfiguredConfig.class).autowire()) .isInstanceOf(BeanCreationException.class) - .hasMessageContaining("no instance of JwtDecoder"); + .hasMessageContaining("No qualifying bean of type"); } // -- support @@ -689,6 +833,50 @@ protected void configure(HttpSecurity http) throws Exception { } } + @EnableWebSecurity + static class CustomJwtDecoderOnDsl extends WebSecurityConfigurerAdapter { + JwtDecoder decoder = mock(JwtDecoder.class); + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt() + .decoder(decoder()); + // @formatter:on + } + + JwtDecoder decoder() { + return this.decoder; + } + } + + @EnableWebSecurity + static class CustomJwtDecoderAsBean extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt(); + // @formatter:on + } + + @Bean + public JwtDecoder decoder() { + return mock(JwtDecoder.class); + } + } + @RestController static class BasicController { @GetMapping("/") From 21c33edd3bee56fac4223b7e3d38d1f5f2fc639f Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 19 Jul 2018 21:46:50 -0500 Subject: [PATCH 148/226] Add ServletOAuth2AuthorizedClientExchangeFilterFunction Fixes: gh-5545 --- ...uthorizedClientExchangeFilterFunction.java | 408 +++++++++++++++ ...izedClientExchangeFilterFunctionTests.java | 478 ++++++++++++++++++ 2 files changed, 886 insertions(+) create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java new file mode 100644 index 00000000000..7001ecd8910 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java @@ -0,0 +1,408 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.web.reactive.function.client; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.util.Assert; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeFunction; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Base64; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; + +import static org.springframework.security.oauth2.core.web.reactive.function.OAuth2BodyExtractors.oauth2AccessTokenResponse; +import static org.springframework.security.web.http.SecurityHeaders.bearerToken; + +/** + * Provides an easy mechanism for using an {@link OAuth2AuthorizedClient} to make OAuth2 requests by including the + * token as a Bearer Token. It also provides mechanisms for looking up the {@link OAuth2AuthorizedClient}. This class is + * intended to be used in a servlet environment. + * + * Example usage: + * + *
    + * OAuth2AuthorizedClientExchangeFilterFunction oauth2 = new OAuth2AuthorizedClientExchangeFilterFunction(authorizedClientService);
    + * WebClient webClient = WebClient.builder()
    + *    .apply(oauth2.oauth2Configuration())
    + *    .build();
    + * Mono response = webClient
    + *    .get()
    + *    .uri(uri)
    + *    .attributes(oauth2AuthorizedClient(authorizedClient))
    + *    // ...
    + *    .retrieve()
    + *    .bodyToMono(String.class);
    + * 
    + * + * An attempt to automatically refresh the token will be made if all of the following + * are true: + * + *
      + *
    • The ReactiveOAuth2AuthorizedClientService on the + * {@link ServletOAuth2AuthorizedClientExchangeFilterFunction} is not null
    • + *
    • A refresh token is present on the OAuth2AuthorizedClient
    • + *
    • The access token will be expired in + * {@link #setAccessTokenExpiresSkew(Duration)}
    • + *
    • The {@link ReactiveSecurityContextHolder} will be used to attempt to save + * the token. If it is empty, then the principal name on the OAuth2AuthorizedClient + * will be used to create an Authentication for saving.
    • + *
    + * + * @author Rob Winch + * @since 5.1 + */ +public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implements ExchangeFilterFunction { + /** + * The request attribute name used to locate the {@link OAuth2AuthorizedClient}. + */ + private static final String OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME = OAuth2AuthorizedClient.class.getName(); + private static final String CLIENT_REGISTRATION_ID_ATTR_NAME = OAuth2AuthorizedClient.class.getName().concat(".CLIENT_REGISTRATION_ID"); + private static final String AUTHENTICATION_ATTR_NAME = Authentication.class.getName(); + private static final String HTTP_SERVLET_REQUEST_ATTR_NAME = HttpServletRequest.class.getName(); + private static final String HTTP_SERVLET_RESPONSE_ATTR_NAME = HttpServletResponse.class.getName(); + + private Clock clock = Clock.systemUTC(); + + private Duration accessTokenExpiresSkew = Duration.ofMinutes(1); + + private OAuth2AuthorizedClientRepository authorizedClientRepository; + + public ServletOAuth2AuthorizedClientExchangeFilterFunction() {} + + public ServletOAuth2AuthorizedClientExchangeFilterFunction(OAuth2AuthorizedClientRepository authorizedClientRepository) { + this.authorizedClientRepository = authorizedClientRepository; + } + + /** + * Configures the builder with {@link #defaultRequest()} and adds this as a {@link ExchangeFilterFunction} + * @return the {@link Consumer} to configure the builder + */ + public Consumer oauth2Configuration() { + return builder -> builder.defaultRequest(defaultRequest()).filter(this); + } + + /** + * Provides defaults for the {@link HttpServletRequest} and the {@link HttpServletResponse} using + * {@link RequestContextHolder}. It also provides defaults for the {@link Authentication} using + * {@link SecurityContextHolder}. It also can default the {@link OAuth2AuthorizedClient} using the + * {@link #clientRegistrationId(String)} or the {@link #authentication(Authentication)}. + * @return the {@link Consumer} to populate the attributes + */ + public Consumer> defaultRequest() { + return spec -> { + spec.attributes(attrs -> { + populateDefaultRequestResponse(attrs); + populateDefaultAuthentication(attrs); + populateDefaultOAuth2AuthorizedClient(attrs); + }); + }; + } + + /** + * Modifies the {@link ClientRequest#attributes()} to include the {@link OAuth2AuthorizedClient} to be used for + * providing the Bearer Token. + * + * @param authorizedClient the {@link OAuth2AuthorizedClient} to use. + * @return the {@link Consumer} to populate the attributes + */ + public static Consumer> oauth2AuthorizedClient(OAuth2AuthorizedClient authorizedClient) { + return attributes -> attributes.put(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME, authorizedClient); + } + + /** + * Modifies the {@link ClientRequest#attributes()} to include the {@link ClientRegistration#getRegistrationId()} to + * be used to look up the {@link OAuth2AuthorizedClient}. + * + * @param clientRegistrationId the {@link ClientRegistration#getRegistrationId()} to + * be used to look up the {@link OAuth2AuthorizedClient}. + * @return the {@link Consumer} to populate the attributes + */ + public static Consumer> clientRegistrationId(String clientRegistrationId) { + return attributes -> attributes.put(CLIENT_REGISTRATION_ID_ATTR_NAME, clientRegistrationId); + } + + /** + * Modifies the {@link ClientRequest#attributes()} to include the {@link Authentication} used to + * look up and save the {@link OAuth2AuthorizedClient}. The value is defaulted in + * {@link ServletOAuth2AuthorizedClientExchangeFilterFunction#defaultRequest()} + * + * @param authentication the {@link Authentication} to use. + * @return the {@link Consumer} to populate the attributes + */ + public static Consumer> authentication(Authentication authentication) { + return attributes -> attributes.put(AUTHENTICATION_ATTR_NAME, authentication); + } + + /** + * Modifies the {@link ClientRequest#attributes()} to include the {@link HttpServletRequest} used to + * look up and save the {@link OAuth2AuthorizedClient}. The value is defaulted in + * {@link ServletOAuth2AuthorizedClientExchangeFilterFunction#defaultRequest()} + * + * @param request the {@link HttpServletRequest} to use. + * @return the {@link Consumer} to populate the attributes + */ + public static Consumer> httpServletRequest(HttpServletRequest request) { + return attributes -> attributes.put(HTTP_SERVLET_REQUEST_ATTR_NAME, request); + } + + /** + * Modifies the {@link ClientRequest#attributes()} to include the {@link HttpServletResponse} used to + * save the {@link OAuth2AuthorizedClient}. The value is defaulted in + * {@link ServletOAuth2AuthorizedClientExchangeFilterFunction#defaultRequest()} + * + * @param response the {@link HttpServletResponse} to use. + * @return the {@link Consumer} to populate the attributes + */ + public static Consumer> httpServletResponse(HttpServletResponse response) { + return attributes -> attributes.put(HTTP_SERVLET_RESPONSE_ATTR_NAME, response); + } + + /** + * An access token will be considered expired by comparing its expiration to now + + * this skewed Duration. The default is 1 minute. + * @param accessTokenExpiresSkew the Duration to use. + */ + public void setAccessTokenExpiresSkew(Duration accessTokenExpiresSkew) { + Assert.notNull(accessTokenExpiresSkew, "accessTokenExpiresSkew cannot be null"); + this.accessTokenExpiresSkew = accessTokenExpiresSkew; + } + + @Override + public Mono filter(ClientRequest request, ExchangeFunction next) { + Optional attribute = request.attribute(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME) + .map(OAuth2AuthorizedClient.class::cast); + return Mono.justOrEmpty(attribute) + .flatMap(authorizedClient -> authorizedClient(request, next, authorizedClient)) + .map(authorizedClient -> bearer(request, authorizedClient)) + .flatMap(next::exchange) + .switchIfEmpty(next.exchange(request)); + } + + private void populateDefaultRequestResponse(Map attrs) { + if (attrs.containsKey(HTTP_SERVLET_REQUEST_ATTR_NAME) && attrs.containsKey( + HTTP_SERVLET_RESPONSE_ATTR_NAME)) { + return; + } + ServletRequestAttributes context = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + HttpServletRequest request = null; + HttpServletResponse response = null; + if (context != null) { + request = context.getRequest(); + response = context.getResponse(); + } + attrs.putIfAbsent(HTTP_SERVLET_REQUEST_ATTR_NAME, request); + attrs.putIfAbsent(HTTP_SERVLET_RESPONSE_ATTR_NAME, response); + } + + private void populateDefaultAuthentication(Map attrs) { + if (attrs.containsKey(AUTHENTICATION_ATTR_NAME)) { + return; + } + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + attrs.putIfAbsent(AUTHENTICATION_ATTR_NAME, authentication); + } + + private void populateDefaultOAuth2AuthorizedClient(Map attrs) { + if (this.authorizedClientRepository == null || attrs.containsKey(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME)) { + return; + } + + Authentication authentication = getAuthentication(attrs); + String clientRegistrationId = getClientRegistrationId(attrs); + if (clientRegistrationId == null && authentication instanceof OAuth2AuthenticationToken) { + clientRegistrationId = ((OAuth2AuthenticationToken) authentication).getAuthorizedClientRegistrationId(); + } + if (clientRegistrationId != null) { + HttpServletRequest request = (HttpServletRequest) attrs.get( + HTTP_SERVLET_REQUEST_ATTR_NAME); + OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository + .loadAuthorizedClient(clientRegistrationId, authentication, + request); + oauth2AuthorizedClient(authorizedClient).accept(attrs); + } + } + + private Mono authorizedClient(ClientRequest request, ExchangeFunction next, OAuth2AuthorizedClient authorizedClient) { + if (shouldRefresh(authorizedClient)) { + return refreshAuthorizedClient(request, next, authorizedClient); + } + return Mono.just(authorizedClient); + } + + private Mono refreshAuthorizedClient(ClientRequest request, ExchangeFunction next, + OAuth2AuthorizedClient authorizedClient) { + ClientRegistration clientRegistration = authorizedClient + .getClientRegistration(); + String tokenUri = clientRegistration + .getProviderDetails().getTokenUri(); + ClientRequest refreshRequest = ClientRequest.create(HttpMethod.POST, URI.create(tokenUri)) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .headers(httpBasic(clientRegistration.getClientId(), clientRegistration.getClientSecret())) + .body(refreshTokenBody(authorizedClient.getRefreshToken().getTokenValue())) + .build(); + return next.exchange(refreshRequest) + .flatMap(response -> response.body(oauth2AccessTokenResponse())) + .map(accessTokenResponse -> new OAuth2AuthorizedClient(authorizedClient.getClientRegistration(), authorizedClient.getPrincipalName(), accessTokenResponse.getAccessToken(), accessTokenResponse.getRefreshToken())) + .map(result -> { + Authentication principal = (Authentication) request.attribute( + AUTHENTICATION_ATTR_NAME).orElse(new PrincipalNameAuthentication(authorizedClient.getPrincipalName())); + HttpServletRequest httpRequest = (HttpServletRequest) request.attributes().get( + HTTP_SERVLET_REQUEST_ATTR_NAME); + HttpServletResponse httpResponse = (HttpServletResponse) request.attributes().get( + HTTP_SERVLET_RESPONSE_ATTR_NAME); + this.authorizedClientRepository.saveAuthorizedClient(result, principal, httpRequest, httpResponse); + return result; + }) + .publishOn(Schedulers.elastic()); + } + + private static Consumer httpBasic(String username, String password) { + return httpHeaders -> { + String credentialsString = username + ":" + password; + byte[] credentialBytes = credentialsString.getBytes(StandardCharsets.ISO_8859_1); + byte[] encodedBytes = Base64.getEncoder().encode(credentialBytes); + String encodedCredentials = new String(encodedBytes, StandardCharsets.ISO_8859_1); + httpHeaders.set(HttpHeaders.AUTHORIZATION, "Basic " + encodedCredentials); + }; + } + + private boolean shouldRefresh(OAuth2AuthorizedClient authorizedClient) { + if (this.authorizedClientRepository == null) { + return false; + } + OAuth2RefreshToken refreshToken = authorizedClient.getRefreshToken(); + if (refreshToken == null) { + return false; + } + Instant now = this.clock.instant(); + Instant expiresAt = authorizedClient.getAccessToken().getExpiresAt(); + if (now.isAfter(expiresAt.minus(this.accessTokenExpiresSkew))) { + return true; + } + return false; + } + + private ClientRequest bearer(ClientRequest request, OAuth2AuthorizedClient authorizedClient) { + return ClientRequest.from(request) + .headers(bearerToken(authorizedClient.getAccessToken().getTokenValue())) + .build(); + } + + private static BodyInserters.FormInserter refreshTokenBody(String refreshToken) { + return BodyInserters + .fromFormData("grant_type", AuthorizationGrantType.REFRESH_TOKEN.getValue()) + .with("refresh_token", refreshToken); + } + + static OAuth2AuthorizedClient getOAuth2AuthorizedClient(Map attrs) { + return (OAuth2AuthorizedClient) attrs.get(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME); + } + + static String getClientRegistrationId(Map attrs) { + return (String) attrs.get(CLIENT_REGISTRATION_ID_ATTR_NAME); + } + + static Authentication getAuthentication(Map attrs) { + return (Authentication) attrs.get(AUTHENTICATION_ATTR_NAME); + } + + static HttpServletRequest getRequest(Map attrs) { + return (HttpServletRequest) attrs.get(HTTP_SERVLET_REQUEST_ATTR_NAME); + } + + static HttpServletResponse getResponse(Map attrs) { + return (HttpServletResponse) attrs.get(HTTP_SERVLET_RESPONSE_ATTR_NAME); + } + + private static class PrincipalNameAuthentication implements Authentication { + private final String username; + + private PrincipalNameAuthentication(String username) { + this.username = username; + } + + @Override + public Collection getAuthorities() { + throw unsupported(); + } + + @Override + public Object getCredentials() { + throw unsupported(); + } + + @Override + public Object getDetails() { + throw unsupported(); + } + + @Override + public Object getPrincipal() { + throw unsupported(); + } + + @Override + public boolean isAuthenticated() { + throw unsupported(); + } + + @Override + public void setAuthenticated(boolean isAuthenticated) + throws IllegalArgumentException { + throw unsupported(); + } + + @Override + public String getName() { + return this.username; + } + + private UnsupportedOperationException unsupported() { + return new UnsupportedOperationException("Not Supported"); + } + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java new file mode 100644 index 00000000000..a8e2b93dfc4 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java @@ -0,0 +1,478 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.web.reactive.function.client; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.core.codec.ByteBufferEncoder; +import org.springframework.core.codec.CharSequenceEncoder; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.codec.EncoderHttpMessageWriter; +import org.springframework.http.codec.FormHttpMessageWriter; +import org.springframework.http.codec.HttpMessageWriter; +import org.springframework.http.codec.ResourceHttpMessageWriter; +import org.springframework.http.codec.ServerSentEventHttpMessageWriter; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.http.codec.multipart.MultipartHttpMessageWriter; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.mock.http.client.reactive.MockClientHttpRequest; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.reactive.function.BodyInserter; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.net.URI; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.*; + +/** + * @author Rob Winch + * @since 5.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests { + @Mock + private OAuth2AuthorizedClientRepository authorizedClientRepository; + @Mock + private WebClient.RequestHeadersSpec spec; + @Captor + private ArgumentCaptor>> attrs; + + /** + * Used for get the attributes from defaultRequest. + */ + private Map result = new HashMap<>(); + + private ServletOAuth2AuthorizedClientExchangeFilterFunction function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(); + + private MockExchangeFunction exchange = new MockExchangeFunction(); + + private Authentication authentication; + + private ClientRegistration github = ClientRegistration.withRegistrationId("github") + .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .scope("read:user") + .authorizationUri("https://github.com/login/oauth/authorize") + .tokenUri("https://github.com/login/oauth/access_token") + .userInfoUri("https://api.github.com/user") + .userNameAttributeName("id") + .clientName("GitHub") + .clientId("clientId") + .clientSecret("clientSecret") + .build(); + + private OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + "token-0", + Instant.now(), + Instant.now().plus(Duration.ofDays(1))); + + @Before + public void setup() { + this.authentication = new TestingAuthenticationToken("test", "this"); + } + + @After + public void cleanup() { + SecurityContextHolder.clearContext(); + RequestContextHolder.resetRequestAttributes(); + } + + @Test + public void defaultRequestRequestResponseWhenNullRequestContextThenRequestAndResponseNull() { + Map attrs = getDefaultRequestAttributes(); + assertThat(getRequest(attrs)).isNull(); + assertThat(getResponse(attrs)).isNull(); + } + + @Test + public void defaultRequestRequestResponseWhenRequestContextThenRequestAndResponseSet() { + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, response)); + Map attrs = getDefaultRequestAttributes(); + assertThat(getRequest(attrs)).isEqualTo(request); + assertThat(getResponse(attrs)).isEqualTo(response); + } + + @Test + public void defaultRequestAuthenticationWhenSecurityContextEmptyThenAuthenticationNull() { + Map attrs = getDefaultRequestAttributes(); + assertThat(getAuthentication(attrs)).isNull(); + } + + @Test + public void defaultRequestAuthenticationWhenAuthenticationSetThenAuthenticationSet() { + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + SecurityContextHolder.getContext().setAuthentication(this.authentication); + Map attrs = getDefaultRequestAttributes(); + assertThat(getAuthentication(attrs)).isEqualTo(this.authentication); + verifyZeroInteractions(this.authorizedClientRepository); + } + + @Test + public void defaultRequestOAuth2AuthorizedClientWhenOAuth2AuthorizationClientAndClientIdThenNotOverride() { + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken); + oauth2AuthorizedClient(authorizedClient).accept(this.result); + Map attrs = getDefaultRequestAttributes(); + assertThat(getOAuth2AuthorizedClient(attrs)).isEqualTo(authorizedClient); + verifyZeroInteractions(this.authorizedClientRepository); + } + + @Test + public void defaultRequestOAuth2AuthorizedClientWhenAuthenticationNullAndClientRegistrationIdNullThenOAuth2AuthorizedClientNull() { + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + Map attrs = getDefaultRequestAttributes(); + assertThat(getOAuth2AuthorizedClient(attrs)).isNull(); + verifyZeroInteractions(this.authorizedClientRepository); + } + + @Test + public void defaultRequestOAuth2AuthorizedClientWhenAuthenticationWrongTypeAndClientRegistrationIdNullThenOAuth2AuthorizedClientNull() { + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + Map attrs = getDefaultRequestAttributes(); + assertThat(getOAuth2AuthorizedClient(attrs)).isNull(); + verifyZeroInteractions(this.authorizedClientRepository); + } + + @Test + public void defaultRequestOAuth2AuthorizedClientWhenRepositoryNullThenOAuth2AuthorizedClient() { + OAuth2User user = mock(OAuth2User.class); + List authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); + OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(user, authorities, "id"); + authentication(token).accept(this.result); + + Map attrs = getDefaultRequestAttributes(); + + assertThat(getOAuth2AuthorizedClient(attrs)).isNull(); + } + + @Test + public void defaultRequestOAuth2AuthorizedClientWhenAuthenticationAndClientRegistrationIdNullThenOAuth2AuthorizedClient() { + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + OAuth2User user = mock(OAuth2User.class); + List authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); + OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(user, authorities, "id"); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken); + when(this.authorizedClientRepository.loadAuthorizedClient(any(), any(), any())).thenReturn(authorizedClient); + authentication(token).accept(this.result); + + Map attrs = getDefaultRequestAttributes(); + + assertThat(getOAuth2AuthorizedClient(attrs)).isEqualTo(authorizedClient); + verify(this.authorizedClientRepository).loadAuthorizedClient(eq(token.getAuthorizedClientRegistrationId()), any(), any()); + } + + @Test + public void defaultRequestOAuth2AuthorizedClientWhenAuthenticationAndClientRegistrationIdThenIdIsExplicit() { + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + OAuth2User user = mock(OAuth2User.class); + List authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); + OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(user, authorities, "id"); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken); + when(this.authorizedClientRepository.loadAuthorizedClient(any(), any(), any())).thenReturn(authorizedClient); + authentication(token).accept(this.result); + clientRegistrationId("explicit").accept(this.result); + + Map attrs = getDefaultRequestAttributes(); + + assertThat(getOAuth2AuthorizedClient(attrs)).isEqualTo(authorizedClient); + verify(this.authorizedClientRepository).loadAuthorizedClient(eq("explicit"), any(), any()); + } + + @Test + public void defaultRequestOAuth2AuthorizedClientWhenAuthenticationNullAndClientRegistrationIdThenOAuth2AuthorizedClient() { + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + OAuth2User user = mock(OAuth2User.class); + List authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken); + when(this.authorizedClientRepository.loadAuthorizedClient(any(), any(), any())).thenReturn(authorizedClient); + clientRegistrationId("id").accept(this.result); + + Map attrs = getDefaultRequestAttributes(); + + assertThat(getOAuth2AuthorizedClient(attrs)).isEqualTo(authorizedClient); + verify(this.authorizedClientRepository).loadAuthorizedClient(eq("id"), any(), any()); + } + + private Map getDefaultRequestAttributes() { + this.function.defaultRequest().accept(this.spec); + verify(this.spec).attributes(this.attrs.capture()); + + this.attrs.getValue().accept(this.result); + + return this.result; + } + + @Test + public void filterWhenAuthorizedClientNullThenAuthorizationHeaderNull() { + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .build(); + + this.function.filter(request, this.exchange).block(); + + assertThat(this.exchange.getRequest().headers().getFirst(HttpHeaders.AUTHORIZATION)).isNull(); + } + + @Test + public void filterWhenAuthorizedClientThenAuthorizationHeader() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + this.function.filter(request, this.exchange).block(); + + assertThat(this.exchange.getRequest().headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + this.accessToken.getTokenValue()); + } + + @Test + public void filterWhenExistingAuthorizationThenSingleAuthorizationHeader() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .header(HttpHeaders.AUTHORIZATION, "Existing") + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + this.function.filter(request, this.exchange).block(); + + HttpHeaders headers = this.exchange.getRequest().headers(); + assertThat(headers.get(HttpHeaders.AUTHORIZATION)).containsOnly("Bearer " + this.accessToken.getTokenValue()); + } + + @Test + public void filterWhenRefreshRequiredThenRefresh() { + OAuth2AccessTokenResponse response = OAuth2AccessTokenResponse.withToken("token-1") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(3600) + .refreshToken("refresh-1") + .build(); + when(this.exchange.getResponse().body(any())).thenReturn(Mono.just(response)); + Instant issuedAt = Instant.now().minus(Duration.ofDays(1)); + Instant accessTokenExpiresAt = issuedAt.plus(Duration.ofHours(1)); + Instant refreshTokenExpiresAt = Instant.now().plus(Duration.ofHours(1)); + + this.accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(), + this.accessToken.getTokenValue(), + issuedAt, + accessTokenExpiresAt); + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", issuedAt, refreshTokenExpiresAt); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken, refreshToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .attributes(authentication(this.authentication)) + .build(); + + this.function.filter(request, this.exchange).block(); + + verify(this.authorizedClientRepository).saveAuthorizedClient(any(), eq(this.authentication), any(), any()); + + List requests = this.exchange.getRequests(); + assertThat(requests).hasSize(2); + + ClientRequest request0 = requests.get(0); + assertThat(request0.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0"); + assertThat(request0.url().toASCIIString()).isEqualTo("https://github.com/login/oauth/access_token"); + assertThat(request0.method()).isEqualTo(HttpMethod.POST); + assertThat(getBody(request0)).isEqualTo("grant_type=refresh_token&refresh_token=refresh-token"); + + ClientRequest request1 = requests.get(1); + assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-1"); + assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com"); + assertThat(request1.method()).isEqualTo(HttpMethod.GET); + assertThat(getBody(request1)).isEmpty(); + } + + @Test + public void filterWhenRefreshRequiredAndEmptyReactiveSecurityContextThenSaved() { + OAuth2AccessTokenResponse response = OAuth2AccessTokenResponse.withToken("token-1") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(3600) + .refreshToken("refresh-1") + .build(); + when(this.exchange.getResponse().body(any())).thenReturn(Mono.just(response)); + Instant issuedAt = Instant.now().minus(Duration.ofDays(1)); + Instant accessTokenExpiresAt = issuedAt.plus(Duration.ofHours(1)); + Instant refreshTokenExpiresAt = Instant.now().plus(Duration.ofHours(1)); + + this.accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(), + this.accessToken.getTokenValue(), + issuedAt, + accessTokenExpiresAt); + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", issuedAt, refreshTokenExpiresAt); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken, refreshToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + this.function.filter(request, this.exchange) + .block(); + + verify(this.authorizedClientRepository).saveAuthorizedClient(any(), any(), any(), any()); + + List requests = this.exchange.getRequests(); + assertThat(requests).hasSize(2); + + ClientRequest request0 = requests.get(0); + assertThat(request0.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0"); + assertThat(request0.url().toASCIIString()).isEqualTo("https://github.com/login/oauth/access_token"); + assertThat(request0.method()).isEqualTo(HttpMethod.POST); + assertThat(getBody(request0)).isEqualTo("grant_type=refresh_token&refresh_token=refresh-token"); + + ClientRequest request1 = requests.get(1); + assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-1"); + assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com"); + assertThat(request1.method()).isEqualTo(HttpMethod.GET); + assertThat(getBody(request1)).isEmpty(); + } + + @Test + public void filterWhenRefreshTokenNullThenShouldRefreshFalse() { + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + this.function.filter(request, this.exchange).block(); + + List requests = this.exchange.getRequests(); + assertThat(requests).hasSize(1); + + ClientRequest request0 = requests.get(0); + assertThat(request0.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-0"); + assertThat(request0.url().toASCIIString()).isEqualTo("https://example.com"); + assertThat(request0.method()).isEqualTo(HttpMethod.GET); + assertThat(getBody(request0)).isEmpty(); + } + + @Test + public void filterWhenNotExpiredThenShouldRefreshFalse() { + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", this.accessToken.getIssuedAt(), this.accessToken.getExpiresAt()); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken, refreshToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + this.function.filter(request, this.exchange).block(); + + List requests = this.exchange.getRequests(); + assertThat(requests).hasSize(1); + + ClientRequest request0 = requests.get(0); + assertThat(request0.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-0"); + assertThat(request0.url().toASCIIString()).isEqualTo("https://example.com"); + assertThat(request0.method()).isEqualTo(HttpMethod.GET); + assertThat(getBody(request0)).isEmpty(); + } + + private static String getBody(ClientRequest request) { + final List> messageWriters = new ArrayList<>(); + messageWriters.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder())); + messageWriters.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.textPlainOnly())); + messageWriters.add(new ResourceHttpMessageWriter()); + Jackson2JsonEncoder jsonEncoder = new Jackson2JsonEncoder(); + messageWriters.add(new EncoderHttpMessageWriter<>(jsonEncoder)); + messageWriters.add(new ServerSentEventHttpMessageWriter(jsonEncoder)); + messageWriters.add(new FormHttpMessageWriter()); + messageWriters.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes())); + messageWriters.add(new MultipartHttpMessageWriter(messageWriters)); + + BodyInserter.Context context = new BodyInserter.Context() { + @Override + public List> messageWriters() { + return messageWriters; + } + + @Override + public Optional serverRequest() { + return Optional.empty(); + } + + @Override + public Map hints() { + return new HashMap<>(); + } + }; + + MockClientHttpRequest body = new MockClientHttpRequest(HttpMethod.GET, "/"); + request.body().insert(body, context).block(); + return body.getBodyAsString().block(); + } + +} From dc68483d4e1e695c0dc0ad307bf4d02597d12c58 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 19 Jul 2018 20:49:28 -0500 Subject: [PATCH 149/226] Rename to ServerOAuth2AuthorizedClientExchangeFilterFunction Rename OAuth2AuthorizedClientExchangeFilterFunction to ServerOAuth2AuthorizedClientExchangeFilterFunction-> Issue: gh-5386 --- ...th2AuthorizedClientExchangeFilterFunction.java} | 8 ++++---- ...thorizedClientExchangeFilterFunctionTests.java} | 14 +++++++------- .../main/java/sample/config/WebClientConfig.java | 4 ++-- .../java/sample/web/GitHubReposController.java | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) rename oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/{OAuth2AuthorizedClientExchangeFilterFunction.java => ServerOAuth2AuthorizedClientExchangeFilterFunction.java} (96%) rename oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/{OAuth2AuthorizedClientExchangeFilterFunctionTests.java => ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java} (94%) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java similarity index 96% rename from oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunction.java rename to oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java index 45212eb593e..26b0fbbabe8 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunction.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java @@ -57,7 +57,7 @@ * @author Rob Winch * @since 5.1 */ -public final class OAuth2AuthorizedClientExchangeFilterFunction implements ExchangeFilterFunction { +public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements ExchangeFilterFunction { /** * The request attribute name used to locate the {@link OAuth2AuthorizedClient}. */ @@ -69,9 +69,9 @@ public final class OAuth2AuthorizedClientExchangeFilterFunction implements Excha private ReactiveOAuth2AuthorizedClientService authorizedClientService; - public OAuth2AuthorizedClientExchangeFilterFunction() {} + public ServerOAuth2AuthorizedClientExchangeFilterFunction() {} - public OAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientService authorizedClientService) { + public ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientService authorizedClientService) { this.authorizedClientService = authorizedClientService; } @@ -97,7 +97,7 @@ public OAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClie * *
      *
    • The ReactiveOAuth2AuthorizedClientService on the - * {@link OAuth2AuthorizedClientExchangeFilterFunction} is not null
    • + * {@link ServerOAuth2AuthorizedClientExchangeFilterFunction} is not null *
    • A refresh token is present on the OAuth2AuthorizedClient
    • *
    • The access token will be expired in * {@link #setAccessTokenExpiresSkew(Duration)}
    • diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunctionTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java similarity index 94% rename from oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunctionTests.java rename to oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java index d673d845b4b..d19d1ee386f 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunctionTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java @@ -62,18 +62,18 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.http.HttpMethod.GET; -import static org.springframework.security.oauth2.client.web.reactive.function.client.OAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient; +import static org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient; /** * @author Rob Winch * @since 5.1 */ @RunWith(MockitoJUnitRunner.class) -public class OAuth2AuthorizedClientExchangeFilterFunctionTests { +public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests { @Mock private ReactiveOAuth2AuthorizedClientService authorizedClientService; - private OAuth2AuthorizedClientExchangeFilterFunction function = new OAuth2AuthorizedClientExchangeFilterFunction(); + private ServerOAuth2AuthorizedClientExchangeFilterFunction function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(); private MockExchangeFunction exchange = new MockExchangeFunction(); @@ -151,7 +151,7 @@ public void filterWhenRefreshRequiredThenRefresh() { this.accessToken.getTokenValue(), issuedAt, accessTokenExpiresAt); - this.function = new OAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientService); + this.function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientService); OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", issuedAt, refreshTokenExpiresAt); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, @@ -200,7 +200,7 @@ public void filterWhenRefreshRequiredAndEmptyReactiveSecurityContextThenSaved() this.accessToken.getTokenValue(), issuedAt, accessTokenExpiresAt); - this.function = new OAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientService); + this.function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientService); OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", issuedAt, refreshTokenExpiresAt); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, @@ -232,7 +232,7 @@ public void filterWhenRefreshRequiredAndEmptyReactiveSecurityContextThenSaved() @Test public void filterWhenRefreshTokenNullThenShouldRefreshFalse() { - this.function = new OAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientService); + this.function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientService); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, "principalName", this.accessToken); @@ -254,7 +254,7 @@ public void filterWhenRefreshTokenNullThenShouldRefreshFalse() { @Test public void filterWhenNotExpiredThenShouldRefreshFalse() { - this.function = new OAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientService); + this.function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientService); OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", this.accessToken.getIssuedAt(), this.accessToken.getExpiresAt()); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, diff --git a/samples/boot/authcodegrant/src/main/java/sample/config/WebClientConfig.java b/samples/boot/authcodegrant/src/main/java/sample/config/WebClientConfig.java index 09dfea17ab1..fe714c7b5b5 100644 --- a/samples/boot/authcodegrant/src/main/java/sample/config/WebClientConfig.java +++ b/samples/boot/authcodegrant/src/main/java/sample/config/WebClientConfig.java @@ -18,7 +18,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.client.web.reactive.function.client.OAuth2AuthorizedClientExchangeFilterFunction; +import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction; import org.springframework.web.reactive.function.client.WebClient; /** @@ -31,7 +31,7 @@ public class WebClientConfig { @Bean WebClient webClient() { return WebClient.builder() - .filter(new OAuth2AuthorizedClientExchangeFilterFunction()) + .filter(new ServletOAuth2AuthorizedClientExchangeFilterFunction()) .build(); } } diff --git a/samples/boot/authcodegrant/src/main/java/sample/web/GitHubReposController.java b/samples/boot/authcodegrant/src/main/java/sample/web/GitHubReposController.java index 76e5cab5029..2e85edc0f9b 100644 --- a/samples/boot/authcodegrant/src/main/java/sample/web/GitHubReposController.java +++ b/samples/boot/authcodegrant/src/main/java/sample/web/GitHubReposController.java @@ -24,7 +24,7 @@ import java.util.List; -import static org.springframework.security.oauth2.client.web.reactive.function.client.OAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient; +import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient; /** * @author Joe Grandja From 329706eb84d97cfc3c6fd480341f04c40fd29da2 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 20 Jul 2018 10:15:03 -0500 Subject: [PATCH 150/226] Add static methods for ServletOAuth2AuthorizedClientExchangeFilterFunction This will allow us to break up ServletOAuth2AuthorizedClientExchangeFilterFunction into multiple components if we decide to later. Issue: gh-5545 --- .../client/OAuth2ExchangeFilterFunctions.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2ExchangeFilterFunctions.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2ExchangeFilterFunctions.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2ExchangeFilterFunctions.java new file mode 100644 index 00000000000..fcf2f56008a --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2ExchangeFilterFunctions.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.web.reactive.function.client; + +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.web.reactive.function.client.WebClient; + +import java.util.function.Consumer; + +/** + * @author Rob Winch + * @since 5.1 + */ +public final class OAuth2ExchangeFilterFunctions { + + /** + * Configures the WebClient for OAuth2 support in a servlet environment. + * @param repository the repository to use + * @return the {@link Consumer} to configure the WebClient + */ + public static Consumer oauth2ServletConfig(OAuth2AuthorizedClientRepository repository) { + return new ServletOAuth2AuthorizedClientExchangeFilterFunction(repository) + .oauth2Configuration(); + } + + /** + * Configures the WebClient for OAuth2 support in a servlet environment. + * @return the {@link Consumer} to configure the WebClient + */ + public static Consumer oauth2ServletConfig() { + return new ServletOAuth2AuthorizedClientExchangeFilterFunction() + .oauth2Configuration(); + } + + private OAuth2ExchangeFilterFunctions() {} +} From cc63d87d4831cc4852db7fd77cfb4149e5131a2c Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Sun, 22 Jul 2018 12:01:42 -0700 Subject: [PATCH 151/226] ServletOAuth2AuthorizedClientExchangeFilterFunction handles null authorized client Issue: gh-5545 --- ...OAuth2AuthorizedClientExchangeFilterFunction.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java index 7001ecd8910..bc10e7f2ed5 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java @@ -23,6 +23,7 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -150,7 +151,13 @@ public Consumer> defaultRequest() { * @return the {@link Consumer} to populate the attributes */ public static Consumer> oauth2AuthorizedClient(OAuth2AuthorizedClient authorizedClient) { - return attributes -> attributes.put(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME, authorizedClient); + return attributes -> { + if (authorizedClient == null) { + attributes.remove(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME); + } else { + attributes.put(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME, authorizedClient); + } + }; } /** @@ -262,6 +269,9 @@ private void populateDefaultOAuth2AuthorizedClient(Map attrs) { OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository .loadAuthorizedClient(clientRegistrationId, authentication, request); + if (authorizedClient == null) { + throw new ClientAuthorizationRequiredException(clientRegistrationId); + } oauth2AuthorizedClient(authorizedClient).accept(attrs); } } From cf6ee79fe1c0d594016c4999bce96e57fb002747 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Sat, 21 Jul 2018 20:48:18 -0400 Subject: [PATCH 152/226] Auto-redirect to provider login when one client configured Fixes gh-5347 --- ...bstractAuthenticationFilterConfigurer.java | 44 +++++-- .../oauth2/client/OAuth2LoginConfigurer.java | 76 ++++++++--- .../client/OAuth2LoginConfigurerTests.java | 118 ++++++++++++++++-- 3 files changed, 200 insertions(+), 38 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java index 32f861983dc..a6477923541 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -234,20 +234,27 @@ public final T failureHandler( @Override public void init(B http) throws Exception { updateAuthenticationDefaults(); - if (permitAll) { - PermitAllSupport.permitAll(http, loginPage, loginProcessingUrl, failureUrl); - } - + updateAccessDefaults(http); registerDefaultAuthenticationEntryPoint(http); } @SuppressWarnings("unchecked") - private void registerDefaultAuthenticationEntryPoint(B http) { + protected final void registerDefaultAuthenticationEntryPoint(B http) { + registerAuthenticationEntryPoint(http, this.authenticationEntryPoint); + } + + @SuppressWarnings("unchecked") + protected final void registerAuthenticationEntryPoint(B http, AuthenticationEntryPoint authenticationEntryPoint) { ExceptionHandlingConfigurer exceptionHandling = http .getConfigurer(ExceptionHandlingConfigurer.class); if (exceptionHandling == null) { return; } + exceptionHandling.defaultAuthenticationEntryPointFor( + postProcess(authenticationEntryPoint), getAuthenticationEntryPointMatcher(http)); + } + + protected final RequestMatcher getAuthenticationEntryPointMatcher(B http) { ContentNegotiationStrategy contentNegotiationStrategy = http .getSharedObject(ContentNegotiationStrategy.class); if (contentNegotiationStrategy == null) { @@ -262,10 +269,7 @@ private void registerDefaultAuthenticationEntryPoint(B http) { RequestMatcher notXRequestedWith = new NegatedRequestMatcher( new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest")); - RequestMatcher preferredMatcher = new AndRequestMatcher(Arrays.asList(notXRequestedWith, mediaMatcher)); - - exceptionHandling.defaultAuthenticationEntryPointFor( - postProcess(authenticationEntryPoint), preferredMatcher); + return new AndRequestMatcher(Arrays.asList(notXRequestedWith, mediaMatcher)); } @Override @@ -351,6 +355,15 @@ protected final String getLoginPage() { return loginPage; } + /** + * Gets the Authentication Entry Point + * + * @return the Authentication Entry Point + */ + protected final AuthenticationEntryPoint getAuthenticationEntryPoint() { + return authenticationEntryPoint; + } + /** * Gets the URL to submit an authentication request to (i.e. where username/password * must be submitted) @@ -375,7 +388,7 @@ protected final String getFailureUrl() { * * @throws Exception */ - private void updateAuthenticationDefaults() { + protected final void updateAuthenticationDefaults() { if (loginProcessingUrl == null) { loginProcessingUrl(loginPage); } @@ -390,6 +403,15 @@ private void updateAuthenticationDefaults() { } } + /** + * Updates the default values for access. + */ + protected final void updateAccessDefaults(B http) { + if (permitAll) { + PermitAllSupport.permitAll(http, loginPage, loginProcessingUrl, failureUrl); + } + } + /** * Sets the loginPage and updates the {@link AuthenticationEntryPoint}. * @param loginPage diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java index eeeb5a738c9..b84f4bb390b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java @@ -54,15 +54,23 @@ import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; import org.springframework.security.web.savedrequest.RequestCache; +import org.springframework.security.web.util.matcher.AndRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.NegatedRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -420,10 +428,24 @@ public void init(B http) throws Exception { this.loginProcessingUrl); this.setAuthenticationFilter(authenticationFilter); super.loginProcessingUrl(this.loginProcessingUrl); + if (this.loginPage != null) { + // Set custom login page super.loginPage(this.loginPage); + super.init(http); + } else { + Map loginUrlToClientName = this.getLoginLinks(); + if (loginUrlToClientName.size() == 1) { + // Setup auto-redirect to provider login page + // when only 1 client is configured + this.updateAuthenticationDefaults(); + this.updateAccessDefaults(http); + String providerLoginPage = loginUrlToClientName.keySet().iterator().next(); + this.registerAuthenticationEntryPoint(http, this.getLoginEntryPoint(http, providerLoginPage)); + } else { + super.init(http); + } } - super.init(http); OAuth2AccessTokenResponseClient accessTokenResponseClient = this.tokenEndpointConfig.accessTokenResponseClient; @@ -529,9 +551,9 @@ private GrantedAuthoritiesMapper getGrantedAuthoritiesMapper() { private GrantedAuthoritiesMapper getGrantedAuthoritiesMapperBean() { Map grantedAuthoritiesMapperMap = - BeanFactoryUtils.beansOfTypeIncludingAncestors( - this.getBuilder().getSharedObject(ApplicationContext.class), - GrantedAuthoritiesMapper.class); + BeanFactoryUtils.beansOfTypeIncludingAncestors( + this.getBuilder().getSharedObject(ApplicationContext.class), + GrantedAuthoritiesMapper.class); return (!grantedAuthoritiesMapperMap.isEmpty() ? grantedAuthoritiesMapperMap.values().iterator().next() : null); } @@ -541,29 +563,51 @@ private void initDefaultLoginFilter(B http) { return; } + loginPageGeneratingFilter.setOauth2LoginEnabled(true); + loginPageGeneratingFilter.setOauth2AuthenticationUrlToClientName(this.getLoginLinks()); + loginPageGeneratingFilter.setLoginPageUrl(this.getLoginPage()); + loginPageGeneratingFilter.setFailureUrl(this.getFailureUrl()); + } + + @SuppressWarnings("unchecked") + private Map getLoginLinks() { Iterable clientRegistrations = null; ClientRegistrationRepository clientRegistrationRepository = - OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()); + OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()); ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository).as(Iterable.class); if (type != ResolvableType.NONE && ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) { clientRegistrations = (Iterable) clientRegistrationRepository; } if (clientRegistrations == null) { - return; + return Collections.emptyMap(); } String authorizationRequestBaseUri = this.authorizationEndpointConfig.authorizationRequestBaseUri != null ? - this.authorizationEndpointConfig.authorizationRequestBaseUri : - OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; - Map authenticationUrlToClientName = new HashMap<>(); + this.authorizationEndpointConfig.authorizationRequestBaseUri : + OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; + Map loginUrlToClientName = new HashMap<>(); + clientRegistrations.forEach(registration -> loginUrlToClientName.put( + authorizationRequestBaseUri + "/" + registration.getRegistrationId(), + registration.getClientName())); + + return loginUrlToClientName; + } - clientRegistrations.forEach(registration -> authenticationUrlToClientName.put( - authorizationRequestBaseUri + "/" + registration.getRegistrationId(), - registration.getClientName())); - loginPageGeneratingFilter.setOauth2LoginEnabled(true); - loginPageGeneratingFilter.setOauth2AuthenticationUrlToClientName(authenticationUrlToClientName); - loginPageGeneratingFilter.setLoginPageUrl(this.getLoginPage()); - loginPageGeneratingFilter.setFailureUrl(this.getFailureUrl()); + private AuthenticationEntryPoint getLoginEntryPoint(B http, String providerLoginPage) { + RequestMatcher loginPageMatcher = new AntPathRequestMatcher(this.getLoginPage()); + RequestMatcher faviconMatcher = new AntPathRequestMatcher("/favicon.ico"); + RequestMatcher defaultEntryPointMatcher = this.getAuthenticationEntryPointMatcher(http); + RequestMatcher defaultLoginPageMatcher = new AndRequestMatcher( + new OrRequestMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher); + + LinkedHashMap entryPoints = new LinkedHashMap<>(); + entryPoints.put(new NegatedRequestMatcher(defaultLoginPageMatcher), + new LoginUrlAuthenticationEntryPoint(providerLoginPage)); + + DelegatingAuthenticationEntryPoint loginEntryPoint = new DelegatingAuthenticationEntryPoint(entryPoints); + loginEntryPoint.setDefaultEntryPoint(this.getAuthenticationEntryPoint()); + + return loginEntryPoint; } private static class OidcAuthenticationRequestChecker implements AuthenticationProvider { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java index 11c41eb90b0..01a7111cfcb 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java @@ -15,6 +15,7 @@ */ package org.springframework.security.config.annotation.web.configurers.oauth2.client; +import org.apache.http.HttpHeaders; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -22,6 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -81,14 +83,19 @@ * Tests for {@link OAuth2LoginConfigurer}. * * @author Kazuki Shimizu + * @author Joe Grandja * @since 5.0.1 */ public class OAuth2LoginConfigurerTests { - private static final ClientRegistration CLIENT_REGISTRATION = CommonOAuth2Provider.GOOGLE + private static final ClientRegistration GOOGLE_CLIENT_REGISTRATION = CommonOAuth2Provider.GOOGLE .getBuilder("google").clientId("clientId").clientSecret("clientSecret") .build(); + private static final ClientRegistration GITHUB_CLIENT_REGISTRATION = CommonOAuth2Provider.GITHUB + .getBuilder("github").clientId("clientId").clientSecret("clientSecret") + .build(); + private ConfigurableApplicationContext context; @Autowired @@ -239,6 +246,62 @@ public void oauth2LoginWithCustomAuthorizationRequestParameters() throws Excepti assertThat(this.response.getRedirectedUrl()).matches("https://accounts.google.com/o/oauth2/v2/auth\\?response_type=code&client_id=clientId&scope=openid\\+profile\\+email&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fgoogle&custom-param1=custom-value1"); } + // gh-5347 + @Test + public void oauth2LoginWithOneClientConfiguredThenRedirectForAuthorization() throws Exception { + loadConfig(OAuth2LoginConfig.class); + + String requestUri = "/"; + this.request = new MockHttpServletRequest("GET", requestUri); + this.request.setServletPath(requestUri); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + + assertThat(this.response.getRedirectedUrl()).matches("http://localhost/oauth2/authorization/google"); + } + + // gh-5347 + @Test + public void oauth2LoginWithOneClientConfiguredAndRequestFaviconNotAuthenticatedThenRedirectDefaultLoginPage() throws Exception { + loadConfig(OAuth2LoginConfig.class); + + String requestUri = "/favicon.ico"; + this.request = new MockHttpServletRequest("GET", requestUri); + this.request.setServletPath(requestUri); + this.request.addHeader(HttpHeaders.ACCEPT, new MediaType("image", "*").toString()); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + + assertThat(this.response.getRedirectedUrl()).matches("http://localhost/login"); + } + + // gh-5347 + @Test + public void oauth2LoginWithMultipleClientsConfiguredThenRedirectDefaultLoginPage() throws Exception { + loadConfig(OAuth2LoginConfigMultipleClients.class); + + String requestUri = "/"; + this.request = new MockHttpServletRequest("GET", requestUri); + this.request.setServletPath(requestUri); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + + assertThat(this.response.getRedirectedUrl()).matches("http://localhost/login"); + } + + @Test + public void oauth2LoginWithCustomLoginPageThenRedirectCustomLoginPage() throws Exception { + loadConfig(OAuth2LoginConfigCustomLoginPage.class); + + String requestUri = "/"; + this.request = new MockHttpServletRequest("GET", requestUri); + this.request.setServletPath(requestUri); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + + assertThat(this.response.getRedirectedUrl()).matches("http://localhost/custom-login"); + } + @Test public void oidcLogin() throws Exception { // setup application context @@ -348,15 +411,19 @@ private void registerJwtDecoder() { } private OAuth2AuthorizationRequest createOAuth2AuthorizationRequest(String... scopes) { + return this.createOAuth2AuthorizationRequest(GOOGLE_CLIENT_REGISTRATION, scopes); + } + + private OAuth2AuthorizationRequest createOAuth2AuthorizationRequest(ClientRegistration registration, String... scopes) { return OAuth2AuthorizationRequest.authorizationCode() - .authorizationUri(CLIENT_REGISTRATION.getProviderDetails().getAuthorizationUri()) - .clientId(CLIENT_REGISTRATION.getClientId()) + .authorizationUri(registration.getProviderDetails().getAuthorizationUri()) + .clientId(registration.getClientId()) .state("state123") .redirectUri("http://localhost") .additionalParameters( - Collections.singletonMap( - OAuth2ParameterNames.REGISTRATION_ID, - CLIENT_REGISTRATION.getRegistrationId())) + Collections.singletonMap( + OAuth2ParameterNames.REGISTRATION_ID, + registration.getRegistrationId())) .scope(scopes) .build(); } @@ -368,7 +435,7 @@ protected void configure(HttpSecurity http) throws Exception { http .oauth2Login() .clientRegistrationRepository( - new InMemoryClientRegistrationRepository(CLIENT_REGISTRATION)); + new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION)); super.configure(http); } } @@ -380,7 +447,7 @@ protected void configure(HttpSecurity http) throws Exception { http .oauth2Login() .clientRegistrationRepository( - new InMemoryClientRegistrationRepository(CLIENT_REGISTRATION)) + new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION)) .userInfoEndpoint() .userAuthoritiesMapper(createGrantedAuthoritiesMapper()); super.configure(http); @@ -398,7 +465,7 @@ protected void configure(HttpSecurity http) throws Exception { @Bean ClientRegistrationRepository clientRegistrationRepository() { - return new InMemoryClientRegistrationRepository(CLIENT_REGISTRATION); + return new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION); } @Bean @@ -414,7 +481,7 @@ protected void configure(HttpSecurity http) throws Exception { http .oauth2Login() .clientRegistrationRepository( - new InMemoryClientRegistrationRepository(CLIENT_REGISTRATION)) + new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION)) .loginProcessingUrl("/login/oauth2/*"); super.configure(http); } @@ -423,7 +490,7 @@ protected void configure(HttpSecurity http) throws Exception { @EnableWebSecurity static class OAuth2LoginConfigCustomAuthorizationRequestResolver extends CommonWebSecurityConfigurerAdapter { private ClientRegistrationRepository clientRegistrationRepository = - new InMemoryClientRegistrationRepository(CLIENT_REGISTRATION); + new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION); @Override protected void configure(HttpSecurity http) throws Exception { @@ -449,10 +516,39 @@ private OAuth2AuthorizationRequestResolver getAuthorizationRequestResolver() { } } + @EnableWebSecurity + static class OAuth2LoginConfigMultipleClients extends CommonWebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .oauth2Login() + .clientRegistrationRepository( + new InMemoryClientRegistrationRepository( + GOOGLE_CLIENT_REGISTRATION, GITHUB_CLIENT_REGISTRATION)); + super.configure(http); + } + } + + @EnableWebSecurity + static class OAuth2LoginConfigCustomLoginPage extends CommonWebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .oauth2Login() + .clientRegistrationRepository( + new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION)) + .loginPage("/custom-login"); + super.configure(http); + } + } + private static abstract class CommonWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http + .authorizeRequests() + .anyRequest().authenticated() + .and() .securityContext() .securityContextRepository(securityContextRepository()) .and() From 9a3307d638f11c589a353662ca95047903398f76 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 23 Jul 2018 12:28:48 -0400 Subject: [PATCH 153/226] Fix NPE when null Authentication in authorization_code grant Fixes gh-5560 --- .../OAuth2AuthorizationCodeGrantFilter.java | 3 +- ...uth2AuthorizationCodeGrantFilterTests.java | 38 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java index 90497366de3..841e02a3917 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java @@ -192,10 +192,11 @@ private void processAuthorizationResponse(HttpServletRequest request, HttpServle } Authentication currentAuthentication = SecurityContextHolder.getContext().getAuthentication(); + String principalName = currentAuthentication != null ? currentAuthentication.getName() : "anonymousUser"; OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( authenticationResult.getClientRegistration(), - currentAuthentication.getName(), + principalName, authenticationResult.getAccessToken(), authenticationResult.getRefreshToken()); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java index e9b615e98f1..fa834f919ea 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java @@ -338,6 +338,44 @@ public void doFilterWhenAuthorizationResponseSuccessAndAnonymousAccessThenAuthor assertThat(authorizedClients.values().iterator().next()).isSameAs(authorizedClient); } + @Test + public void doFilterWhenAuthorizationResponseSuccessAndAnonymousAccessNullAuthenticationThenAuthorizedClientSavedToHttpSession() throws Exception { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + SecurityContextHolder.setContext(securityContext); // null Authentication + + String requestUri = "/callback/client-1"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + request.addParameter(OAuth2ParameterNames.CODE, "code"); + request.addParameter(OAuth2ParameterNames.STATE, "state"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.setUpAuthorizationRequest(request, response, this.registration1); + this.setUpAuthenticationResult(this.registration1); + + this.filter.doFilter(request, response, filterChain); + + OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient( + this.registration1.getRegistrationId(), null, request); + assertThat(authorizedClient).isNotNull(); + + assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1); + assertThat(authorizedClient.getPrincipalName()).isEqualTo("anonymousUser"); + assertThat(authorizedClient.getAccessToken()).isNotNull(); + + HttpSession session = request.getSession(false); + assertThat(session).isNotNull(); + + @SuppressWarnings("unchecked") + Map authorizedClients = (Map) + session.getAttribute(HttpSessionOAuth2AuthorizedClientRepository.class.getName() + ".AUTHORIZED_CLIENTS"); + assertThat(authorizedClients).isNotEmpty(); + assertThat(authorizedClients).hasSize(1); + assertThat(authorizedClients.values().iterator().next()).isSameAs(authorizedClient); + } + private void setUpAuthorizationRequest(HttpServletRequest request, HttpServletResponse response, ClientRegistration registration) { Map additionalParameters = new HashMap<>(); From f6a9ce7506390bfe3bd142d541ca6655c5deba2b Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 20 Jul 2018 09:34:49 -0600 Subject: [PATCH 154/226] OpenIDConfigTests groovy->java For the remember me test, there is some hand configuration that was carried over from the groovy test as there isn't a way via the xml config to achieve the same result. For the attribute exchange test, in order to reduce the amount of endpoint configuration, the test uses a bit of reflection to disable the OpenID association step. This is because the xml config does not support wiring a custom ConsumerManager, like the java configurer does. Issue: gh-4939 --- .../config/http/HttpOpenIDConfigTests.groovy | 166 ------------- .../config/http/OpenIDConfigTests.java | 223 ++++++++++++++++++ .../config/http/OpenIDConfigTests-Sec2919.xml | 36 +++ .../http/OpenIDConfigTests-WithFormLogin.xml | 32 +++ ...Tests-WithFormLoginAndOpenIDLoginPages.xml | 32 +++ .../OpenIDConfigTests-WithFormLoginPage.xml | 32 +++ ...OpenIDConfigTests-WithOpenIDAttributes.xml | 36 +++ ...gTests-WithOpenIDLoginPageAndFormLogin.xml | 32 +++ .../http/OpenIDConfigTests-WithRememberMe.xml | 33 +++ 9 files changed, 456 insertions(+), 166 deletions(-) delete mode 100644 config/src/test/groovy/org/springframework/security/config/http/HttpOpenIDConfigTests.groovy create mode 100644 config/src/test/java/org/springframework/security/config/http/OpenIDConfigTests.java create mode 100644 config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-Sec2919.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithFormLogin.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithFormLoginAndOpenIDLoginPages.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithFormLoginPage.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithOpenIDAttributes.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithOpenIDLoginPageAndFormLogin.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithRememberMe.xml diff --git a/config/src/test/groovy/org/springframework/security/config/http/HttpOpenIDConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/HttpOpenIDConfigTests.groovy deleted file mode 100644 index 1f4c03b8ac0..00000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/HttpOpenIDConfigTests.groovy +++ /dev/null @@ -1,166 +0,0 @@ -package org.springframework.security.config.http - -import javax.servlet.http.HttpServletRequest -import org.springframework.beans.factory.parsing.BeanDefinitionParsingException -import org.springframework.mock.web.MockFilterChain -import org.springframework.mock.web.MockHttpServletRequest -import org.springframework.mock.web.MockHttpServletResponse -import org.springframework.security.config.BeanIds -import org.springframework.security.openid.OpenIDAuthenticationFilter -import org.springframework.security.openid.OpenIDAuthenticationToken -import org.springframework.security.openid.OpenIDConsumer -import org.springframework.security.openid.OpenIDConsumerException - -import org.springframework.security.web.access.ExceptionTranslationFilter -import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices -import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter - -import javax.servlet.Filter - -/** - * - * @author Luke Taylor - */ -class OpenIDConfigTests extends AbstractHttpConfigTests { - - def openIDAndFormLoginWorkTogether() { - xml.http() { - 'openid-login'() - 'form-login'() - } - createAppContext() - - def etf = getFilter(ExceptionTranslationFilter) - def ap = etf.getAuthenticationEntryPoint(); - - expect: - ap.loginFormUrl == "/login" - // Default login filter should be present since we haven't specified any login URLs - getFilter(DefaultLoginPageGeneratingFilter) != null - } - - def formLoginEntryPointTakesPrecedenceIfLoginUrlIsSet() { - xml.http() { - 'openid-login'() - 'form-login'('login-page': '/form-page') - } - createAppContext() - - expect: - getFilter(ExceptionTranslationFilter).authenticationEntryPoint.loginFormUrl == '/form-page' - } - - def openIDEntryPointTakesPrecedenceIfLoginUrlIsSet() { - xml.http() { - 'openid-login'('login-page': '/openid-page') - 'form-login'() - } - createAppContext() - - expect: - getFilter(ExceptionTranslationFilter).authenticationEntryPoint.loginFormUrl == '/openid-page' - } - - def multipleLoginPagesCausesError() { - when: - xml.http() { - 'openid-login'('login-page': '/openid-page') - 'form-login'('login-page': '/form-page') - } - createAppContext() - then: - thrown(BeanDefinitionParsingException) - } - - def openIDAndRememberMeWorkTogether() { - xml.debug() - xml.http() { - interceptUrl('/**', 'denyAll') - 'openid-login'() - 'remember-me'() - 'csrf'(disabled:true) - } - createAppContext() - - // Default login filter should be present since we haven't specified any login URLs - def loginFilter = getFilter(DefaultLoginPageGeneratingFilter) - def openIDFilter = getFilter(OpenIDAuthenticationFilter) - openIDFilter.setConsumer(new OpenIDConsumer() { - public String beginConsumption(HttpServletRequest req, String claimedIdentity, String returnToUrl, String realm) - throws OpenIDConsumerException { - return "http://testopenid.com?openid.return_to=" + returnToUrl; - } - - public OpenIDAuthenticationToken endConsumption(HttpServletRequest req) throws OpenIDConsumerException { - throw new UnsupportedOperationException(); - } - }) - Set returnToUrlParameters = new HashSet() - returnToUrlParameters.add(AbstractRememberMeServices.DEFAULT_PARAMETER) - openIDFilter.setReturnToUrlParameters(returnToUrlParameters) - assert loginFilter.openIDrememberMeParameter != null - - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET'); - MockHttpServletResponse response = new MockHttpServletResponse(); - - when: "Initial request is made" - Filter fc = appContext.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN) - request.setServletPath("/something.html") - fc.doFilter(request, response, new MockFilterChain()) - then: "Redirected to login" - response.getRedirectedUrl().endsWith("/login") - when: "Login page is requested" - request.setServletPath("/login") - request.setRequestURI("/login") - response = new MockHttpServletResponse() - fc.doFilter(request, response, new MockFilterChain()) - then: "Remember-me choice is added to page" - response.getContentAsString().contains(AbstractRememberMeServices.DEFAULT_PARAMETER) - when: "Login is submitted with remember-me selected" - request.servletPath = "/login/openid" - request.setParameter(OpenIDAuthenticationFilter.DEFAULT_CLAIMED_IDENTITY_FIELD, "http://hey.openid.com/") - request.setParameter(AbstractRememberMeServices.DEFAULT_PARAMETER, "on") - response = new MockHttpServletResponse(); - fc.doFilter(request, response, new MockFilterChain()); - String expectedReturnTo = request.getRequestURL().append("?") - .append(AbstractRememberMeServices.DEFAULT_PARAMETER) - .append("=").append("on").toString(); - then: "return_to URL contains remember-me choice" - response.getRedirectedUrl() == "http://testopenid.com?openid.return_to=" + expectedReturnTo - } - - def openIDWithAttributeExchangeConfigurationIsParsedCorrectly() { - xml.http() { - 'openid-login'() { - 'attribute-exchange'() { - 'openid-attribute'(name: 'nickname', type: 'http://schema.openid.net/namePerson/friendly') - 'openid-attribute'(name: 'email', type: 'http://schema.openid.net/contact/email', required: 'true', - 'count': '2') - } - } - } - createAppContext() - - List attributes = getFilter(OpenIDAuthenticationFilter).consumer.attributesToFetchFactory.createAttributeList('http://someid') - - expect: - attributes.size() == 2 - attributes[0].name == 'nickname' - attributes[0].type == 'http://schema.openid.net/namePerson/friendly' - !attributes[0].required - attributes[1].required - attributes[1].getCount() == 2 - } - - def 'SEC-2919: DefaultLoginGeneratingFilter should not be present if login-page="/login"'() { - when: - xml.http() { - 'openid-login'('login-page':'/login') - } - createAppContext() - - then: - getFilter(DefaultLoginPageGeneratingFilter) == null - } - -} diff --git a/config/src/test/java/org/springframework/security/config/http/OpenIDConfigTests.java b/config/src/test/java/org/springframework/security/config/http/OpenIDConfigTests.java new file mode 100644 index 00000000000..07e81a2caf4 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/OpenIDConfigTests.java @@ -0,0 +1,223 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.http; + +import java.util.HashSet; +import java.util.Set; +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.Rule; +import org.junit.Test; +import org.openid4java.consumer.ConsumerManager; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.openid.OpenID4JavaConsumer; +import org.springframework.security.openid.OpenIDAuthenticationFilter; +import org.springframework.security.openid.OpenIDConsumer; +import org.springframework.security.util.FieldUtils; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices; +import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.hamcrest.CoreMatchers.containsString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.openid4java.discovery.yadis.YadisResolver.YADIS_XRDS_LOCATION; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests usage of the <openid-login> element + * + * @author Luke Taylor + */ +public class OpenIDConfigTests { + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/OpenIDConfigTests"; + + @Autowired + MockMvc mvc; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Test + public void requestWhenOpenIDAndFormLoginBothConfiguredThenRedirectsToGeneratedLoginPage() + throws Exception { + + this.spring.configLocations(this.xml("WithFormLogin")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login")); + + assertThat(getFilter(DefaultLoginPageGeneratingFilter.class)).isNotNull(); + } + + @Test + public void requestWhenOpenIDAndFormLoginWithFormLoginPageConfiguredThenFormLoginPageWins() + throws Exception { + + this.spring.configLocations(this.xml("WithFormLoginPage")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/form-page")); + } + + @Test + public void requestWhenOpenIDAndFormLoginWithOpenIDLoginPageConfiguredThenOpenIDLoginPageWins() + throws Exception { + + this.spring.configLocations(this.xml("WithOpenIDLoginPageAndFormLogin")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/openid-page")); + } + + @Test + public void configureWhenOpenIDAndFormLoginBothConfigureLoginPagesThenWiringException() + throws Exception { + + assertThatCode(() -> this.spring.configLocations(this.xml("WithFormLoginAndOpenIDLoginPages")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class); + } + + @Test + public void requestWhenOpenIDAndRememberMeConfiguredThenRememberMePassedToIdp() + throws Exception { + + this.spring.configLocations(this.xml("WithRememberMe")).autowire(); + + OpenIDAuthenticationFilter openIDFilter = getFilter(OpenIDAuthenticationFilter.class); + + String openIdEndpointUrl = "http://testopenid.com?openid.return_to="; + Set returnToUrlParameters = new HashSet<>(); + returnToUrlParameters.add(AbstractRememberMeServices.DEFAULT_PARAMETER); + openIDFilter.setReturnToUrlParameters(returnToUrlParameters); + + OpenIDConsumer consumer = mock(OpenIDConsumer.class); + when(consumer.beginConsumption(any(HttpServletRequest.class), anyString(), anyString(), anyString())) + .then(invocation -> openIdEndpointUrl + invocation.getArgument(2)); + openIDFilter.setConsumer(consumer); + + String expectedReturnTo = new StringBuilder("http://localhost/login/openid").append("?") + .append(AbstractRememberMeServices.DEFAULT_PARAMETER) + .append("=").append("on").toString(); + + this.mvc.perform(get("/")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login")); + + this.mvc.perform(get("/login")) + .andExpect(status().isOk()) + .andExpect(content().string(containsString(AbstractRememberMeServices.DEFAULT_PARAMETER))); + + this.mvc.perform(get("/login/openid") + .param(OpenIDAuthenticationFilter.DEFAULT_CLAIMED_IDENTITY_FIELD, "http://hey.openid.com/") + .param(AbstractRememberMeServices.DEFAULT_PARAMETER, "on")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl(openIdEndpointUrl + expectedReturnTo)); + } + + @Test + public void requestWhenAttributeExchangeConfiguredThenFetchAttributesPassedToIdp() + throws Exception { + + this.spring.configLocations(this.xml("WithOpenIDAttributes")).autowire(); + + OpenIDAuthenticationFilter openIDFilter = getFilter(OpenIDAuthenticationFilter.class); + OpenID4JavaConsumer consumer = getFieldValue(openIDFilter, "consumer"); + ConsumerManager manager = getFieldValue(consumer, "consumerManager"); + manager.setMaxAssocAttempts(0); + + try ( MockWebServer server = new MockWebServer() ) { + String endpoint = server.url("/").toString(); + + server.enqueue(new MockResponse() + .addHeader(YADIS_XRDS_LOCATION, endpoint)); + server.enqueue(new MockResponse() + .setBody(String.format( + "%s", + endpoint))); + + this.mvc.perform(get("/login/openid") + .param(OpenIDAuthenticationFilter.DEFAULT_CLAIMED_IDENTITY_FIELD, endpoint)) + .andExpect(status().isFound()) + .andExpect(result -> result.getResponse().getRedirectedUrl().endsWith( + "openid.ext1.type.nickname=http%3A%2F%2Fschema.openid.net%2FnamePerson%2Ffriendly&" + + "openid.ext1.if_available=nickname&" + + "openid.ext1.type.email=http%3A%2F%2Fschema.openid.net%2Fcontact%2Femail&" + + "openid.ext1.required=email&" + + "openid.ext1.count.email=2")); + } + } + + /** + * SEC-2919 + */ + @Test + public void requestWhenLoginPageConfiguredWithPhraseLoginThenRedirectsOnlyToUserGeneratedLoginPage() + throws Exception { + + this.spring.configLocations(this.xml("Sec2919")).autowire(); + + assertThat(getFilter(DefaultLoginPageGeneratingFilter.class)).isNull(); + + this.mvc.perform(get("/login")) + .andExpect(status().isOk()) + .andExpect(content().string("a custom login page")); + } + + @RestController + static class CustomLoginController { + @GetMapping("/login") + public String custom() { + return "a custom login page"; + } + } + + private T getFilter(Class clazz) { + FilterChainProxy filterChain = this.spring.getContext().getBean(FilterChainProxy.class); + return (T) filterChain.getFilters("/").stream() + .filter(clazz::isInstance) + .findFirst() + .orElse(null); + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } + + private static T getFieldValue(Object bean, String fieldName) throws IllegalAccessException { + return (T) FieldUtils.getFieldValue(bean, fieldName); + } +} diff --git a/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-Sec2919.xml b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-Sec2919.xml new file mode 100644 index 00000000000..db2e2869759 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-Sec2919.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithFormLogin.xml b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithFormLogin.xml new file mode 100644 index 00000000000..331369ef87c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithFormLogin.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithFormLoginAndOpenIDLoginPages.xml b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithFormLoginAndOpenIDLoginPages.xml new file mode 100644 index 00000000000..de9bb8a1346 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithFormLoginAndOpenIDLoginPages.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithFormLoginPage.xml b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithFormLoginPage.xml new file mode 100644 index 00000000000..cf84079b711 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithFormLoginPage.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithOpenIDAttributes.xml b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithOpenIDAttributes.xml new file mode 100644 index 00000000000..9edaef345a9 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithOpenIDAttributes.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithOpenIDLoginPageAndFormLogin.xml b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithOpenIDLoginPageAndFormLogin.xml new file mode 100644 index 00000000000..a61ef9af0a2 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithOpenIDLoginPageAndFormLogin.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithRememberMe.xml b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithRememberMe.xml new file mode 100644 index 00000000000..63108937bff --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithRememberMe.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + From df73a4306ed86c1e1cc986c15b056573cd5ffa7d Mon Sep 17 00:00:00 2001 From: "mhyeon.lee" Date: Mon, 23 Jul 2018 10:47:23 +0900 Subject: [PATCH 155/226] Fix OAuth2AuthorizationRequestRedirectWebFilter baseurl exclude querystring To create redirect_uri in OAuth2AuthorizationRequestRedirectWebFilter, queryParam is included in the current request-based baseUrl. So when binding to the redirectUriTemplate, the wrong type of redirect_uri may be created. Fixed: gh-5520 --- ...AuthorizationRequestRedirectWebFilter.java | 1 + ...rizationRequestRedirectWebFilterTests.java | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilter.java index b350d5ed581..ae89c614989 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilter.java @@ -199,6 +199,7 @@ private String expandRedirectUri(ServerHttpRequest request, ClientRegistration c String baseUrl = UriComponentsBuilder.fromHttpRequest(new ServerHttpRequestDecorator(request)) .replacePath(request.getPath().contextPath().value()) + .replaceQuery(null) .build() .toUriString(); uriVariables.put("baseUrl", baseUrl); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilterTests.java index ce839c68107..b2ff790cbe4 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilterTests.java @@ -135,6 +135,26 @@ public void filterWhenDoesMatchThenClientRegistrationRepositoryNotSubscribed() { verify(this.authzRequestRepository).saveAuthorizationRequest(any(), any()); } + // gh-5520 + @Test + public void filterWhenDoesMatchThenResolveRedirectUriExpandedExcludesQueryString() { + FluxExchangeResult result = this.client.get() + .uri("https://example.com/oauth2/authorization/github?foo=bar").exchange() + .expectStatus().is3xxRedirection().returnResult(String.class); + result.assertWithDiagnostics(() -> { + URI location = result.getResponseHeaders().getLocation(); + assertThat(location) + .hasScheme("https") + .hasHost("github.com") + .hasPath("/login/oauth/authorize") + .hasParameter("response_type", "code") + .hasParameter("client_id", "clientId") + .hasParameter("scope", "read:user") + .hasParameter("state") + .hasParameter("redirect_uri", "https://example.com/login/oauth2/code/github"); + }); + } + @Test public void filterWhenExceptionThenRedirected() { FilteringWebHandler webHandler = new FilteringWebHandler(e -> Mono.error(new ClientAuthorizationRequiredException(this.github.getRegistrationId())), Arrays.asList(this.filter)); From 22741431d946c9f0b360b1787df58157aac36e25 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 20 Jul 2018 14:05:27 -0600 Subject: [PATCH 156/226] Bearer Token Resolver Configuration This introduces #bearerTokenResolver(BearerTokenResolver) to the Resource Server DSL, allowing users to configure the resolver to allow the access token as part of the request body or a query parameter. It also allows the user to replace the resolver with a completely custom one. This also introduces the same ability by exposing a bean of type BearerTokenResolver Fixes: gh-5496 --- .../configurers/oauth2/OAuth2Configurer.java | 4 +- .../OAuth2ResourceServerConfigurer.java | 41 +++- .../OAuth2ResourceServerConfigurerTests.java | 230 +++++++++++++++++- 3 files changed, 260 insertions(+), 15 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java index 772e8122970..433119b453c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java @@ -16,6 +16,7 @@ package org.springframework.security.config.annotation.web.configurers.oauth2; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -96,7 +97,8 @@ private void initClientConfigurer() { } private void initResourceServerConfigurer() { - this.resourceServerConfigurer = new OAuth2ResourceServerConfigurer<>(); + ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); + this.resourceServerConfigurer = new OAuth2ResourceServerConfigurer<>(context); this.resourceServerConfigurer.setBuilder(this.getBuilder()); this.resourceServerConfigurer.addObjectPostProcessor(this.objectPostProcessor); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java index ae3db1588b5..03088dfebcb 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java @@ -25,6 +25,7 @@ import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; @@ -47,6 +48,7 @@ * The following configuration options are available: * *
        + *
      • {@link #bearerTokenResolver(BearerTokenResolver)} - customizes how to resolve a bearer token from the request
      • *
      • {@link #jwt()} - enables Jwt-encoded bearer token support
      • *
      * @@ -99,7 +101,11 @@ public final class OAuth2ResourceServerConfigurer> extends AbstractHttpConfigurer, H> { - private BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver(); + private final ApplicationContext context; + + private BearerTokenResolver bearerTokenResolver; + private JwtConfigurer jwtConfigurer; + private BearerTokenRequestMatcher requestMatcher = new BearerTokenRequestMatcher(); private BearerTokenAuthenticationEntryPoint authenticationEntryPoint @@ -108,12 +114,20 @@ public final class OAuth2ResourceServerConfigurer bearerTokenResolver(BearerTokenResolver bearerTokenResolver) { + Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null"); + this.bearerTokenResolver = bearerTokenResolver; + return this; + } public JwtConfigurer jwt() { if ( this.jwtConfigurer == null ) { - ApplicationContext context = this.getBuilder().getSharedObject(ApplicationContext.class); - this.jwtConfigurer = new JwtConfigurer(context); + this.jwtConfigurer = new JwtConfigurer(this.context); } return this.jwtConfigurer; @@ -231,17 +245,28 @@ private void registerDefaultCsrfOverride(H http) { csrf.ignoringRequestMatchers(this.requestMatcher); } - private BearerTokenResolver getBearerTokenResolver() { + BearerTokenResolver getBearerTokenResolver() { + if ( this.bearerTokenResolver == null ) { + if ( this.context.getBeanNamesForType(BearerTokenResolver.class).length > 0 ) { + this.bearerTokenResolver = this.context.getBean(BearerTokenResolver.class); + } else { + this.bearerTokenResolver = new DefaultBearerTokenResolver(); + } + } + return this.bearerTokenResolver; } private static final class BearerTokenRequestMatcher implements RequestMatcher { - private BearerTokenResolver bearerTokenResolver - = new DefaultBearerTokenResolver(); + private BearerTokenResolver bearerTokenResolver; @Override public boolean matches(HttpServletRequest request) { - return this.bearerTokenResolver.resolve(request) != null; + try { + return this.bearerTokenResolver.resolve(request) != null; + } catch ( OAuth2AuthenticationException e ) { + return false; + } } public void setBearerTokenResolver(BearerTokenResolver tokenResolver) { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index 6b89689ad96..fbd63ad1586 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -66,6 +66,8 @@ import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; +import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; @@ -82,9 +84,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; +import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -526,6 +530,134 @@ public void requestWhenSessionManagementConfiguredThenUserConfigurationOverrides assertThat(result.getRequest().getSession(false)).isNotNull(); } + // -- custom bearer token resolver + + @Test + public void requestWhenBearerTokenResolverAllowsRequestBodyThenEitherHeaderOrRequestBodyIsAccepted() + throws Exception { + + this.spring.register(AllowBearerTokenInRequestBodyConfig.class, JwtDecoderConfig.class, + BasicController.class).autowire(); + + JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class); + when(decoder.decode(anyString())).thenReturn(JWT); + + this.mvc.perform(get("/authenticated") + .with(bearerToken(JWT_TOKEN))) + .andExpect(status().isOk()) + .andExpect(content().string(JWT_SUBJECT)); + + this.mvc.perform(post("/authenticated") + .param("access_token", JWT_TOKEN)) + .andExpect(status().isOk()) + .andExpect(content().string(JWT_SUBJECT)); + } + + @Test + public void requestWhenBearerTokenResolverAllowsQueryParameterThenEitherHeaderOrQueryParameterIsAccepted() + throws Exception { + + this.spring.register(AllowBearerTokenAsQueryParameterConfig.class, JwtDecoderConfig.class, + BasicController.class).autowire(); + + JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class); + when(decoder.decode(anyString())).thenReturn(JWT); + + this.mvc.perform(get("/authenticated") + .with(bearerToken(JWT_TOKEN))) + .andExpect(status().isOk()) + .andExpect(content().string(JWT_SUBJECT)); + + this.mvc.perform(get("/authenticated") + .param("access_token", JWT_TOKEN)) + .andExpect(status().isOk()) + .andExpect(content().string(JWT_SUBJECT)); + } + + @Test + public void requestWhenBearerTokenResolverAllowsRequestBodyAndRequestContainsTwoTokensThenInvalidRequest() + throws Exception { + + this.spring.register(AllowBearerTokenInRequestBodyConfig.class, JwtDecoderConfig.class, + BasicController.class).autowire(); + + JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class); + when(decoder.decode(anyString())).thenReturn(JWT); + + this.mvc.perform(post("/authenticated") + .param("access_token", JWT_TOKEN) + .with(bearerToken(JWT_TOKEN)) + .with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, containsString("invalid_request"))); + } + + @Test + public void requestWhenBearerTokenResolverAllowsQueryParameterAndRequestContainsTwoTokensThenInvalidRequest() + throws Exception { + + this.spring.register(AllowBearerTokenAsQueryParameterConfig.class, JwtDecoderConfig.class, + BasicController.class).autowire(); + + JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class); + when(decoder.decode(anyString())).thenReturn(JWT); + + this.mvc.perform(get("/authenticated") + .with(bearerToken(JWT_TOKEN)) + .param("access_token", JWT_TOKEN)) + .andExpect(status().isBadRequest()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, containsString("invalid_request"))); + } + + @Test + public void getBearerTokenResolverWhenDuplicateResolverBeansAndAnotherOnTheDslThenTheDslOneIsUsed() { + BearerTokenResolver resolverBean = mock(BearerTokenResolver.class); + BearerTokenResolver resolver = mock(BearerTokenResolver.class); + + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean("resolverOne", BearerTokenResolver.class, () -> resolverBean); + context.registerBean("resolverTwo", BearerTokenResolver.class, () -> resolverBean); + this.spring.context(context).autowire(); + + OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context); + + oauth2.bearerTokenResolver(resolver); + + assertThat(oauth2.getBearerTokenResolver()).isEqualTo(resolver); + } + + @Test + public void getBearerTokenResolverWhenDuplicateResolverBeansThenWiringException() { + assertThatCode(() -> this.spring.register(MultipleBearerTokenResolverBeansConfig.class).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasRootCauseInstanceOf(NoUniqueBeanDefinitionException.class); + } + + @Test + public void getBearerTokenResolverWhenResolverBeanAndAnotherOnTheDslThenTheDslOneIsUsed() { + BearerTokenResolver resolver = mock(BearerTokenResolver.class); + BearerTokenResolver resolverBean = mock(BearerTokenResolver.class); + + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean(BearerTokenResolver.class, () -> resolverBean); + this.spring.context(context).autowire(); + + OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context); + oauth2.bearerTokenResolver(resolver); + + assertThat(oauth2.getBearerTokenResolver()).isEqualTo(resolver); + } + + @Test + public void getBearerTokenResolverWhenNoResolverSpecifiedThenTheDefaultIsUsed() { + ApplicationContext context = + this.spring.context(new GenericWebApplicationContext()).getContext(); + + OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context); + + assertThat(oauth2.getBearerTokenResolver()).isInstanceOf(DefaultBearerTokenResolver.class); + } + // -- custom jwt decoder @Test @@ -563,8 +695,10 @@ public void requestWhenCustomJwtDecoderExposedAsBeanThenUsed() @Test public void getJwtDecoderWhenConfiguredWithDecoderAndJwkSetUriThenLastOneWins() { + ApplicationContext context = mock(ApplicationContext.class); + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = - new OAuth2ResourceServerConfigurer().new JwtConfigurer(null); + new OAuth2ResourceServerConfigurer(context).jwt(); JwtDecoder decoder = mock(JwtDecoder.class); @@ -574,7 +708,7 @@ public void getJwtDecoderWhenConfiguredWithDecoderAndJwkSetUriThenLastOneWins() assertThat(jwtConfigurer.getJwtDecoder()).isEqualTo(decoder); jwtConfigurer = - new OAuth2ResourceServerConfigurer().new JwtConfigurer(null); + new OAuth2ResourceServerConfigurer(context).jwt(); jwtConfigurer.decoder(decoder); jwtConfigurer.jwkSetUri(JWK_SET_URI); @@ -593,7 +727,7 @@ public void getJwtDecoderWhenConflictingJwtDecodersThenTheDslWiredOneTakesPreced when(context.getBean(JwtDecoder.class)).thenReturn(decoderBean); OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = - new OAuth2ResourceServerConfigurer().new JwtConfigurer(context); + new OAuth2ResourceServerConfigurer(context).jwt(); jwtConfigurer.decoder(decoder); assertThat(jwtConfigurer.getJwtDecoder()).isEqualTo(decoder); @@ -607,7 +741,7 @@ public void getJwtDecoderWhenContextHasBeanAndUserConfiguresJwkSetUriThenJwkSetU when(context.getBean(JwtDecoder.class)).thenReturn(decoder); OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = - new OAuth2ResourceServerConfigurer().new JwtConfigurer(context); + new OAuth2ResourceServerConfigurer(context).jwt(); jwtConfigurer.jwkSetUri(JWK_SET_URI); @@ -627,7 +761,7 @@ public void getJwtDecoderWhenTwoJwtDecoderBeansAndAnotherWiredOnDslThenDslWiredO this.spring.context(context).autowire(); OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = - new OAuth2ResourceServerConfigurer().new JwtConfigurer(context); + new OAuth2ResourceServerConfigurer(context).jwt(); jwtConfigurer.decoder(decoder); assertThat(jwtConfigurer.getJwtDecoder()).isEqualTo(decoder); @@ -644,7 +778,7 @@ public void getJwtDecoderWhenTwoJwtDecoderBeansThenThrowsException() { this.spring.context(context).autowire(); OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = - new OAuth2ResourceServerConfigurer().new JwtConfigurer(context); + new OAuth2ResourceServerConfigurer(context).jwt(); assertThatCode(() -> jwtConfigurer.getJwtDecoder()) .isInstanceOf(NoUniqueBeanDefinitionException.class); @@ -833,6 +967,82 @@ protected void configure(HttpSecurity http) throws Exception { } } + @EnableWebSecurity + static class AllowBearerTokenInRequestBodyConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .bearerTokenResolver(allowRequestBody()) + .jwt(); + // @formatter:on + } + + private BearerTokenResolver allowRequestBody() { + DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver(); + resolver.setAllowFormEncodedBodyParameter(true); + return resolver; + } + } + + @EnableWebSecurity + static class AllowBearerTokenAsQueryParameterConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt(); + // @formatter:on + } + + @Bean + BearerTokenResolver allowQueryParameter() { + DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver(); + resolver.setAllowUriQueryParameter(true); + return resolver; + } + } + + @EnableWebSecurity + static class MultipleBearerTokenResolverBeansConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt(); + // @formatter:on + } + + @Bean + BearerTokenResolver resolverOne() { + DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver(); + resolver.setAllowUriQueryParameter(true); + return resolver; + } + + @Bean + BearerTokenResolver resolverTwo() { + DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver(); + resolver.setAllowFormEncodedBodyParameter(true); + return resolver; + } + } + @EnableWebSecurity static class CustomJwtDecoderOnDsl extends WebSecurityConfigurerAdapter { JwtDecoder decoder = mock(JwtDecoder.class); @@ -877,6 +1087,14 @@ public JwtDecoder decoder() { } } + @Configuration + static class JwtDecoderConfig { + @Bean + public JwtDecoder jwtDecoder() { + return mock(JwtDecoder.class); + } + } + @RestController static class BasicController { @GetMapping("/") From 96e0d0d6c3f17d8db7d600b70d1ed8c3b7ac1f03 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 17 Jul 2018 11:18:16 -0600 Subject: [PATCH 157/226] Bearer Token Exception Handling Configuration This exposes #authenticationEntryPoint(), #accessDeniedHandler, on the Resource Server DSL. With these, a user can customize the error responses when a bearer token request fails. Fixes: gh-5497 --- .../OAuth2ResourceServerConfigurer.java | 29 ++- .../OAuth2ResourceServerConfigurerTests.java | 179 ++++++++++++++++++ 2 files changed, 200 insertions(+), 8 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java index 03088dfebcb..4f252d7912f 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java @@ -34,6 +34,8 @@ import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver; import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; @@ -48,6 +50,8 @@ * The following configuration options are available: * *
        + *
      • {@link #accessDeniedHandler(AccessDeniedHandler)}
      • - customizes how access denied errors are handled + *
      • {@link #authenticationEntryPoint(AuthenticationEntryPoint)}
      • - customizes how authentication failures are handled *
      • {@link #bearerTokenResolver(BearerTokenResolver)} - customizes how to resolve a bearer token from the request
      • *
      • {@link #jwt()} - enables Jwt-encoded bearer token support
      • *
      @@ -106,19 +110,27 @@ public final class OAuth2ResourceServerConfigurer accessDeniedHandler(AccessDeniedHandler accessDeniedHandler) { + Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null"); + this.accessDeniedHandler = accessDeniedHandler; + return this; + } + + public OAuth2ResourceServerConfigurer authenticationEntryPoint(AuthenticationEntryPoint entryPoint) { + Assert.notNull(entryPoint, "entryPoint cannot be null"); + this.authenticationEntryPoint = entryPoint; + return this; + } + public OAuth2ResourceServerConfigurer bearerTokenResolver(BearerTokenResolver bearerTokenResolver) { Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null"); this.bearerTokenResolver = bearerTokenResolver; @@ -141,7 +153,7 @@ public void setBuilder(H http) { @Override public void init(H http) throws Exception { - registerDefaultDeniedHandler(http); + registerDefaultAccessDeniedHandler(http); registerDefaultEntryPoint(http); registerDefaultCsrfOverride(http); } @@ -156,6 +168,7 @@ public void configure(H http) throws Exception { BearerTokenAuthenticationFilter filter = new BearerTokenAuthenticationFilter(manager); filter.setBearerTokenResolver(bearerTokenResolver); + filter.setAuthenticationEntryPoint(this.authenticationEntryPoint); filter = postProcess(filter); http.addFilter(filter); @@ -211,7 +224,7 @@ private void initSessionCreationPolicy(H http) { } } - private void registerDefaultDeniedHandler(H http) { + private void registerDefaultAccessDeniedHandler(H http) { ExceptionHandlingConfigurer exceptionHandling = http .getConfigurer(ExceptionHandlingConfigurer.class); if (exceptionHandling == null) { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index fbd63ad1586..cae0e334237 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -64,11 +64,17 @@ import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtClaimNames; import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtException; import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint; import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver; +import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler; import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.AccessDeniedHandlerImpl; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultMatcher; @@ -85,6 +91,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.core.StringStartsWith.startsWith; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -784,8 +791,101 @@ public void getJwtDecoderWhenTwoJwtDecoderBeansThenThrowsException() { .isInstanceOf(NoUniqueBeanDefinitionException.class); } + // -- exception handling + + @Test + public void requestWhenRealmNameConfiguredThenUsesOnUnauthenticated() + throws Exception { + + this.spring.register(RealmNameConfiguredOnEntryPoint.class, JwtDecoderConfig.class).autowire(); + + JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class); + when(decoder.decode(anyString())).thenThrow(JwtException.class); + + this.mvc.perform(get("/authenticated") + .with(bearerToken("invalid_token"))) + .andExpect(status().isUnauthorized()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer realm=\"myRealm\""))); + } + + @Test + public void requestWhenRealmNameConfiguredThenUsesOnAccessDenied() + throws Exception { + + this.spring.register(RealmNameConfiguredOnAccessDeniedHandler.class, JwtDecoderConfig.class).autowire(); + + JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class); + when(decoder.decode(anyString())).thenReturn(JWT); + + this.mvc.perform(get("/authenticated") + .with(bearerToken("insufficiently_scoped"))) + .andExpect(status().isForbidden()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer realm=\"myRealm\""))); + } + + @Test + public void authenticationEntryPointWhenGivenNullThenThrowsException() { + ApplicationContext context = mock(ApplicationContext.class); + OAuth2ResourceServerConfigurer configurer = new OAuth2ResourceServerConfigurer(context); + assertThatCode(() -> configurer.authenticationEntryPoint(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void accessDeniedHandlerWhenGivenNullThenThrowsException() { + ApplicationContext context = mock(ApplicationContext.class); + OAuth2ResourceServerConfigurer configurer = new OAuth2ResourceServerConfigurer(context); + assertThatCode(() -> configurer.accessDeniedHandler(null)) + .isInstanceOf(IllegalArgumentException.class); + } + // -- In combination with other authentication providers + @Test + public void requestWhenBasicAndResourceServerEntryPointsThenMatchedByRequest() + throws Exception { + + this.spring.register(BasicAndResourceServerConfig.class, JwtDecoderConfig.class).autowire(); + + JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class); + when(decoder.decode(anyString())).thenThrow(JwtException.class); + + this.mvc.perform(get("/authenticated") + .with(httpBasic("some", "user"))) + .andExpect(status().isUnauthorized()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Basic"))); + + this.mvc.perform(get("/authenticated")) + .andExpect(status().isUnauthorized()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Basic"))); + + this.mvc.perform(get("/authenticated") + .with(bearerToken("invalid_token"))) + .andExpect(status().isUnauthorized()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer"))); + } + + @Test + public void requestWhenDefaultAndResourceServerAccessDeniedHandlersThenMatchedByRequest() + throws Exception { + + this.spring.register(ExceptionHandlingAndResourceServerWithAccessDeniedHandlerConfig.class, + JwtDecoderConfig.class).autowire(); + + JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class); + when(decoder.decode(anyString())).thenReturn(JWT); + + this.mvc.perform(get("/authenticated") + .with(httpBasic("basic-user", "basic-password"))) + .andExpect(status().isForbidden()) + .andExpect(header().doesNotExist(HttpHeaders.WWW_AUTHENTICATE)); + + this.mvc.perform(get("/authenticated") + .with(bearerToken("insufficiently_scoped"))) + .andExpect(status().isForbidden()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer"))); + } + @Test public void getWhenAlsoUsingHttpBasicThenCorrectProviderEngages() throws Exception { @@ -901,6 +1001,85 @@ protected void configure(HttpSecurity http) throws Exception { } } + @EnableWebSecurity + static class RealmNameConfiguredOnEntryPoint extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .authenticationEntryPoint(authenticationEntryPoint()) + .jwt(); + // @formatter:on + } + + AuthenticationEntryPoint authenticationEntryPoint() { + BearerTokenAuthenticationEntryPoint entryPoint = + new BearerTokenAuthenticationEntryPoint(); + entryPoint.setRealmName("myRealm"); + return entryPoint; + } + } + + @EnableWebSecurity + static class RealmNameConfiguredOnAccessDeniedHandler extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().denyAll() + .and() + .oauth2() + .resourceServer() + .accessDeniedHandler(accessDeniedHandler()) + .jwt(); + // @formatter:on + } + + AccessDeniedHandler accessDeniedHandler() { + BearerTokenAccessDeniedHandler accessDeniedHandler = + new BearerTokenAccessDeniedHandler(); + accessDeniedHandler.setRealmName("myRealm"); + return accessDeniedHandler; + } + } + + @EnableWebSecurity + static class ExceptionHandlingAndResourceServerWithAccessDeniedHandlerConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().denyAll() + .and() + .exceptionHandling() + .defaultAccessDeniedHandlerFor(new AccessDeniedHandlerImpl(), request -> false) + .and() + .httpBasic() + .and() + .oauth2() + .resourceServer() + .jwt(); + // @formatter:on + } + + @Bean + public UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager( + org.springframework.security.core.userdetails.User.withDefaultPasswordEncoder() + .username("basic-user") + .password("basic-password") + .roles("USER") + .build()); + } + } + @EnableWebSecurity static class BasicAndResourceServerConfig extends WebSecurityConfigurerAdapter { @Value("${mock.jwk-set-uri:https://example.org}") String uri; From 165537b451e2e502a29d2d76fd255c0b0ca8b1ea Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 24 Jul 2018 16:16:52 -0400 Subject: [PATCH 158/226] Update to cglib-nodep:3.2.7 Fixes gh-5567 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 8bd35d5deab..a092e9022b3 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -17,7 +17,7 @@ dependencyManagement { mavenBom "org.springframework.data:spring-data-releasetrain:${springDataVersion}" } dependencies { - dependency 'cglib:cglib-nodep:3.2.6' + dependency 'cglib:cglib-nodep:3.2.7' dependency 'com.squareup.okhttp3:mockwebserver:3.10.0' dependency 'opensymphony:sitemesh:2.4.2' dependency 'org.gebish:geb-spock:0.10.0' From cd632f5cd736aecbeae373dcd4638e7236c4edd6 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 24 Jul 2018 16:21:12 -0400 Subject: [PATCH 159/226] Update to nimbus-jose-jwt:5.14 Fixes gh-5568 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index a092e9022b3..5e52c7ce121 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -54,7 +54,7 @@ dependencyManagement { dependency 'com.google.guava:guava:20.0' dependency 'com.google.inject:guice:3.0' dependency 'com.nimbusds:lang-tag:1.4.3' - dependency 'com.nimbusds:nimbus-jose-jwt:5.11' + dependency 'com.nimbusds:nimbus-jose-jwt:5.14' dependency 'com.nimbusds:oauth2-oidc-sdk:5.62' dependency 'com.squareup.okhttp3:okhttp:3.9.0' dependency 'com.squareup.okio:okio:1.13.0' From 9298be041812b89a63cfd5ce5d47eff452fb127a Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 24 Jul 2018 16:23:22 -0400 Subject: [PATCH 160/226] Update to oauth2-oidc-sdk:5.64.2 Fixes gh-5569 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 5e52c7ce121..900db99f845 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -55,7 +55,7 @@ dependencyManagement { dependency 'com.google.inject:guice:3.0' dependency 'com.nimbusds:lang-tag:1.4.3' dependency 'com.nimbusds:nimbus-jose-jwt:5.14' - dependency 'com.nimbusds:oauth2-oidc-sdk:5.62' + dependency 'com.nimbusds:oauth2-oidc-sdk:5.64.2' dependency 'com.squareup.okhttp3:okhttp:3.9.0' dependency 'com.squareup.okio:okio:1.13.0' dependency 'com.sun.xml.bind:jaxb-core:2.3.0' From 80f121b5f5a5964e0ca93359f27c902aaeb475e7 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 24 Jul 2018 16:29:22 -0400 Subject: [PATCH 161/226] Update to javax.servlet.jsp.jstl-api:1.2.2 Fixes gh-5571 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 900db99f845..0cf3a394c5e 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -74,7 +74,7 @@ dependencyManagement { dependency 'javax.annotation:jsr250-api:1.0' dependency 'javax.inject:javax.inject:1' dependency 'javax.mail:mail:1.4.7' - dependency 'javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.1' + dependency 'javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.2' dependency 'javax.servlet.jsp:javax.servlet.jsp-api:2.3.2-b02' dependency 'javax.servlet:javax.servlet-api:4.0.1' dependency 'javax.validation:validation-api:2.0.1.Final' From 4276996c630c268243baf518cc7f1e036232f486 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 24 Jul 2018 16:32:08 -0400 Subject: [PATCH 162/226] Update to ehcache:2.10.5 Fixes gh-5572 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 0cf3a394c5e..10d775334a8 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -86,7 +86,7 @@ dependencyManagement { dependency 'net.jcip:jcip-annotations:1.0' dependency 'net.minidev:accessors-smart:1.2' dependency 'net.minidev:json-smart:2.3' - dependency 'net.sf.ehcache:ehcache:2.10.4' + dependency 'net.sf.ehcache:ehcache:2.10.5' dependency 'net.sourceforge.htmlunit:htmlunit:2.31' dependency 'net.sourceforge.htmlunit:neko-htmlunit:2.31' dependency 'net.sourceforge.nekohtml:nekohtml:1.9.22' From c79ce708c1039499cf2c24f57fe9b8bac5b44e53 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 24 Jul 2018 16:37:14 -0400 Subject: [PATCH 163/226] Update to org.apache.httpcomponents:httpclient:4.5.6 Fixes gh-5573 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 10d775334a8..70c8b5509dc 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -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.5' + dependency 'org.apache.httpcomponents:httpclient:4.5.6' dependency 'org.apache.httpcomponents:httpcore:4.4.8' dependency 'org.apache.httpcomponents:httpmime:4.5.3' dependency 'org.apache.mina:mina-core:2.0.0-M6' From e8a29e996bd3ac65a6760dd63bd767afd8ea36c3 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 24 Jul 2018 16:38:49 -0400 Subject: [PATCH 164/226] Update to bcpkix-jdk15on:1.60 Fixes gh-5574 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 70c8b5509dc..27310711952 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -145,7 +145,7 @@ dependencyManagement { dependency 'org.aspectj:aspectjweaver:1.9.1' dependency 'org.assertj:assertj-core:3.10.0' dependency 'org.attoparser:attoparser:2.0.4.RELEASE' - dependency 'org.bouncycastle:bcpkix-jdk15on:1.59' + dependency 'org.bouncycastle:bcpkix-jdk15on:1.60' dependency 'org.bouncycastle:bcprov-jdk15on:1.58' dependency 'org.codehaus.groovy:groovy-all:2.4.14' dependency 'org.codehaus.groovy:groovy-json:2.4.14' From e65b6c842c5a61abd89c28c52012bb2dfaec6c1a Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 24 Jul 2018 16:46:33 -0400 Subject: [PATCH 165/226] Update to hibernate-entitymanager:5.3.3.Final Fixes gh-5575 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 27310711952..92aa573f3e8 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -171,7 +171,7 @@ dependencyManagement { dependency 'org.hibernate.common:hibernate-commons-annotations:5.0.1.Final' dependency 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final' dependency 'org.hibernate:hibernate-core:5.2.17.Final' - dependency 'org.hibernate:hibernate-entitymanager:5.2.17.Final' + dependency 'org.hibernate:hibernate-entitymanager:5.3.3.Final' dependency 'org.hibernate:hibernate-validator:6.0.10.Final' dependency 'org.hsqldb:hsqldb:2.4.1' dependency 'org.jasig.cas.client:cas-client-core:3.5.0' From 9ece4459f2e3780d52a7f93c9109a7c1ecac9817 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 24 Jul 2018 16:48:36 -0400 Subject: [PATCH 166/226] Update to hibernate-validator:6.0.11.Final Fixes gh-5576 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 92aa573f3e8..003d084ec68 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -172,7 +172,7 @@ dependencyManagement { dependency 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final' dependency 'org.hibernate:hibernate-core:5.2.17.Final' dependency 'org.hibernate:hibernate-entitymanager:5.3.3.Final' - dependency 'org.hibernate:hibernate-validator:6.0.10.Final' + dependency 'org.hibernate:hibernate-validator:6.0.11.Final' dependency 'org.hsqldb:hsqldb:2.4.1' dependency 'org.jasig.cas.client:cas-client-core:3.5.0' dependency 'org.javassist:javassist:3.22.0-CR2' From 47c994998d6ce087a0ad53305b04c97ee0fe0b2e Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 24 Jul 2018 16:50:57 -0400 Subject: [PATCH 167/226] Update to selenium-java:3.13.0 Fixes gh-5577 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 003d084ec68..5ad133f4d82 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -185,7 +185,7 @@ dependencyManagement { dependency 'org.ow2.asm:asm:6.0' dependency 'org.reactivestreams:reactive-streams:1.0.1' dependency 'org.seleniumhq.selenium:htmlunit-driver:2.31.0' - dependency 'org.seleniumhq.selenium:selenium-java:3.12.0' + dependency 'org.seleniumhq.selenium:selenium-java:3.13.0' dependency 'org.seleniumhq.selenium:selenium-support:3.12.0' dependency 'org.skyscreamer:jsonassert:1.5.0' dependency 'org.slf4j:jcl-over-slf4j:1.7.25' From 33b0af47ef3d188262934936c3f1179142f3ce95 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 24 Jul 2018 16:52:19 -0400 Subject: [PATCH 168/226] Update to selenium-support:3.13.0 Fixes gh-5578 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 5ad133f4d82..3a5fcf50bc9 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -186,7 +186,7 @@ dependencyManagement { dependency 'org.reactivestreams:reactive-streams:1.0.1' dependency 'org.seleniumhq.selenium:htmlunit-driver:2.31.0' dependency 'org.seleniumhq.selenium:selenium-java:3.13.0' - dependency 'org.seleniumhq.selenium:selenium-support:3.12.0' + dependency 'org.seleniumhq.selenium:selenium-support:3.13.0' dependency 'org.skyscreamer:jsonassert:1.5.0' dependency 'org.slf4j:jcl-over-slf4j:1.7.25' dependency 'org.slf4j:jul-to-slf4j:1.7.25' From 9fa1529f89fcb09f8130b83ba28ce551aad8b2ed Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Thu, 26 Jul 2018 23:04:21 +0900 Subject: [PATCH 169/226] Fix typo (#5580) --- .../security/oauth2/client/registration/ClientRegistration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java index 4b9bb1e24b2..9783ff44c1f 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java @@ -269,7 +269,7 @@ private Builder(String registrationId) { } /** - * Sets the client identifier. + * Sets the registration id. * * @param registrationId the registration id * @return the {@link Builder} From 1fd7c398858cac3ea4c1e323e6db5d74a7eef1d6 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 24 Jul 2018 09:43:26 -0400 Subject: [PATCH 170/226] Add HttpServletResponse param to removeAuthorizationRequest Fixes gh-5313 --- .../web/AuthorizationRequestRepository.java | 13 +++++++++++++ ...nOAuth2AuthorizationRequestRepository.java | 8 +++++++- .../OAuth2AuthorizationCodeGrantFilter.java | 3 ++- .../web/OAuth2LoginAuthenticationFilter.java | 3 ++- ...h2AuthorizationRequestRepositoryTests.java | 19 ++++++++++++++----- 5 files changed, 38 insertions(+), 8 deletions(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthorizationRequestRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthorizationRequestRepository.java index 9e316f0fa7a..72226656c7a 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthorizationRequestRepository.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthorizationRequestRepository.java @@ -63,9 +63,22 @@ void saveAuthorizationRequest(T authorizationRequest, HttpServletRequest request * Removes and returns the {@link OAuth2AuthorizationRequest} associated to the * provided {@code HttpServletRequest} or if not available returns {@code null}. * + * @deprecated Use {@link #removeAuthorizationRequest(HttpServletRequest, HttpServletResponse)} instead * @param request the {@code HttpServletRequest} * @return the removed {@link OAuth2AuthorizationRequest} or {@code null} if not available */ T removeAuthorizationRequest(HttpServletRequest request); + /** + * Removes and returns the {@link OAuth2AuthorizationRequest} associated to the + * provided {@code HttpServletRequest} or if not available returns {@code null}. + * + * @since 5.1 + * @param request the {@code HttpServletRequest} + * @param response the {@code HttpServletResponse} + * @return the {@link OAuth2AuthorizationRequest} or {@code null} if not available + */ + default T removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) { + return removeAuthorizationRequest(request); + } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepository.java index 09e1b6bd713..3cc9517e6f3 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepository.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepository.java @@ -58,7 +58,7 @@ public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationReq Assert.notNull(request, "request cannot be null"); Assert.notNull(response, "response cannot be null"); if (authorizationRequest == null) { - this.removeAuthorizationRequest(request); + this.removeAuthorizationRequest(request, response); return; } String state = authorizationRequest.getState(); @@ -85,6 +85,12 @@ public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest return originalRequest; } + @Override + public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) { + Assert.notNull(response, "response cannot be null"); + return this.removeAuthorizationRequest(request); + } + /** * Gets the state parameter from the {@link HttpServletRequest} * @param request the request to use diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java index 841e02a3917..798f49831d0 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java @@ -158,7 +158,8 @@ private boolean shouldProcessAuthorizationResponse(HttpServletRequest request) { private void processAuthorizationResponse(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest(request); + OAuth2AuthorizationRequest authorizationRequest = + this.authorizationRequestRepository.removeAuthorizationRequest(request, response); String registrationId = (String) authorizationRequest.getAdditionalParameters().get(OAuth2ParameterNames.REGISTRATION_ID); ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java index 18e4cb7b275..c44512e508e 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java @@ -156,7 +156,8 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } - OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest(request); + OAuth2AuthorizationRequest authorizationRequest = + this.authorizationRequestRepository.removeAuthorizationRequest(request, response); if (authorizationRequest == null) { OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepositoryTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepositoryTests.java index 3bb2ed3244b..ace3225af17 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepositoryTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepositoryTests.java @@ -217,9 +217,16 @@ public void saveAuthorizationRequestWhenNullThenRemoved() { assertThat(loadedAuthorizationRequest).isNull(); } - @Test(expected = IllegalArgumentException.class) + @Test public void removeAuthorizationRequestWhenHttpServletRequestIsNullThenThrowIllegalArgumentException() { - this.authorizationRequestRepository.removeAuthorizationRequest(null); + assertThatThrownBy(() -> this.authorizationRequestRepository.removeAuthorizationRequest( + null, new MockHttpServletResponse())).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void removeAuthorizationRequestWhenHttpServletResponseIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.authorizationRequestRepository.removeAuthorizationRequest( + new MockHttpServletRequest(), null)).isInstanceOf(IllegalArgumentException.class); } @Test @@ -234,7 +241,7 @@ public void removeAuthorizationRequestWhenSavedThenRemoved() { request.addParameter(OAuth2ParameterNames.STATE, authorizationRequest.getState()); OAuth2AuthorizationRequest removedAuthorizationRequest = - this.authorizationRequestRepository.removeAuthorizationRequest(request); + this.authorizationRequestRepository.removeAuthorizationRequest(request, response); OAuth2AuthorizationRequest loadedAuthorizationRequest = this.authorizationRequestRepository.loadAuthorizationRequest(request); @@ -255,7 +262,7 @@ public void removeAuthorizationRequestWhenSavedThenRemovedFromSession() { request.addParameter(OAuth2ParameterNames.STATE, authorizationRequest.getState()); OAuth2AuthorizationRequest removedAuthorizationRequest = - this.authorizationRequestRepository.removeAuthorizationRequest(request); + this.authorizationRequestRepository.removeAuthorizationRequest(request, response); String sessionAttributeName = HttpSessionOAuth2AuthorizationRequestRepository.class.getName() + ".AUTHORIZATION_REQUEST"; @@ -269,8 +276,10 @@ public void removeAuthorizationRequestWhenNotSavedThenNotRemoved() { MockHttpServletRequest request = new MockHttpServletRequest(); request.addParameter(OAuth2ParameterNames.STATE, "state-1234"); + MockHttpServletResponse response = new MockHttpServletResponse(); + OAuth2AuthorizationRequest removedAuthorizationRequest = - this.authorizationRequestRepository.removeAuthorizationRequest(request); + this.authorizationRequestRepository.removeAuthorizationRequest(request, response); assertThat(removedAuthorizationRequest).isNull(); } From f8fbe1647bd5ecabc4a6e7a85c6fa0cfd9046155 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 25 Jul 2018 21:07:55 -0700 Subject: [PATCH 171/226] Add SimpleSavedRequest Fixes: gh-5581 --- .../web/savedrequest/SimpleSavedRequest.java | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 web/src/main/java/org/springframework/security/web/savedrequest/SimpleSavedRequest.java diff --git a/web/src/main/java/org/springframework/security/web/savedrequest/SimpleSavedRequest.java b/web/src/main/java/org/springframework/security/web/savedrequest/SimpleSavedRequest.java new file mode 100644 index 00000000000..c53839465a8 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/savedrequest/SimpleSavedRequest.java @@ -0,0 +1,131 @@ +/* + * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.web.savedrequest; + +import org.springframework.util.Assert; + +import javax.servlet.http.Cookie; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * A Bean implementation of SavedRequest + * @author Rob Winch + * @since 5.1 + */ +public class SimpleSavedRequest implements SavedRequest { + private String redirectUrl; + + private List cookies = new ArrayList<>(); + + private String method = "GET"; + + private Map> headers = new HashMap<>(); + + private List locales = new ArrayList<>(); + + private Map parameters = new HashMap<>(); + + public SimpleSavedRequest() {} + + public SimpleSavedRequest(String redirectUrl) { + this.redirectUrl = redirectUrl; + } + + public SimpleSavedRequest(SavedRequest request) { + this.redirectUrl = request.getRedirectUrl(); + this.cookies = request.getCookies(); + for (String headerName : request.getHeaderNames()) { + this.headers.put(headerName, request.getHeaderValues(headerName)); + } + this.locales = request.getLocales(); + this.parameters = request.getParameterMap(); + } + + @Override + public String getRedirectUrl() { + return this.redirectUrl; + } + + @Override + public List getCookies() { + return this.cookies; + } + + @Override + public String getMethod() { + return null; + } + + @Override + public List getHeaderValues(String name) { + return this.headers.getOrDefault(name, new ArrayList<>()); + } + + @Override + public Collection getHeaderNames() { + return this.headers.keySet(); + } + + @Override + public List getLocales() { + return this.locales; + } + + @Override + public String[] getParameterValues(String name) { + return this.parameters.getOrDefault(name, new String[0]); + } + + @Override + public Map getParameterMap() { + return this.parameters; + } + + public void setRedirectUrl(String redirectUrl) { + Assert.notNull(redirectUrl, "redirectUrl cannot be null"); + this.redirectUrl = redirectUrl; + } + + public void setCookies(List cookies) { + Assert.notNull(cookies, "cookies cannot be null"); + this.cookies = cookies; + } + + public void setMethod(String method) { + Assert.notNull(method, "method cannot be null"); + this.method = method; + } + + public void setHeaders(Map> headers) { + Assert.notNull(headers, "headers cannot be null"); + this.headers = headers; + } + + public void setLocales(List locales) { + Assert.notNull("locales cannot be null"); + this.locales = locales; + } + + public void setParameters(Map parameters) { + Assert.notNull(parameters, "parameters cannot be null"); + this.parameters = parameters; + } +} From 64893f05d71bddc8f10259e2134612fbbef6f602 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 25 Jul 2018 21:15:10 -0700 Subject: [PATCH 172/226] Rename FormLoginConfigurerTests to FormLoginConfigurerSpec Rename so can add new Java based tests for gh-5582 Issue: gh-5582 --- ...ginConfigurerTests.groovy => FormLoginConfigurerSpec.groovy} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/{FormLoginConfigurerTests.groovy => FormLoginConfigurerSpec.groovy} (99%) diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerSpec.groovy similarity index 99% rename from config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.groovy rename to config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerSpec.groovy index 1f9eeea0d31..09848e960ae 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerSpec.groovy @@ -57,7 +57,7 @@ import spock.lang.Unroll * * @author Rob Winch */ -class FormLoginConfigurerTests extends BaseSpringSpec { +class FormLoginConfigurerSpec extends BaseSpringSpec { def "Form Login"() { when: "load formLogin()" context = new AnnotationConfigApplicationContext(FormLoginConfig) From 5ada70fd3a5281a14e37ac1c89611dc91f262b95 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 26 Jul 2018 12:46:13 -0500 Subject: [PATCH 173/226] Simplify Configuring RequestCache Now the RequestCache is configured on any default success handler. Fixes: gh-5582 --- ...bstractAuthenticationFilterConfigurer.java | 10 ++- .../configurers/FormLoginConfigurerTests.java | 72 +++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java index a6477923541..c39919867cc 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java @@ -32,6 +32,7 @@ import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; +import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.util.matcher.AndRequestMatcher; import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; import org.springframework.security.web.util.matcher.NegatedRequestMatcher; @@ -65,7 +66,8 @@ public abstract class AbstractAuthenticationFilterConfigurer authenticationDetailsSource; - private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); + private SavedRequestAwareAuthenticationSuccessHandler defaultSuccessHandler = new SavedRequestAwareAuthenticationSuccessHandler(); + private AuthenticationSuccessHandler successHandler = this.defaultSuccessHandler; private LoginUrlAuthenticationEntryPoint authenticationEntryPoint; @@ -128,6 +130,7 @@ public final T defaultSuccessUrl(String defaultSuccessUrl, boolean alwaysUse) { SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler(); handler.setDefaultTargetUrl(defaultSuccessUrl); handler.setAlwaysUseDefaultTargetUrl(alwaysUse); + this.defaultSuccessHandler = handler; return successHandler(handler); } @@ -279,6 +282,11 @@ public void configure(B http) throws Exception { authenticationEntryPoint.setPortMapper(portMapper); } + RequestCache requestCache = http.getSharedObject(RequestCache.class); + if (requestCache != null) { + this.defaultSuccessHandler.setRequestCache(requestCache); + } + authFilter.setAuthenticationManager(http .getSharedObject(AuthenticationManager.class)); authFilter.setAuthenticationSuccessHandler(successHandler); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java new file mode 100644 index 00000000000..0d32a4d0a3a --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.configurers; + +import org.junit.Rule; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.config.users.AuthenticationTestConfiguration; +import org.springframework.security.web.savedrequest.RequestCache; +import org.springframework.test.web.servlet.MockMvc; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class FormLoginConfigurerTests { + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + private MockMvc mockMvc; + + @Test + public void requestCache() throws Exception { + this.spring.register(RequestCacheConfig.class, + AuthenticationTestConfiguration.class).autowire(); + + RequestCacheConfig config = this.spring.getContext().getBean(RequestCacheConfig.class); + + this.mockMvc.perform(formLogin()) + .andExpect(authenticated()); + + verify(config.requestCache).getRequest(any(), any()); + } + + @EnableWebSecurity + static class RequestCacheConfig extends WebSecurityConfigurerAdapter { + private RequestCache requestCache = mock(RequestCache.class); + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .formLogin().and() + .requestCache() + .requestCache(this.requestCache); + } + } +} From b7f4e7344357f6a8328f19c1fb75942b46ef41e3 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 26 Jul 2018 14:09:22 -0500 Subject: [PATCH 174/226] Default RequestCache as @Bean Fixes: gh-5583 --- .../configurers/RequestCacheConfigurer.java | 17 ++++++++++++++ .../configurers/FormLoginConfigurerTests.java | 22 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java index 628b9eca21d..4cc62c887b3 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java @@ -19,6 +19,8 @@ import java.util.Collections; import java.util.List; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; import org.springframework.http.MediaType; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -113,11 +115,26 @@ private RequestCache getRequestCache(H http) { if (result != null) { return result; } + result = getBeanOrNull(RequestCache.class); + if (result != null) { + return result; + } HttpSessionRequestCache defaultCache = new HttpSessionRequestCache(); defaultCache.setRequestMatcher(createDefaultSavedRequestMatcher(http)); return defaultCache; } + private T getBeanOrNull(Class type) { + ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); + if (context == null) { + return null; + } + try { + return context.getBean(type); + } catch (NoSuchBeanDefinitionException e) { + return null; + } + } @SuppressWarnings("unchecked") private RequestMatcher createDefaultSavedRequestMatcher(H http) { ContentNegotiationStrategy contentNegotiationStrategy = http diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java index 0d32a4d0a3a..a8e4663988a 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java @@ -19,6 +19,7 @@ import org.junit.Rule; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @@ -69,4 +70,25 @@ protected void configure(HttpSecurity http) throws Exception { .requestCache(this.requestCache); } } + + @Test + public void requestCacheAsBean() throws Exception { + this.spring.register(RequestCacheBeanConfig.class, + AuthenticationTestConfiguration.class).autowire(); + + RequestCache requestCache = this.spring.getContext().getBean(RequestCache.class); + + this.mockMvc.perform(formLogin()) + .andExpect(authenticated()); + + verify(requestCache).getRequest(any(), any()); + } + + @EnableWebSecurity + static class RequestCacheBeanConfig { + @Bean + RequestCache requestCache() { + return mock(RequestCache.class); + } + } } From 626c5d949bf9a79091d7733f41367134e1370295 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 26 Jul 2018 12:50:00 -0500 Subject: [PATCH 175/226] HttpSessionRequestCache Allow Any SavedRequest Fixes: gh-5585 --- .../savedrequest/HttpSessionRequestCache.java | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/savedrequest/HttpSessionRequestCache.java b/web/src/main/java/org/springframework/security/web/savedrequest/HttpSessionRequestCache.java index a959afa9e2a..0cd238bdbfc 100644 --- a/web/src/main/java/org/springframework/security/web/savedrequest/HttpSessionRequestCache.java +++ b/web/src/main/java/org/springframework/security/web/savedrequest/HttpSessionRequestCache.java @@ -23,6 +23,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.security.web.PortResolver; import org.springframework.security.web.PortResolverImpl; +import org.springframework.security.web.util.UrlUtils; import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -88,13 +89,9 @@ public void removeRequest(HttpServletRequest currentRequest, public HttpServletRequest getMatchingRequest(HttpServletRequest request, HttpServletResponse response) { - DefaultSavedRequest saved = (DefaultSavedRequest) getRequest(request, response); + SavedRequest saved = getRequest(request, response); - if (saved == null) { - return null; - } - - if (!saved.doesRequestMatch(request, portResolver)) { + if (!matchesSavedRequest(request, saved)) { logger.debug("saved request doesn't match"); return null; } @@ -104,6 +101,20 @@ public HttpServletRequest getMatchingRequest(HttpServletRequest request, return new SavedRequestAwareWrapper(saved, request); } + private boolean matchesSavedRequest(HttpServletRequest request, SavedRequest savedRequest) { + if (savedRequest == null) { + return false; + } + + if (savedRequest instanceof DefaultSavedRequest) { + DefaultSavedRequest defaultSavedRequest = (DefaultSavedRequest) savedRequest; + return defaultSavedRequest.doesRequestMatch(request, this.portResolver); + } + + String currentUrl = UrlUtils.buildFullRequestUrl(request); + return savedRequest.getRedirectUrl().equals(currentUrl); + } + /** * Allows selective use of saved requests for a subset of requests. By default any * request will be cached by the {@code saveRequest} method. From 9707f65a72b7689fb8acb3564b3316e57b431148 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 26 Jul 2018 14:55:09 -0500 Subject: [PATCH 176/226] Update to Reactor Californium M1 Fixes: gh-5587 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 3a5fcf50bc9..9540bf8b3f1 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -1,5 +1,5 @@ if (!project.hasProperty('reactorVersion')) { - ext.reactorVersion = 'Californium-BUILD-SNAPSHOT' + ext.reactorVersion = 'Californium-M1' } if (!project.hasProperty('springVersion')) { From d716ef6a1718312ccb9a7d3b65c9a77b7588f131 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 26 Jul 2018 14:55:27 -0500 Subject: [PATCH 177/226] Update to Spring Framework 5.1.0.RC1 Fixes: gh-5588 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 9540bf8b3f1..99718cf207c 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -3,7 +3,7 @@ if (!project.hasProperty('reactorVersion')) { } if (!project.hasProperty('springVersion')) { - ext.springVersion = '5.1.0.BUILD-SNAPSHOT' + ext.springVersion = '5.1.0.RC1' } if (!project.hasProperty('springDataVersion')) { From 6459768b10a6b4a1a9ecc6fb0b032096ca7cf1f6 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 26 Jul 2018 14:55:53 -0500 Subject: [PATCH 178/226] Update to Spring Data Lovelace RC1 Fixes: gh-5589 --- gradle/dependency-management.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 99718cf207c..178f7f7f2cd 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -7,7 +7,7 @@ if (!project.hasProperty('springVersion')) { } if (!project.hasProperty('springDataVersion')) { - ext.springDataVersion = 'Lovelace-BUILD-SNAPSHOT' + ext.springDataVersion = 'Lovelace-RC1' } dependencyManagement { From 2034b97a66dc0bd3a0ea10cb81f685a37c84068c Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 26 Jul 2018 15:10:33 -0500 Subject: [PATCH 179/226] Rename to OidcAuthorizationCodeReactiveAuthenticationManager Renamed OidcReactiveAuthenticationManager to OidcAuthorizationCodeReactiveAuthenticationManager since it only handles authorization code flow. Fixes: gh-5530 --- .../config/web/server/ServerHttpSecurity.java | 4 ++-- ...horizationCodeReactiveAuthenticationManager.java} | 4 ++-- ...ationCodeReactiveAuthenticationManagerTests.java} | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) rename oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/{OidcReactiveAuthenticationManager.java => OidcAuthorizationCodeReactiveAuthenticationManager.java} (98%) rename oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/{OidcReactiveAuthenticationManagerTests.java => OidcAuthorizationCodeReactiveAuthenticationManagerTests.java} (93%) diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 0e88db487d7..5347ca008ba 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -46,7 +46,7 @@ import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2LoginReactiveAuthenticationManager; import org.springframework.security.oauth2.client.endpoint.NimbusReactiveAuthorizationCodeTokenResponseClient; -import org.springframework.security.oauth2.client.oidc.authentication.OidcReactiveAuthenticationManager; +import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager; import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; @@ -382,7 +382,7 @@ protected void configure(ServerHttpSecurity http) { boolean oidcAuthenticationProviderEnabled = ClassUtils.isPresent( "org.springframework.security.oauth2.jwt.JwtDecoder", this.getClass().getClassLoader()); if (oidcAuthenticationProviderEnabled) { - OidcReactiveAuthenticationManager oidc = new OidcReactiveAuthenticationManager(client, new OidcReactiveOAuth2UserService(), authorizedClientService); + OidcAuthorizationCodeReactiveAuthenticationManager oidc = new OidcAuthorizationCodeReactiveAuthenticationManager(client, new OidcReactiveOAuth2UserService(), authorizedClientService); manager = new DelegatingReactiveAuthenticationManager(oidc, manager); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java similarity index 98% rename from oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManager.java rename to oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java index c7cd9f89597..877e60e8676 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java @@ -73,7 +73,7 @@ * @see Section 4.1.3 Access Token Request * @see Section 4.1.4 Access Token Response */ -public class OidcReactiveAuthenticationManager implements +public class OidcAuthorizationCodeReactiveAuthenticationManager implements ReactiveAuthenticationManager { private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter"; @@ -91,7 +91,7 @@ public class OidcReactiveAuthenticationManager implements private Function decoderFactory = new DefaultDecoderFactory(); - public OidcReactiveAuthenticationManager( + public OidcAuthorizationCodeReactiveAuthenticationManager( ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient, ReactiveOAuth2UserService userService, ReactiveOAuth2AuthorizedClientService authorizedClientService) { diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManagerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManagerTests.java similarity index 93% rename from oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManagerTests.java rename to oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManagerTests.java index 44120c712a3..362beed4da6 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManagerTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManagerTests.java @@ -65,7 +65,7 @@ * @since 5.1 */ @RunWith(MockitoJUnitRunner.class) -public class OidcReactiveAuthenticationManagerTests { +public class OidcAuthorizationCodeReactiveAuthenticationManagerTests { @Mock private ReactiveOAuth2UserService userService; @@ -99,11 +99,11 @@ public class OidcReactiveAuthenticationManagerTests { private OidcIdToken idToken = new OidcIdToken("token123", Instant.now(), Instant.now().plusSeconds(3600), Collections.singletonMap(IdTokenClaimNames.SUB, "sub123")); - private OidcReactiveAuthenticationManager manager; + private OidcAuthorizationCodeReactiveAuthenticationManager manager; @Before public void setup() { - this.manager = new OidcReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService, + this.manager = new OidcAuthorizationCodeReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService, this.authorizedClientService); when(this.authorizedClientService.saveAuthorizedClient(any(), any())).thenReturn( Mono.empty()); @@ -112,7 +112,7 @@ public void setup() { @Test public void constructorWhenNullAccessTokenResponseClientThenIllegalArgumentException() { this.accessTokenResponseClient = null; - assertThatThrownBy(() -> new OidcReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService, + assertThatThrownBy(() -> new OidcAuthorizationCodeReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService, this.authorizedClientService)) .isInstanceOf(IllegalArgumentException.class); } @@ -120,7 +120,7 @@ public void constructorWhenNullAccessTokenResponseClientThenIllegalArgumentExcep @Test public void constructorWhenNullUserServiceThenIllegalArgumentException() { this.userService = null; - assertThatThrownBy(() -> new OidcReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService, + assertThatThrownBy(() -> new OidcAuthorizationCodeReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService, this.authorizedClientService)) .isInstanceOf(IllegalArgumentException.class); } @@ -128,7 +128,7 @@ public void constructorWhenNullUserServiceThenIllegalArgumentException() { @Test public void constructorWhenNullAuthorizedClientServiceThenIllegalArgumentException() { this.authorizedClientService = null; - assertThatThrownBy(() -> new OidcReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService, + assertThatThrownBy(() -> new OidcAuthorizationCodeReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService, this.authorizedClientService)) .isInstanceOf(IllegalArgumentException.class); } From 1d3acc00c0e19320094de6254c82c04058891cfb Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 26 Jul 2018 15:11:54 -0500 Subject: [PATCH 180/226] Rename to WebClientAuthorizationCodeTokenResponseClient Rename NimbusReactiveAUthorizationCodeTokenResponseClient to WebClientReactiveAuthorizationCodeTokenResponseClient Fixes: gh-5529 --- .../security/config/web/server/ServerHttpSecurity.java | 4 ++-- ...ebClientReactiveAuthorizationCodeTokenResponseClient.java} | 2 +- ...entReactiveAuthorizationCodeTokenResponseClientTests.java} | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/{NimbusReactiveAuthorizationCodeTokenResponseClient.java => WebClientReactiveAuthorizationCodeTokenResponseClient.java} (96%) rename oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/{NimbusReactiveAuthorizationCodeTokenResponseClientTests.java => WebClientReactiveAuthorizationCodeTokenResponseClientTests.java} (98%) diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 5347ca008ba..5559159fbb6 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -45,7 +45,7 @@ import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2LoginReactiveAuthenticationManager; -import org.springframework.security.oauth2.client.endpoint.NimbusReactiveAuthorizationCodeTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.WebClientReactiveAuthorizationCodeTokenResponseClient; import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager; import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -374,7 +374,7 @@ protected void configure(ServerHttpSecurity http) { ReactiveOAuth2AuthorizedClientService authorizedClientService = getAuthorizedClientService(); OAuth2AuthorizationRequestRedirectWebFilter oauthRedirectFilter = new OAuth2AuthorizationRequestRedirectWebFilter(clientRegistrationRepository); - NimbusReactiveAuthorizationCodeTokenResponseClient client = new NimbusReactiveAuthorizationCodeTokenResponseClient(); + WebClientReactiveAuthorizationCodeTokenResponseClient client = new WebClientReactiveAuthorizationCodeTokenResponseClient(); ReactiveOAuth2UserService userService = new DefaultReactiveOAuth2UserService(); ReactiveAuthenticationManager manager = new OAuth2LoginReactiveAuthenticationManager(client, userService, authorizedClientService); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java similarity index 96% rename from oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClient.java rename to oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java index 1f7833087b6..83119c6c6bc 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java @@ -47,7 +47,7 @@ * @see Section 4.1.3 Access Token Request (Authorization Code Grant) * @see Section 4.1.4 Access Token Response (Authorization Code Grant) */ -public class NimbusReactiveAuthorizationCodeTokenResponseClient implements ReactiveOAuth2AccessTokenResponseClient { +public class WebClientReactiveAuthorizationCodeTokenResponseClient implements ReactiveOAuth2AccessTokenResponseClient { private WebClient webClient = WebClient.builder() .filter(ExchangeFilterFunctions.basicAuthentication()) .build(); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java similarity index 98% rename from oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClientTests.java rename to oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java index 2fe61e1e61d..49de6e5c790 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java @@ -44,10 +44,10 @@ * @author Rob Winch * @since 5.1 */ -public class NimbusReactiveAuthorizationCodeTokenResponseClientTests { +public class WebClientReactiveAuthorizationCodeTokenResponseClientTests { private ClientRegistration.Builder clientRegistration; - private NimbusReactiveAuthorizationCodeTokenResponseClient tokenResponseClient = new NimbusReactiveAuthorizationCodeTokenResponseClient(); + private WebClientReactiveAuthorizationCodeTokenResponseClient tokenResponseClient = new WebClientReactiveAuthorizationCodeTokenResponseClient(); private MockWebServer server; From 53c5915dc020fd953ddd99f793e428789060892c Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 26 Jul 2018 15:21:11 -0500 Subject: [PATCH 181/226] Release 5.1.0.M2 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 84ac09f34e0..3abffbff26d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ gaeVersion=1.9.64 springBootVersion=2.0.3.RELEASE -version=5.1.0.BUILD-SNAPSHOT +version=5.1.0.M2 From bfeab9291701a010938672bfc04116cad487c320 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 26 Jul 2018 15:22:02 -0500 Subject: [PATCH 182/226] Next Development Version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 3abffbff26d..84ac09f34e0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ gaeVersion=1.9.64 springBootVersion=2.0.3.RELEASE -version=5.1.0.M2 +version=5.1.0.BUILD-SNAPSHOT From 1393699eeda9d72776b7e58b7157c9cdbf1ee897 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 26 Jul 2018 19:37:40 -0500 Subject: [PATCH 183/226] Disable Snapshot for release --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index b4a7554c829..36a9aabbb08 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -44,7 +44,7 @@ try { node { checkout scm try { - sh "./gradlew clean test -PspringVersion='5.+' -PreactorVersion=Californium-BUILD-SNAPSHOT -PspringDataVersion=Lovelace-BUILD-SNAPSHOT --refresh-dependencies --no-daemon --stacktrace" + //sh "./gradlew clean test -PspringVersion='5.+' -PreactorVersion=Californium-BUILD-SNAPSHOT -PspringDataVersion=Lovelace-BUILD-SNAPSHOT --refresh-dependencies --no-daemon --stacktrace" } catch(Exception e) { currentBuild.result = 'FAILED: snapshots' throw e From d36182adcdf166d9df76e5aece7f91cf74d55898 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 26 Jul 2018 15:21:11 -0500 Subject: [PATCH 184/226] Release 5.1.0.M2 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 84ac09f34e0..3abffbff26d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ gaeVersion=1.9.64 springBootVersion=2.0.3.RELEASE -version=5.1.0.BUILD-SNAPSHOT +version=5.1.0.M2 From eab67539c77c36bf1418554f5d8a3072c1342c66 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 26 Jul 2018 20:11:17 -0500 Subject: [PATCH 185/226] Next Development Version --- Jenkinsfile | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 36a9aabbb08..b4a7554c829 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -44,7 +44,7 @@ try { node { checkout scm try { - //sh "./gradlew clean test -PspringVersion='5.+' -PreactorVersion=Californium-BUILD-SNAPSHOT -PspringDataVersion=Lovelace-BUILD-SNAPSHOT --refresh-dependencies --no-daemon --stacktrace" + sh "./gradlew clean test -PspringVersion='5.+' -PreactorVersion=Californium-BUILD-SNAPSHOT -PspringDataVersion=Lovelace-BUILD-SNAPSHOT --refresh-dependencies --no-daemon --stacktrace" } catch(Exception e) { currentBuild.result = 'FAILED: snapshots' throw e diff --git a/gradle.properties b/gradle.properties index 3abffbff26d..84ac09f34e0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ gaeVersion=1.9.64 springBootVersion=2.0.3.RELEASE -version=5.1.0.M2 +version=5.1.0.BUILD-SNAPSHOT From f24a368d6af268b3373bf14dc81f166c164bcbf9 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 5 Jul 2018 16:33:16 -0500 Subject: [PATCH 186/226] Extract JwtConverter Issue: gh-5605 --- .../JwtAuthenticationProvider.java | 38 +---------- .../resource/authentication/JwtConverter.java | 67 +++++++++++++++++++ 2 files changed, 69 insertions(+), 36 deletions(-) create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtConverter.java diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.java index 68c022ff7d3..b70bd2accde 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.java @@ -15,17 +15,13 @@ */ package org.springframework.security.oauth2.server.resource.authentication; -import java.util.Arrays; import java.util.Collection; -import java.util.Collections; -import java.util.stream.Collectors; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.jwt.Jwt; @@ -35,7 +31,6 @@ import org.springframework.security.oauth2.server.resource.BearerTokenError; import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; /** * An {@link AuthenticationProvider} implementation of the {@link Jwt}-encoded @@ -64,10 +59,7 @@ public final class JwtAuthenticationProvider implements AuthenticationProvider { private final JwtDecoder jwtDecoder; - private static final Collection WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES = - Arrays.asList("scope", "scp"); - - private static final String SCOPE_AUTHORITY_PREFIX = "SCOPE_"; + private final JwtConverter jwtConverter = new JwtConverter(); public JwtAuthenticationProvider(JwtDecoder jwtDecoder) { Assert.notNull(jwtDecoder, "jwtDecoder cannot be null"); @@ -101,16 +93,7 @@ public Authentication authenticate(Authentication authentication) throws Authent } throw new OAuth2AuthenticationException(invalidToken, failed); } - - Collection authorities = - this.getScopes(jwt) - .stream() - .map(authority -> SCOPE_AUTHORITY_PREFIX + authority) - .map(SimpleGrantedAuthority::new) - .collect(Collectors.toList()); - - JwtAuthenticationToken token = new JwtAuthenticationToken(jwt, authorities); - + JwtAuthenticationToken token = this.jwtConverter.convert(jwt); token.setDetails(bearer.getDetails()); return token; @@ -131,21 +114,4 @@ private static OAuth2Error invalidToken(String message) { message, "https://tools.ietf.org/html/rfc6750#section-3.1"); } - - private static Collection getScopes(Jwt jwt) { - for ( String attributeName : WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES ) { - Object scopes = jwt.getClaims().get(attributeName); - if (scopes instanceof String) { - if (StringUtils.hasText((String) scopes)) { - return Arrays.asList(((String) scopes).split(" ")); - } else { - return Collections.emptyList(); - } - } else if (scopes instanceof Collection) { - return (Collection) scopes; - } - } - - return Collections.emptyList(); - } } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtConverter.java new file mode 100644 index 00000000000..0ade72982fa --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtConverter.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource.authentication; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.util.StringUtils; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * @author Rob Winch + * @since 5.1 + */ +class JwtConverter { + private static final String SCOPE_AUTHORITY_PREFIX = "SCOPE_"; + + private static final Collection WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES = + Arrays.asList("scope", "scp"); + + + JwtAuthenticationToken convert(Jwt jwt) { + Collection authorities = + this.getScopes(jwt) + .stream() + .map(authority -> SCOPE_AUTHORITY_PREFIX + authority) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + return new JwtAuthenticationToken(jwt, authorities); + } + + private Collection getScopes(Jwt jwt) { + for ( String attributeName : WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES ) { + Object scopes = jwt.getClaims().get(attributeName); + if (scopes instanceof String) { + if (StringUtils.hasText((String) scopes)) { + return Arrays.asList(((String) scopes).split(" ")); + } else { + return Collections.emptyList(); + } + } else if (scopes instanceof Collection) { + return (Collection) scopes; + } + } + + return Collections.emptyList(); + } +} From 2905b90a3e2b7bc15f9ffd0c51c2a17efa42b5a3 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 6 Jul 2018 10:05:41 -0500 Subject: [PATCH 187/226] Add JwtReactiveAuthenticationManager Issue: gh-5605 --- ...ing-security-oauth2-resource-server.gradle | 9 +- .../JwtReactiveAuthenticationManager.java | 72 +++++++++++ ...JwtReactiveAuthenticationManagerTests.java | 118 ++++++++++++++++++ 3 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManager.java create mode 100644 oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManagerTests.java diff --git a/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle b/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle index b05fa58afe4..7fac327e252 100644 --- a/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle +++ b/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle @@ -7,8 +7,13 @@ dependencies { compile springCoreDependency optional project(':spring-security-oauth2-jose') - - testCompile 'com.squareup.okhttp3:mockwebserver' + optional 'io.projectreactor:reactor-core' + optional 'org.springframework:spring-webflux' provided 'javax.servlet:javax.servlet-api' + + testCompile 'com.squareup.okhttp3:mockwebserver' + testCompile 'com.fasterxml.jackson.core:jackson-databind' + testCompile 'io.projectreactor.netty:reactor-netty' + testCompile 'io.projectreactor:reactor-test' } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManager.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManager.java new file mode 100644 index 00000000000..84b40cbe170 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManager.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource.authentication; + +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes; +import org.springframework.util.Assert; +import reactor.core.publisher.Mono; + +/** + * A {@link ReactiveAuthenticationManager} for Jwt tokens. + * + * @author Rob Winch + * @since 5.1 + */ +public class JwtReactiveAuthenticationManager implements ReactiveAuthenticationManager { + private final JwtConverter jwtConverter = new JwtConverter(); + + private final ReactiveJwtDecoder jwtDecoder; + + public JwtReactiveAuthenticationManager(ReactiveJwtDecoder jwtDecoder) { + Assert.notNull(jwtDecoder, "jwtDecoder cannot be null"); + this.jwtDecoder = jwtDecoder; + } + + @Override + public Mono authenticate(Authentication authentication) { + return Mono.justOrEmpty(authentication) + .filter(a -> a instanceof BearerTokenAuthenticationToken) + .cast(BearerTokenAuthenticationToken.class) + .map(BearerTokenAuthenticationToken::getToken) + .flatMap(this.jwtDecoder::decode) + .map(this.jwtConverter::convert) + .cast(Authentication.class) + .onErrorMap(JwtException.class, this::onError); + } + + private OAuth2AuthenticationException onError(JwtException e) { + OAuth2Error invalidRequest = invalidToken(e.getMessage()); + return new OAuth2AuthenticationException(invalidRequest, e.getMessage()); + } + + private static OAuth2Error invalidToken(String message) { + return new BearerTokenError( + BearerTokenErrorCodes.INVALID_TOKEN, + HttpStatus.UNAUTHORIZED, + message, + "https://tools.ietf.org/html/rfc6750#section-3.1"); + } +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManagerTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManagerTests.java new file mode 100644 index 00000000000..ac4ca5b2981 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManagerTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource.authentication; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import reactor.core.publisher.Mono; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + * @since 5.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class JwtReactiveAuthenticationManagerTests { + @Mock + private ReactiveJwtDecoder jwtDecoder; + + private JwtReactiveAuthenticationManager manager; + + private Jwt jwt; + + @Before + public void setup() { + this.manager = new JwtReactiveAuthenticationManager(this.jwtDecoder); + + Map claims = new HashMap<>(); + claims.put("scope", "message:read message:write"); + Instant issuedAt = Instant.now(); + Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600); + this.jwt = new Jwt("jwt", issuedAt, expiresAt, claims, claims); + } + + @Test + public void constructorWhenJwtDecoderNullThenIllegalArgumentException() { + this.jwtDecoder = null; + assertThatCode(() -> new JwtReactiveAuthenticationManager(this.jwtDecoder)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void authenticateWhenWrongTypeThenEmpty() { + TestingAuthenticationToken token = new TestingAuthenticationToken("foo", "bar"); + + assertThat(this.manager.authenticate(token).block()).isNull(); + } + + @Test + public void authenticateWhenEmptyJwtThenEmpty() { + BearerTokenAuthenticationToken token = new BearerTokenAuthenticationToken("token-1"); + when(this.jwtDecoder.decode(token.getToken())).thenReturn(Mono.empty()); + + assertThat(this.manager.authenticate(token).block()).isNull(); + } + + @Test + public void authenticateWhenJwtExceptionThenOAuth2AuthenticationException() { + BearerTokenAuthenticationToken token = new BearerTokenAuthenticationToken("token-1"); + when(this.jwtDecoder.decode(any())).thenReturn(Mono.error(new JwtException("Oops"))); + + assertThatCode(() -> this.manager.authenticate(token).block()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void authenticateWhenNotJwtExceptionThenPropagates() { + BearerTokenAuthenticationToken token = new BearerTokenAuthenticationToken("token-1"); + when(this.jwtDecoder.decode(any())).thenReturn(Mono.error(new RuntimeException("Oops"))); + + assertThatCode(() -> this.manager.authenticate(token).block()) + .isInstanceOf(RuntimeException.class); + } + + @Test + public void authenticateWhenJwtThenSuccess() { + BearerTokenAuthenticationToken token = new BearerTokenAuthenticationToken("token-1"); + when(this.jwtDecoder.decode(token.getToken())).thenReturn(Mono.just(this.jwt)); + + Authentication authentication = this.manager.authenticate(token).block(); + + assertThat(authentication).isNotNull(); + assertThat(authentication.isAuthenticated()).isTrue(); + assertThat(authentication.getAuthorities()).extracting(GrantedAuthority::getAuthority).containsOnly("SCOPE_message:read", "SCOPE_message:write"); + } +} From 590d72d5c12a9be9d12628c57238a8772cb77f99 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 6 Jul 2018 10:25:55 -0500 Subject: [PATCH 188/226] BearerTokenServerAuthenticationEntryPoint Issue: gh-5605 --- ...erTokenServerAuthenticationEntryPoint.java | 121 ++++++++++++++++++ ...enServerAuthenticationEntryPointTests.java | 97 ++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPoint.java create mode 100644 oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPointTests.java diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPoint.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPoint.java new file mode 100644 index 00000000000..d266e55efc6 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPoint.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource.web.server; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.server.ServerAuthenticationEntryPoint; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * An {@link AuthenticationEntryPoint} implementation used to commence authentication of protected resource requests + * using {@link BearerTokenAuthenticationFilter}. + *

      + * Uses information provided by {@link BearerTokenError} to set HTTP response status code and populate + * {@code WWW-Authenticate} HTTP header. + * + * @author Rob Winch + * @since 5.1 + * @see BearerTokenError + * @see RFC 6750 Section 3: The WWW-Authenticate + * Response Header Field + */ +public final class BearerTokenServerAuthenticationEntryPoint implements + ServerAuthenticationEntryPoint { + + private String realmName; + + public void setRealmName(String realmName) { + this.realmName = realmName; + } + + @Override + public Mono commence(ServerWebExchange exchange, AuthenticationException authException) { + HttpStatus status = getStatus(authException); + + Map parameters = createParameters(authException); + String wwwAuthenticate = computeWWWAuthenticateHeaderValue(parameters); + ServerHttpResponse response = exchange.getResponse(); + response.getHeaders().set(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate); + response.setStatusCode(status); + return response.setComplete(); + } + + private Map createParameters(AuthenticationException authException) { + Map parameters = new LinkedHashMap<>(); + if (this.realmName != null) { + parameters.put("realm", this.realmName); + } + + if (authException instanceof OAuth2AuthenticationException) { + OAuth2Error error = ((OAuth2AuthenticationException) authException).getError(); + + parameters.put("error", error.getErrorCode()); + + if (StringUtils.hasText(error.getDescription())) { + parameters.put("error_description", error.getDescription()); + } + + if (StringUtils.hasText(error.getUri())) { + parameters.put("error_uri", error.getUri()); + } + + if (error instanceof BearerTokenError) { + BearerTokenError bearerTokenError = (BearerTokenError) error; + + if (StringUtils.hasText(bearerTokenError.getScope())) { + parameters.put("scope", bearerTokenError.getScope()); + } + } + } + return parameters; + } + + private HttpStatus getStatus(AuthenticationException authException) { + if (authException instanceof OAuth2AuthenticationException) { + OAuth2Error error = ((OAuth2AuthenticationException) authException).getError(); + if (error instanceof BearerTokenError) { + return ((BearerTokenError) error).getHttpStatus(); + } + } + return HttpStatus.UNAUTHORIZED; + } + + private static String computeWWWAuthenticateHeaderValue(Map parameters) { + String wwwAuthenticate = "Bearer"; + if (!parameters.isEmpty()) { + wwwAuthenticate += parameters.entrySet().stream() + .map(attribute -> attribute.getKey() + "=\"" + attribute.getValue() + "\"") + .collect(Collectors.joining(", ", " ", "")); + } + + return wwwAuthenticate; + } +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPointTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPointTests.java new file mode 100644 index 00000000000..cb60fd01312 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPointTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource.web.server; + +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.http.server.reactive.MockServerHttpResponse; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.server.resource.BearerTokenError; + +import static org.assertj.core.api.Assertions.*; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class BearerTokenServerAuthenticationEntryPointTests { + private BearerTokenServerAuthenticationEntryPoint entryPoint = new BearerTokenServerAuthenticationEntryPoint(); + + private MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")); + + @Test + public void commenceWhenNotOAuth2AuthenticationExceptionThenBearer() { + this.entryPoint.commence(this.exchange, new BadCredentialsException("")).block(); + + assertThat(getResponse().getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE)).isEqualTo("Bearer"); + assertThat(getResponse().getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + public void commenceWhenRealmNameThenHasRealmName() { + this.entryPoint.setRealmName("Realm"); + + this.entryPoint.commence(this.exchange, new BadCredentialsException("")).block(); + + assertThat(getResponse().getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE)).isEqualTo("Bearer realm=\"Realm\""); + assertThat(getResponse().getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + public void commenceWhenOAuth2AuthenticationExceptionThenContainsErrorInformation() { + OAuth2Error oauthError = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST); + OAuth2AuthenticationException exception = new OAuth2AuthenticationException(oauthError); + + this.entryPoint.commence(this.exchange, exception).block(); + + assertThat(getResponse().getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE)).isEqualTo("Bearer error=\"invalid_request\""); + assertThat(getResponse().getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + public void commenceWhenOAuth2ErrorCompleteThenContainsErrorInformation() { + OAuth2Error oauthError = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, "Oops", "https://example.com"); + OAuth2AuthenticationException exception = new OAuth2AuthenticationException(oauthError); + + this.entryPoint.commence(this.exchange, exception).block(); + + assertThat(getResponse().getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE)).isEqualTo("Bearer error=\"invalid_request\", error_description=\"Oops\", error_uri=\"https://example.com\""); + assertThat(getResponse().getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + public void commenceWhenBearerTokenThenErrorInformation() { + OAuth2Error oauthError = new BearerTokenError(OAuth2ErrorCodes.INVALID_REQUEST, + HttpStatus.BAD_REQUEST, "Oops", "https://example.com"); + OAuth2AuthenticationException exception = new OAuth2AuthenticationException(oauthError); + + this.entryPoint.commence(this.exchange, exception).block(); + + assertThat(getResponse().getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE)).isEqualTo("Bearer error=\"invalid_request\", error_description=\"Oops\", error_uri=\"https://example.com\""); + assertThat(getResponse().getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + private MockServerHttpResponse getResponse() { + return this.exchange.getResponse(); + } +} From edca9f37f9f329fa577542d8c1ea0fc7a5d729c8 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 6 Jul 2018 13:56:19 -0500 Subject: [PATCH 189/226] Add ServerBearerTokenAuthenticationConverter Issue: gh-5605 --- ...verBearerTokenAuthenticationConverter.java | 107 ++++++++++++++ ...arerTokenAuthenticationConverterTests.java | 134 ++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverter.java create mode 100644 oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverterTests.java diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverter.java new file mode 100644 index 00000000000..1025dfb9c33 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverter.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource.web.server; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A strategy for resolving Bearer Tokens + * from the {@link ServerWebExchange}. + * + * @author Rob Winch + * @since 5.1 + * @see RFC 6750 Section 2: Authenticated Requests + */ +public class ServerBearerTokenAuthenticationConverter implements + Function> { + private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?[a-zA-Z0-9-._~+/]+)=*$"); + + private boolean allowUriQueryParameter = false; + + public Mono apply(ServerWebExchange exchange) { + return Mono.justOrEmpty(this.token(exchange.getRequest())) + .map(BearerTokenAuthenticationToken::new); + } + + private String token(ServerHttpRequest request) { + String authorizationHeaderToken = resolveFromAuthorizationHeader(request.getHeaders()); + String parameterToken = request.getQueryParams().getFirst("access_token"); + if (authorizationHeaderToken != null) { + if (parameterToken != null) { + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST, + HttpStatus.BAD_REQUEST, + "Found multiple bearer tokens in the request", + "https://tools.ietf.org/html/rfc6750#section-3.1"); + throw new OAuth2AuthenticationException(error); + } + return authorizationHeaderToken; + } + else if (parameterToken != null && isParameterTokenSupportedForRequest(request)) { + return parameterToken; + } + return null; + } + + /** + * Set if transport of access token using URI query parameter is supported. Defaults to {@code false}. + * + * The spec recommends against using this mechanism for sending bearer tokens, and even goes as far as + * stating that it was only included for completeness. + * + * @param allowUriQueryParameter if the URI query parameter is supported + */ + public void setAllowUriQueryParameter(boolean allowUriQueryParameter) { + this.allowUriQueryParameter = allowUriQueryParameter; + } + + private static String resolveFromAuthorizationHeader(HttpHeaders headers) { + String authorization = headers.getFirst(HttpHeaders.AUTHORIZATION); + if (StringUtils.hasText(authorization) && authorization.startsWith("Bearer")) { + Matcher matcher = authorizationPattern.matcher(authorization); + + if ( !matcher.matches() ) { + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_TOKEN, + HttpStatus.BAD_REQUEST, + "Bearer token is malformed", + "https://tools.ietf.org/html/rfc6750#section-3.1"); + throw new OAuth2AuthenticationException(error); + } + + return matcher.group("token"); + } + return null; + } + + private boolean isParameterTokenSupportedForRequest(ServerHttpRequest request) { + return this.allowUriQueryParameter && HttpMethod.GET.equals(request.getMethod()); + } +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverterTests.java new file mode 100644 index 00000000000..3326d17c9a1 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverterTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource.web.server; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; + +import java.util.Base64; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class ServerBearerTokenAuthenticationConverterTests { + private static final String TEST_TOKEN = "test-token"; + + private ServerBearerTokenAuthenticationConverter converter; + + @Before + public void setup() { + this.converter = new ServerBearerTokenAuthenticationConverter(); + } + + @Test + public void resolveWhenValidHeaderIsPresentThenTokenIsResolved() { + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + TEST_TOKEN); + + assertThat(convertToToken(request).getToken()).isEqualTo(TEST_TOKEN); + } + + @Test + public void resolveWhenNoHeaderIsPresentThenTokenIsNotResolved() { + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/"); + + assertThat(convertToToken(request)).isNull(); + } + + @Test + public void resolveWhenHeaderWithWrongSchemeIsPresentThenTokenIsNotResolved() { + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString("test:test".getBytes())); + + assertThat(convertToToken(request)).isNull(); + } + + @Test + public void resolveWhenHeaderWithMissingTokenIsPresentThenAuthenticationExceptionIsThrown() { + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .header(HttpHeaders.AUTHORIZATION, "Bearer "); + + assertThatCode(() -> convertToToken(request)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining(("Bearer token is malformed")); + } + + @Test + public void resolveWhenHeaderWithInvalidCharactersIsPresentThenAuthenticationExceptionIsThrown() { + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .header(HttpHeaders.AUTHORIZATION, "Bearer an\"invalid\"token"); + + assertThatCode(() -> convertToToken(request)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining(("Bearer token is malformed")); + } + + @Test + public void resolveWhenValidHeaderIsPresentTogetherWithQueryParameterThenAuthenticationExceptionIsThrown() { + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .queryParam("access_token", TEST_TOKEN) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + TEST_TOKEN); + + assertThatCode(() -> convertToToken(request)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("Found multiple bearer tokens in the request"); + } + + @Test + public void resolveWhenQueryParameterIsPresentAndSupportedThenTokenIsResolved() { + this.converter.setAllowUriQueryParameter(true); + + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .queryParam("access_token", TEST_TOKEN); + + assertThat(convertToToken(request).getToken()).isEqualTo(TEST_TOKEN); + } + + @Test + public void resolveWhenQueryParameterIsPresentAndNotSupportedThenTokenIsNotResolved() { + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .queryParam("access_token", TEST_TOKEN); + + assertThat(convertToToken(request)).isNull(); + } + + private BearerTokenAuthenticationToken convertToToken(MockServerHttpRequest.BaseBuilder request) { + return convertToToken(request.build()); + } + + private BearerTokenAuthenticationToken convertToToken(MockServerHttpRequest request) { + MockServerWebExchange exchange = MockServerWebExchange.from(request); + return this.converter.apply(exchange).cast(BearerTokenAuthenticationToken.class).block(); + } +} From 79f49dbd207908f11f656648ce48a8f6e04df31a Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 6 Jul 2018 16:10:20 -0500 Subject: [PATCH 190/226] Add OAuth2Spec Issue: gh-5605 --- .../config/web/server/ServerHttpSecurity.java | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 5559159fbb6..e754f431cca 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; +import java.security.interfaces.RSAPublicKey; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -54,6 +55,11 @@ import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectWebFilter; import org.springframework.security.oauth2.client.web.ServerOAuth2LoginAuthenticationTokenConverter; +import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager; +import org.springframework.security.oauth2.server.resource.web.server.BearerTokenServerAuthenticationEntryPoint; +import org.springframework.security.oauth2.server.resource.web.server.ServerBearerTokenAuthenticationConverter; import org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint; import org.springframework.security.web.server.MatcherSecurityWebFilterChain; import org.springframework.security.web.server.SecurityWebFilterChain; @@ -185,6 +191,8 @@ public class ServerHttpSecurity { private OAuth2LoginSpec oauth2Login; + private OAuth2Spec oauth2; + private LogoutSpec logout = new LogoutSpec(); private LoginPageSpec loginPage = new LoginPageSpec(); @@ -448,6 +456,140 @@ private ReactiveOAuth2AuthorizedClientService getAuthorizedClientService() { private OAuth2LoginSpec() {} } + /** + * Configures OAuth2 support. An example configuration is provided below: + * + *

      +	 *  @Bean
      +	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
      +	 *      http
      +	 *          // ...
      +	 *          .oauth2()
      +	 *              .resourceServer()
      +	 *                  .jwt()
      +	 *                      .jwkSeturi(jwkSetUri);
      +	 *      return http.build();
      +	 *  }
      +	 * 
      + * + * @return the {@link HttpBasicSpec} to customize + */ + public OAuth2Spec oauth2() { + if (this.oauth2 == null) { + this.oauth2 = new OAuth2Spec(); + } + return this.oauth2; + } + + /** + * Configures OAuth2 Support + * + * @since 5.1 + */ + public class OAuth2Spec { + private ResourceServerSpec resourceServer; + + public ResourceServerSpec resourceServer() { + if (this.resourceServer == null) { + this.resourceServer = new ResourceServerSpec(); + } + return this.resourceServer; + } + + /** + * Configures OAuth2 Resource Server Support + */ + public class ResourceServerSpec { + private JwtSpec jwt; + + public JwtSpec jwt() { + if (this.jwt == null) { + this.jwt = new JwtSpec(); + } + return this.jwt; + } + + protected void configure(ServerHttpSecurity http) { + if (this.jwt != null) { + this.jwt.configure(http); + } + } + + /** + * Configures JWT Resource Server Support + */ + public class JwtSpec { + private ReactiveJwtDecoder jwtDecoder; + + /** + * Configures the {@link ReactiveJwtDecoder} to use + * @param jwtDecoder the decoder to use + * @return the {@code JwtSpec} for additional configuration + */ + public JwtSpec jwtDecoder(ReactiveJwtDecoder jwtDecoder) { + this.jwtDecoder = jwtDecoder; + return this; + } + + /** + * Configures a {@link ReactiveJwtDecoder} that leverages the provided {@link RSAPublicKey} + * + * @param publicKey the public key to use. + * @return the {@code JwtSpec} for additional configuration + */ + public JwtSpec publicKey(RSAPublicKey publicKey) { + this.jwtDecoder = new NimbusReactiveJwtDecoder(publicKey); + return this; + } + + /** + * Configures a {@link ReactiveJwtDecoder} using + * JSON Web Key (JWK) URL + * @param jwkSetUri the URL to use. + * @return the {@code JwtSpec} for additional configuration + */ + public JwtSpec jwkSetUri(String jwkSetUri) { + this.jwtDecoder = new NimbusReactiveJwtDecoder(jwkSetUri); + return this; + } + + public ResourceServerSpec and() { + return ResourceServerSpec.this; + } + + protected void configure(ServerHttpSecurity http) { + BearerTokenServerAuthenticationEntryPoint entryPoint = new BearerTokenServerAuthenticationEntryPoint(); + JwtReactiveAuthenticationManager authenticationManager = new JwtReactiveAuthenticationManager( + this.jwtDecoder); + AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager); + oauth2.setAuthenticationConverter(new ServerBearerTokenAuthenticationConverter()); + oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint)); + http + .exceptionHandling() + .authenticationEntryPoint(entryPoint) + .and() + .addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION); + } + } + + public OAuth2Spec and() { + return OAuth2Spec.this; + } + } + + public ServerHttpSecurity and() { + return ServerHttpSecurity.this; + } + + protected void configure(ServerHttpSecurity http) { + if (this.resourceServer != null) { + this.resourceServer.configure(http); + } + } + + private OAuth2Spec() {} + } + /** * Configures HTTP Response Headers. The default headers are: * @@ -645,6 +787,9 @@ public SecurityWebFilterChain build() { if (this.oauth2Login != null) { this.oauth2Login.configure(this); } + if (this.oauth2 != null) { + this.oauth2.configure(this); + } this.loginPage.configure(this); if(this.logout != null) { this.logout.configure(this); From a4d1f3b0d5d9044746775bb73ed7710903897d0f Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 6 Jul 2018 16:10:47 -0500 Subject: [PATCH 191/226] Add Sample Issue: gh-5605 --- .../oauth2resourceserver-webflux/README.adoc | 169 ++++++++++++++++++ ...s-boot-oauth2resourceserver-webflux.gradle | 15 ++ .../OAuth2ResourceServerController.java | 33 ++++ .../src/main/java/sample/SecurityConfig.java | 78 ++++++++ ...ServerOAuth2ResourceServerApplication.java | 30 ++++ .../src/main/resources/application.yml | 11 ++ .../ServerOauth2ResourceApplicationTests.java | 78 ++++++++ 7 files changed, 414 insertions(+) create mode 100644 samples/boot/oauth2resourceserver-webflux/README.adoc create mode 100644 samples/boot/oauth2resourceserver-webflux/spring-security-samples-boot-oauth2resourceserver-webflux.gradle create mode 100644 samples/boot/oauth2resourceserver-webflux/src/main/java/sample/OAuth2ResourceServerController.java create mode 100644 samples/boot/oauth2resourceserver-webflux/src/main/java/sample/SecurityConfig.java create mode 100644 samples/boot/oauth2resourceserver-webflux/src/main/java/sample/ServerOAuth2ResourceServerApplication.java create mode 100644 samples/boot/oauth2resourceserver-webflux/src/main/resources/application.yml create mode 100644 samples/boot/oauth2resourceserver-webflux/src/test/java/sample/ServerOauth2ResourceApplicationTests.java diff --git a/samples/boot/oauth2resourceserver-webflux/README.adoc b/samples/boot/oauth2resourceserver-webflux/README.adoc new file mode 100644 index 00000000000..1960829a032 --- /dev/null +++ b/samples/boot/oauth2resourceserver-webflux/README.adoc @@ -0,0 +1,169 @@ += OAuth 2.0 Resource Server Sample + +This sample demonstrates integrations with a handful of different authorization servers. + +With it, you can run the integration tests or run the application as a stand-alone service to explore how you can +secure your own service with OAuth 2.0 Bearer Tokens using Spring Security. + +== 1. Running the tests + +To run the tests, do: + +```bash +../../../gradlew integrationTest +``` + +Or import the project into your IDE and run `OAuth2ResourceServerApplicationTests` from there. + +=== What is it doing? + +By default, the tests are pointing at a demonstration Okta instance. The test that performs a valid round trip does so +by querying the Okta Authorization Server using the client_credentials grant type to get a valid JWT token. Then, the test +makes a query to the Resource Server with that token. The Resource Server subsquently verifies with Okta and +authorizes the request, returning the phrase + +```bash +Hello, {subject}! +``` + +where subject is the value of the `sub` field in the JWT returned by the Authorization Server. + +== 2. Running the app + +To run as a stand-alone application, do: + +```bash +../../../gradlew bootRun +``` + +Or import the project into your IDE and run `OAuth2ResourceServerApplication` from there. + +Once it is up, you can retreive a valid JWT token from the authorization server, and then hit the endpoint: + +```bash +curl -H "Authorization: Bearer {token}" localhost:8081 +``` + +Which will respond with the phrase: + +```bash +Hello, {subject}! +``` + +where `subject` is the value of the `sub` field in the JWT returned by the Authorization Server. + +=== How do I obtain a valid JWT token? + +Getting a valid JWT token from an Authorization Server will vary, depending on your setup. However, it will typically +look something like this: + +```bash +curl --user {client id}:{client password} -d "grant_type=client_credentials" {auth server endpoint}/token +``` + +which will respond with a JSON payload containing the `access_token` among other things: + +```bash +{ "access_token" : "{the access token}", "token_type" : "Bearer", "expires_in" : "{an expiry}", "scope" : "{a list of scopes}" } +``` + +For example, the following can be used to hit the sample Okta endpoint for a valid JWT token: + +```bash +curl --user 0oaf5u5g4m6CW4x6z0h7:HR7edRoo3glhF06HTxonOKZvO4I2BWYcC_ocOHlv -d "grant_type=client_credentials" https://dev-805262.oktapreview.com/oauth2/default/v1/token +``` + +Which will give a response similar to this (formatting mine): + +```json +{ + "access_token": "eyJraWQiOiJFRjBFWDFFWHZGc1hGaDhuYkRGazNJN0hMUDBsZnJnc0JKMVdBWmkwRmI0IiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULmtQSUdfMEVMQmM3NVFMN3c4ZHBMVFRtNXZFVFd3d1R2dzJ3aXNISGRMbjgiLCJpc3MiOiJodHRwczovL2Rldi04MDUyNjIub2t0YXByZXZpZXcuY29tL29hdXRoMi9kZWZhdWx0IiwiYXVkIjoicmVzb3VyY2Utc2VydmVyIiwiaWF0IjoxNTI4ODYwMTkxLCJleHAiOjE1Mjg4NjM3OTEsImNpZCI6IjBvYWY1dTVnNG02Q1c0eDZ6MGg3Iiwic2NwIjpbIm9rIl0sInN1YiI6IjBvYWY1dTVnNG02Q1c0eDZ6MGg3In0.G_F9MQ3pqCy-YwfcNhryoPG5E1q4tQ7gV8OIDizR3QouUgrqT7MQsLQCTtGGLF2Fi0qq0Pr-V-wWa2MkyvcboEAhnfYi4rd3UmMrRTrNana6pVZjVWB_uj88-mZ57lFRnoYMCFbepmCxmY6D6p354H964xXWdtY7d6fw7F88DRDWMGQE0iQjMuUDg4izptVcK9db7uMonYTT1PFvOBQfwcn1zCeDVQgZFe7gjQA71CV9M6CIAXYDrpzp_hs95xco7Q3ncN3J7ZkCebLcUL6MdJS2nVuX6D6eC9PrtmCj06mb0-ydlzBSIUCPMaMQk9EhlEM_qK3d1iimCQnwo6KsIQ", + "token_type": "Bearer", + "expires_in": 3600, + "scope": "ok" +} +``` + +Then, using that access token: + +```bash +curl -H "Authorization: Bearer eyJraWQiOiJFRjBFWDFFWHZGc1hGaDhuYkRGazNJN0hMUDBsZnJnc0JKMVdBWmkwRmI0IiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULmtQSUdfMEVMQmM3NVFMN3c4ZHBMVFRtNXZFVFd3d1R2dzJ3aXNISGRMbjgiLCJpc3MiOiJodHRwczovL2Rldi04MDUyNjIub2t0YXByZXZpZXcuY29tL29hdXRoMi9kZWZhdWx0IiwiYXVkIjoicmVzb3VyY2Utc2VydmVyIiwiaWF0IjoxNTI4ODYwMTkxLCJleHAiOjE1Mjg4NjM3OTEsImNpZCI6IjBvYWY1dTVnNG02Q1c0eDZ6MGg3Iiwic2NwIjpbIm9rIl0sInN1YiI6IjBvYWY1dTVnNG02Q1c0eDZ6MGg3In0.G_F9MQ3pqCy-YwfcNhryoPG5E1q4tQ7gV8OIDizR3QouUgrqT7MQsLQCTtGGLF2Fi0qq0Pr-V-wWa2MkyvcboEAhnfYi4rd3UmMrRTrNana6pVZjVWB_uj88-mZ57lFRnoYMCFbepmCxmY6D6p354H964xXWdtY7d6fw7F88DRDWMGQE0iQjMuUDg4izptVcK9db7uMonYTT1PFvOBQfwcn1zCeDVQgZFe7gjQA71CV9M6CIAXYDrpzp_hs95xco7Q3ncN3J7ZkCebLcUL6MdJS2nVuX6D6eC9PrtmCj06mb0-ydlzBSIUCPMaMQk9EhlEM_qK3d1iimCQnwo6KsIQ" \ + localhost:8081 +``` + +I get: + +```bash +Hello, 0oaf5u5g4m6CW4x6z0h7! +``` + +== 3. Testing against other Authorization Servers + +The sample is already prepared to demonstrate integrations with a handful of other Authorization Servers. Do exercise +one, simply uncomment two commented out sections, both in the application.yml file: + +```yaml +spring: + security: + oauth2: + resourceserver: + issuer: +``` + +First, find the above section in the application.yml. Beneath it, you will see sections for each Authorization Server +already prepared with the one for Okta commented out: + +```yaml +# master: #keycloak +# issuer: http://localhost:8080/auth/realms/master +# jwk-set-uri: http://localhost:8080/auth/realms/master/protocol/openid-connect/certs + okta: + issuer: https://dev-805262.oktapreview.com/oauth2/default + jwk-set-uri: https://dev-805262.oktapreview.com/oauth2/default/v1/keys +``` + +Comment out the `okta` section and uncomment the desired section. + +Second, find the following section, which the sample needs in order to retreive a valid token from the Authorization +Server: + +```yaml +# ### keycloak +# token-uri: http://localhost:8080/auth/realms/master/protocol/openid-connect/token +# token-body: +# grant_type: client_credentials +# client-id: service +# client-password: 9114712b-be55-4dab-b270-04734abda1c4 +# container: +# config-file-name: keycloak.config +# docker-file-name: keycloak.docker + ### okta + token-uri: https://dev-805262.oktapreview.com/oauth2/default/v1/token + token-body: + grant_type: client_credentials + client-id: 0oaf5u5g4m6CW4x6z0h7 + client-password: HR7edRoo3glhF06HTxonOKZvO4I2BWYcC_ocOHlv +``` + +Comment out the `okta` section and uncomment the desired section. + +=== How can I test with my own Authorization Server instance? + +To test with your own Okta or other Authorization Server instance, simply provide the following information: + +```yaml +spring.security.oauth2.resourceserver.issuer.name.uri: the issuer uri +spring.security.oauth2.resourceserver.issuer.name.jwk-set-uri: the jwk key uri +``` + +And indicate, using the sample.provider properties, how the sample should generate a valid JWT token: + +```yaml +sample.provider.token-uri: the token endpoint +sample.provider.token-body.grant_type: the grant to use +sample.provider.token-body.another_property: another_value +sample.provider.client-id: the client id +sample.provider.client-password: the client password, only required for confidential clients +``` + +You can provide values for any OAuth 2.0-compliant Authorization Server. diff --git a/samples/boot/oauth2resourceserver-webflux/spring-security-samples-boot-oauth2resourceserver-webflux.gradle b/samples/boot/oauth2resourceserver-webflux/spring-security-samples-boot-oauth2resourceserver-webflux.gradle new file mode 100644 index 00000000000..cf0d0d6202b --- /dev/null +++ b/samples/boot/oauth2resourceserver-webflux/spring-security-samples-boot-oauth2resourceserver-webflux.gradle @@ -0,0 +1,15 @@ +apply plugin: 'io.spring.convention.spring-sample-boot' + +dependencies { + compile project(':spring-security-config') + compile project(':spring-security-oauth2-jose') + compile project(':spring-security-oauth2-client') + compile project(':spring-security-oauth2-resource-server') + + compile 'org.springframework.boot:spring-boot-starter-webflux' + + testCompile project(':spring-security-test') + testCompile 'org.springframework.boot:spring-boot-starter-test' + + testCompile 'com.squareup.okhttp3:mockwebserver' +} diff --git a/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/OAuth2ResourceServerController.java b/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/OAuth2ResourceServerController.java new file mode 100644 index 00000000000..7024f39f909 --- /dev/null +++ b/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/OAuth2ResourceServerController.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Josh Cummings + */ +@RestController +public class OAuth2ResourceServerController { + + @GetMapping("/") + public String index(@AuthenticationPrincipal Jwt jwt) { + return String.format("Hello, %s!", jwt.getSubject()); + } +} diff --git a/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/SecurityConfig.java b/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/SecurityConfig.java new file mode 100644 index 00000000000..f8ce3b3de0f --- /dev/null +++ b/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/SecurityConfig.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; + +/** + * @author Rob Winch + * @since 5.1 + */ +@EnableWebFluxSecurity +public class SecurityConfig { + private static final String JWK_SET_URI_PROP = "sample.jwk-set-uri"; + + @Bean + @ConditionalOnProperty(SecurityConfig.JWK_SET_URI_PROP) + SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, @Value("${sample.jwk-set-uri}") String jwkSetUri) throws Exception { + http + .authorizeExchange() + .anyExchange().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt() + .jwkSetUri(jwkSetUri); + return http.build(); + } + + @Bean + @ConditionalOnProperty(matchIfMissing = true, name = SecurityConfig.JWK_SET_URI_PROP) + SecurityWebFilterChain springSecurityFilterChainWithJwkSetUri(ServerHttpSecurity http) throws Exception { + http + .authorizeExchange() + .anyExchange().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt() + .publicKey(publicKey()); + return http.build(); + } + + private RSAPublicKey publicKey() + throws NoSuchAlgorithmException, InvalidKeySpecException { + String modulus = "21301844740604653578042500449274548398885553541276518010855123403873267398204269788903348794459771698460057967144865511347818036788093430902099139850950702438493841101242291810362822203615900335437741117578551216365305797763072813300890517195382010982402736091906390325356368590938709762826676219814134995844721978269999358693499223506089799649124650473473086179730568497569430199548044603025675755473148289824338392487941265829853008714754732175256733090080910187256164496297277607612684421019218165083081805792835073696987599616469568512360535527950859101589894643349122454163864596223876828010734083744763850611111"; + String exponent = "65537"; + + RSAPublicKeySpec spec = new RSAPublicKeySpec(new BigInteger(modulus), new BigInteger(exponent)); + KeyFactory factory = KeyFactory.getInstance("RSA"); + return (RSAPublicKey) factory.generatePublic(spec); + } +} diff --git a/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/ServerOAuth2ResourceServerApplication.java b/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/ServerOAuth2ResourceServerApplication.java new file mode 100644 index 00000000000..3061103fbcf --- /dev/null +++ b/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/ServerOAuth2ResourceServerApplication.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Rob Winch + */ +@SpringBootApplication +public class ServerOAuth2ResourceServerApplication { + + public static void main(String[] args) { + SpringApplication.run(ServerOAuth2ResourceServerApplication.class, args); + } +} diff --git a/samples/boot/oauth2resourceserver-webflux/src/main/resources/application.yml b/samples/boot/oauth2resourceserver-webflux/src/main/resources/application.yml new file mode 100644 index 00000000000..cf18ae712d2 --- /dev/null +++ b/samples/boot/oauth2resourceserver-webflux/src/main/resources/application.yml @@ -0,0 +1,11 @@ +logging: + level: + root: INFO + org.springframework.web: INFO + org.springframework.security: INFO +# org.springframework.boot.autoconfigure: DEBUG + +sample: +# By default this sample uses a hard coded public key in SecurityConfig +# To use a JWK Set URI, uncomment and change the value below +# jwk-set-uri: https://example.com/oauth2/default/v1/keys diff --git a/samples/boot/oauth2resourceserver-webflux/src/test/java/sample/ServerOauth2ResourceApplicationTests.java b/samples/boot/oauth2resourceserver-webflux/src/test/java/sample/ServerOauth2ResourceApplicationTests.java new file mode 100644 index 00000000000..7c8dce252e9 --- /dev/null +++ b/samples/boot/oauth2resourceserver-webflux/src/test/java/sample/ServerOauth2ResourceApplicationTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.security.web.http.SecurityHeaders.bearerToken; + +/** + * @author Rob Winch + * @since 5.1 + */ +@SpringBootTest +@AutoConfigureWebTestClient +@RunWith(SpringJUnit4ClassRunner.class) +public class ServerOauth2ResourceApplicationTests { + @Autowired + private WebTestClient rest; + + @Test + public void getWhenValidTokenThenIsOk() { + String token = "eyJhbGciOiJSUzI1NiJ9.eyJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6MzEwNjMyODEzMSwianRpIjoiOGY5ZjFiYzItOWVlMi00NTJkLThhMGEtODg3YmE4YmViYjYzIn0.CM_KulSsIrNXW1x6NFeN5VwKQiIW-LIAScJzakRFDox8Ql7o4WOb0ubY3CjWYnglwqYzBvH9McCFqVrUtzdfODY5tyEEJSxWndIGExOi2osrwRPsY3AGzNa23GMfC9I03BFP1IFCq4ZfL-L6yVcIjLke-rA40UG-r-oA7r-N_zsLc5poO7Azf29IQgQF0GSRp4AKQprYHF5Q-Nz9XkILMDz9CwPQ9cbdLCC9smvaGmEAjMUr-C1QgM-_ulb42gWtRDLorW_eArg8g-fmIP0_w82eNWCBjLTy-WaDMACnDVrrUVsUMCqx6jS6h8_uejKly2NFuhyueIHZTTySqCZoTA"; + this.rest.get().uri("/") + .headers(bearerToken(token)) + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("Hello, null!"); + } + + @Test + public void getWhenNoTokenThenIsUnauthorized() { + this.rest.get().uri("/") + .exchange() + .expectStatus().isUnauthorized() + .expectHeader().valueEquals(HttpHeaders.WWW_AUTHENTICATE, "Bearer"); + } + + @Test + public void getWhenNone() { + String token = "ew0KICAiYWxnIjogIm5vbmUiLA0KICAidHlwIjogIkpXVCINCn0.ew0KICAic3ViIjogIjEyMzQ1Njc4OTAiLA0KICAibmFtZSI6ICJKb2huIERvZSIsDQogICJpYXQiOiAxNTE2MjM5MDIyDQp9."; + this.rest.get().uri("/") + .headers(bearerToken(token)) + .exchange() + .expectStatus().isUnauthorized() + .expectHeader().valueEquals(HttpHeaders.WWW_AUTHENTICATE, "Bearer error=\"invalid_token\", error_description=\"Unsupported algorithm of none\", error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\""); + } + + @Test + public void getWhenInvalidToken() { + String token = "a"; + this.rest.get().uri("/") + .headers(bearerToken(token)) + .exchange() + .expectStatus().isUnauthorized() + .expectHeader().valueEquals(HttpHeaders.WWW_AUTHENTICATE, "Bearer error=\"invalid_token\", error_description=\"An error occurred while attempting to decode the Jwt: Invalid JWT serialization: Missing dot delimiter(s)\", error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\""); + } +} From 2d762707afc24e037f094de9fc339acd868b7f1f Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 30 Jul 2018 12:02:42 -0500 Subject: [PATCH 192/226] WebFlux Handles Undefined State Parameter Currently if a state exists, but an undefined state parameter is provided a NullPointerException occurs. This commit handles the null value. Fixes: gh-5599 --- ...ReactiveAuthorizationRequestRepository.java | 6 +++++- ...iveAuthorizationRequestRepositoryTests.java | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/WebSessionOAuth2ReactiveAuthorizationRequestRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/WebSessionOAuth2ReactiveAuthorizationRequestRepository.java index 2783c5968f3..aa8b4302a63 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/WebSessionOAuth2ReactiveAuthorizationRequestRepository.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/WebSessionOAuth2ReactiveAuthorizationRequestRepository.java @@ -84,7 +84,11 @@ public Mono removeAuthorizationRequest( if (stateToAuthzRequest.isEmpty()) { sessionAttrs.remove(this.sessionAttributeName); } - sink.next(removedValue); + if (removedValue == null) { + sink.complete(); + } else { + sink.next(removedValue); + } }); } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/WebSessionOAuth2ReactiveAuthorizationRequestRepositoryTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/WebSessionOAuth2ReactiveAuthorizationRequestRepositoryTests.java index c4fc7adaedb..50d18bf6dbe 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/WebSessionOAuth2ReactiveAuthorizationRequestRepositoryTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/WebSessionOAuth2ReactiveAuthorizationRequestRepositoryTests.java @@ -24,6 +24,7 @@ import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.http.server.reactive.MockServerHttpResponse; +import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; @@ -179,6 +180,23 @@ public void removeAuthorizationRequestWhenPresentThenFoundAndRemoved() { .verifyComplete(); } + // gh-5599 + @Test + public void removeAuthorizationRequestWhenStateMissingThenNoErrors() { + MockServerHttpRequest otherState = MockServerHttpRequest.get("/") + .queryParam(OAuth2ParameterNames.STATE, "other") + .build(); + ServerWebExchange otherStateExchange = this.exchange.mutate() + .request(otherState) + .build(); + Mono saveAndRemove = this.repository + .saveAuthorizationRequest(this.authorizationRequest, this.exchange) + .then(this.repository.removeAuthorizationRequest(otherStateExchange)); + + StepVerifier.create(saveAndRemove) + .verifyComplete(); + } + @Test public void removeAuthorizationRequestWhenMultipleThenOnlyOneRemoved() { String oldState = "state0"; From 03a41178af8a1a0d531bee551d71702123279863 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 30 Jul 2018 12:07:51 -0500 Subject: [PATCH 193/226] Add OAuth2LoginSpec.and() Fixes: gh-5609 --- .../security/config/web/server/ServerHttpSecurity.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index e754f431cca..dde83846af8 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -377,6 +377,15 @@ public OAuth2LoginSpec authorizedClientService(ReactiveOAuth2AuthorizedClientSer return this; } + /** + * Allows method chaining to continue configuring the {@link ServerHttpSecurity} + * @return the {@link ServerHttpSecurity} to continue configuring + */ + public ServerHttpSecurity and() { + return ServerHttpSecurity.this; + } + + protected void configure(ServerHttpSecurity http) { ReactiveClientRegistrationRepository clientRegistrationRepository = getClientRegistrationRepository(); ReactiveOAuth2AuthorizedClientService authorizedClientService = getAuthorizedClientService(); From cd1657b290b1f560f1204d5f03fc78c00304fcf5 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 30 Jul 2018 12:15:53 -0500 Subject: [PATCH 194/226] Fix Imports Issue: gh-5599 --- ...SessionOAuth2ReactiveAuthorizationRequestRepositoryTests.java | 1 - 1 file changed, 1 deletion(-) diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/WebSessionOAuth2ReactiveAuthorizationRequestRepositoryTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/WebSessionOAuth2ReactiveAuthorizationRequestRepositoryTests.java index 50d18bf6dbe..142fc0a8da1 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/WebSessionOAuth2ReactiveAuthorizationRequestRepositoryTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/WebSessionOAuth2ReactiveAuthorizationRequestRepositoryTests.java @@ -24,7 +24,6 @@ import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.http.server.reactive.MockServerHttpResponse; -import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; From c67e2a1fb2197eac573700c42764698db558041f Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 30 Jul 2018 12:38:36 -0400 Subject: [PATCH 195/226] Default to server_error when OAuth2Error.errorCode is null Fixes gh-5594 --- ...sAuthorizationCodeTokenResponseClient.java | 12 ++++++++-- ...orizationCodeTokenResponseClientTests.java | 22 +++++++++++++++++++ ...orizationCodeTokenResponseClientTests.java | 11 ++++++++++ ...Auth2AccessTokenResponseBodyExtractor.java | 15 ++++++++----- 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClient.java index 1f1b4ee4c80..7957a2dd1b1 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClient.java @@ -37,6 +37,7 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.util.CollectionUtils; @@ -111,8 +112,15 @@ public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRe if (!tokenResponse.indicatesSuccess()) { TokenErrorResponse tokenErrorResponse = (TokenErrorResponse) tokenResponse; ErrorObject errorObject = tokenErrorResponse.getErrorObject(); - OAuth2Error oauth2Error = new OAuth2Error(errorObject.getCode(), errorObject.getDescription(), - (errorObject.getURI() != null ? errorObject.getURI().toString() : null)); + OAuth2Error oauth2Error; + if (errorObject == null) { + oauth2Error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR); + } else { + oauth2Error = new OAuth2Error( + errorObject.getCode() != null ? errorObject.getCode() : OAuth2ErrorCodes.SERVER_ERROR, + errorObject.getDescription(), + errorObject.getURI() != null ? errorObject.getURI().toString() : null); + } throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClientTests.java index 00d3f8b77be..807fbb3c959 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClientTests.java @@ -214,6 +214,28 @@ public void getTokenResponseWhenErrorResponseThenThrowOAuth2AuthenticationExcept } } + // gh-5594 + @Test + public void getTokenResponseWhenServerErrorResponseThenThrowOAuth2AuthenticationException() throws Exception { + this.exception.expect(OAuth2AuthenticationException.class); + this.exception.expectMessage(containsString("server_error")); + + MockWebServer server = new MockWebServer(); + + server.enqueue(new MockResponse().setResponseCode(500)); + server.start(); + + String tokenUri = server.url("/oauth2/token").toString(); + when(this.providerDetails.getTokenUri()).thenReturn(tokenUri); + + try { + this.tokenResponseClient.getTokenResponse( + new OAuth2AuthorizationCodeGrantRequest(this.clientRegistration, this.authorizationExchange)); + } finally { + server.shutdown(); + } + } + @Test public void getTokenResponseWhenSuccessResponseAndNotBearerTokenTypeThenThrowOAuth2AuthenticationException() throws Exception { this.exception.expect(OAuth2AuthenticationException.class); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java index 49de6e5c790..5e408841873 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java @@ -187,6 +187,17 @@ public void getTokenResponseWhenErrorResponseThenThrowOAuth2AuthenticationExcept .hasMessageContaining("unauthorized_client"); } + // gh-5594 + @Test + public void getTokenResponseWhenServerErrorResponseThenThrowOAuth2AuthenticationException() throws Exception { + String accessTokenErrorResponse = "{}"; + this.server.enqueue(jsonResponse(accessTokenErrorResponse).setResponseCode(HttpStatus.INTERNAL_SERVER_ERROR.value())); + + assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(authorizationCodeGrantRequest()).block()) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("server_error"); + } + @Test public void getTokenResponseWhenSuccessResponseAndNotBearerTokenTypeThenThrowOAuth2AuthenticationException() throws Exception { String accessTokenSuccessResponse = "{\n" + diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2AccessTokenResponseBodyExtractor.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2AccessTokenResponseBodyExtractor.java index a14287eb120..188f2bd0d54 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2AccessTokenResponseBodyExtractor.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2AccessTokenResponseBodyExtractor.java @@ -28,6 +28,7 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.web.reactive.function.BodyExtractor; import org.springframework.web.reactive.function.BodyExtractors; @@ -80,11 +81,15 @@ private static Mono oauth2AccessTokenResponse(TokenResponse } TokenErrorResponse tokenErrorResponse = (TokenErrorResponse) tokenResponse; ErrorObject errorObject = tokenErrorResponse.getErrorObject(); - OAuth2Error oauth2Error = new OAuth2Error(errorObject.getCode(), - errorObject.getDescription(), (errorObject.getURI() != null ? - errorObject.getURI().toString() : - null)); - + OAuth2Error oauth2Error; + if (errorObject == null) { + oauth2Error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR); + } else { + oauth2Error = new OAuth2Error( + errorObject.getCode() != null ? errorObject.getCode() : OAuth2ErrorCodes.SERVER_ERROR, + errorObject.getDescription(), + errorObject.getURI() != null ? errorObject.getURI().toString() : null); + } return Mono.error(new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString())); } From eb6ebd3cebf3298057d1521ef5d885e5718f2b9a Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 30 Jul 2018 15:31:41 -0400 Subject: [PATCH 196/226] ClaimAccessor.getClaimAsString() checks null claim value Fixes gh-5608 --- .../security/oauth2/core/ClaimAccessor.java | 12 ++++++++---- .../security/oauth2/core/ClaimAccessorTests.java | 9 +++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClaimAccessor.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClaimAccessor.java index f9bc43b2911..e4505a4a907 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClaimAccessor.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClaimAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,13 +53,17 @@ default Boolean containsClaim(String claim) { } /** - * Returns the claim value as a {@code String} or {@code null} if it does not exist. + * Returns the claim value as a {@code String} or {@code null} if it does not exist or is equal to {@code null}. * * @param claim the name of the claim - * @return the claim value or {@code null} if it does not exist + * @return the claim value or {@code null} if it does not exist or is equal to {@code null} */ default String getClaimAsString(String claim) { - return (this.containsClaim(claim) ? this.getClaims().get(claim).toString() : null); + if (!this.containsClaim(claim)) { + return null; + } + Object claimValue = this.getClaims().get(claim); + return (claimValue != null ? claimValue.toString() : null); } /** diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ClaimAccessorTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ClaimAccessorTests.java index 4a7f0efe02d..2db9e7df975 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ClaimAccessorTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ClaimAccessorTests.java @@ -92,4 +92,13 @@ public void getClaimAsInstantWhenDoubleTypeSecondsThenReturnInstant() { assertThat(this.claimAccessor.getClaimAsInstant(claimName)).isBetween( expectedClaimValue.minusSeconds(1), expectedClaimValue.plusSeconds(1)); } + + // gh-5608 + @Test + public void getClaimAsStringWhenValueIsNullThenReturnNull() { + String claimName = "claim-with-null-value"; + this.claims.put(claimName, null); + + assertThat(this.claimAccessor.getClaimAsString(claimName)).isEqualTo(null); + } } From c9af1c1c3a22c6dfdde526e97063f766f6cb4c97 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 30 Jul 2018 14:27:18 -0500 Subject: [PATCH 197/226] Update to Spring Boot 2.1.0.M1 Fixes: gh-5613 --- gradle.properties | 2 +- ...security-samples-boot-authcodegrant.gradle | 2 +- ...ReactiveOAuth2ClientAutoConfiguration.java | 43 -------------- ...ntRegistrationRepositoryConfiguration.java | 57 ------------------- ...eactiveOAuth2WebSecurityConfiguration.java | 46 --------------- .../security/oauth2/client/package-info.java | 20 ------- 6 files changed, 2 insertions(+), 168 deletions(-) delete mode 100644 samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientAutoConfiguration.java delete mode 100644 samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientRegistrationRepositoryConfiguration.java delete mode 100644 samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2WebSecurityConfiguration.java delete mode 100644 samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/package-info.java diff --git a/gradle.properties b/gradle.properties index 84ac09f34e0..93f1db4d3c4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ gaeVersion=1.9.64 -springBootVersion=2.0.3.RELEASE +springBootVersion=2.1.0.M1 version=5.1.0.BUILD-SNAPSHOT diff --git a/samples/boot/authcodegrant/spring-security-samples-boot-authcodegrant.gradle b/samples/boot/authcodegrant/spring-security-samples-boot-authcodegrant.gradle index 572fe40331c..dc46d8f456b 100644 --- a/samples/boot/authcodegrant/spring-security-samples-boot-authcodegrant.gradle +++ b/samples/boot/authcodegrant/spring-security-samples-boot-authcodegrant.gradle @@ -9,7 +9,7 @@ dependencies { compile 'org.springframework.boot:spring-boot-starter-thymeleaf' compile 'org.springframework.boot:spring-boot-starter-web' compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4' - compile 'io.projectreactor.ipc:reactor-netty' + compile 'io.projectreactor.netty:reactor-netty' testCompile project(':spring-security-test') testCompile 'org.springframework.boot:spring-boot-starter-test' diff --git a/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientAutoConfiguration.java b/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientAutoConfiguration.java deleted file mode 100644 index dd3d788fe94..00000000000 --- a/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientAutoConfiguration.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2012-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.security.oauth2.client; - -import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; -import org.springframework.security.oauth2.client.registration.ClientRegistration; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for OAuth client support. - * - * @author Madhura Bhave - * @author Phillip Webb - * @since 2.0.0 - */ -@Configuration -@AutoConfigureBefore(name = "org.springframework.boot.autoconfigure.security.reactive.WebFluxSecurityConfiguration") -@ConditionalOnClass({ EnableWebFluxSecurity.class, ClientRegistration.class }) -@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) -@Import({ ReactiveOAuth2ClientRegistrationRepositoryConfiguration.class, - ReactiveOAuth2WebSecurityConfiguration.class }) -public class ReactiveOAuth2ClientAutoConfiguration { - -} diff --git a/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientRegistrationRepositoryConfiguration.java b/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientRegistrationRepositoryConfiguration.java deleted file mode 100644 index abd5e767a18..00000000000 --- a/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientRegistrationRepositoryConfiguration.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2012-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.security.oauth2.client; - -import java.util.ArrayList; -import java.util.List; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; -import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; - -/** - * {@link Configuration} used to map {@link OAuth2ClientProperties} to client - * registrations. - * - * @author Madhura Bhave - * @author Phillip Webb - */ -@Configuration -@EnableConfigurationProperties(OAuth2ClientProperties.class) -@Conditional(OAuth2ClientRegistrationRepositoryConfiguration.ClientsConfiguredCondition.class) -class ReactiveOAuth2ClientRegistrationRepositoryConfiguration { - - private final OAuth2ClientProperties properties; - - ReactiveOAuth2ClientRegistrationRepositoryConfiguration(OAuth2ClientProperties properties) { - this.properties = properties; - } - - @Bean - @ConditionalOnMissingBean(ReactiveClientRegistrationRepository.class) - public InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() { - List registrations = new ArrayList<>( - OAuth2ClientPropertiesRegistrationAdapter - .getClientRegistrations(this.properties).values()); - return new InMemoryReactiveClientRegistrationRepository(registrations); - } -} diff --git a/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2WebSecurityConfiguration.java b/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2WebSecurityConfiguration.java deleted file mode 100644 index 13234672d69..00000000000 --- a/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2WebSecurityConfiguration.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2012-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.security.oauth2.client; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; - -/** - * {@link WebSecurityConfigurerAdapter} to add OAuth client support. - * - * @author Madhura Bhave - * @author Phillip Webb - * @since 2.0.0 - */ -@Configuration -@ConditionalOnBean(ReactiveClientRegistrationRepository.class) -class ReactiveOAuth2WebSecurityConfiguration { - - @Bean - @ConditionalOnMissingBean - public ReactiveOAuth2AuthorizedClientService authorizedClientService( - ReactiveClientRegistrationRepository clientRegistrationRepository) { - return new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository); - } - -} diff --git a/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/package-info.java b/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/package-info.java deleted file mode 100644 index 6cefe5bb02b..00000000000 --- a/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2012-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Auto-configuration for Spring Security's Reactive OAuth 2 client. This will be merged into Spring Boot 2.1. - */ -package org.springframework.boot.autoconfigure.security.oauth2.client; From 04a0610cc390980292e8de8fa034e4aeaef25d35 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 30 Jul 2018 14:28:41 -0500 Subject: [PATCH 198/226] Remove SecurityHeaders We no longer need this since Spring Framework now provides HttpHeaders.setBearerAuth Issue: gh-5612 --- .../DefaultReactiveOAuth2UserService.java | 3 +- ...uthorizedClientExchangeFilterFunction.java | 3 +- ...uthorizedClientExchangeFilterFunction.java | 3 +- .../ServerOauth2ResourceApplicationTests.java | 8 ++-- .../web/http/SecurityHeadersTests.java | 42 ------------------- 5 files changed, 6 insertions(+), 53 deletions(-) delete mode 100644 web/src/test/groovy/org/springframework/security/web/http/SecurityHeadersTests.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java index 533400c391d..83d35f267f7 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java @@ -16,7 +16,6 @@ package org.springframework.security.oauth2.client.userinfo; -import static org.springframework.security.web.http.SecurityHeaders.bearerToken; import java.net.UnknownHostException; import java.util.HashSet; @@ -114,7 +113,7 @@ public Mono loadUser(OAuth2UserRequest userRequest) requestHeadersSpec = this.webClient.get() .uri(userInfoUri) .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) - .headers(bearerToken(userRequest.getAccessToken().getTokenValue())); + .headers(headers -> headers.setBearerAuth(userRequest.getAccessToken().getTokenValue())); } Mono> userAttributes = requestHeadersSpec .retrieve() diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java index 26b0fbbabe8..b8edf6f1336 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java @@ -48,7 +48,6 @@ import java.util.function.Consumer; import static org.springframework.security.oauth2.core.web.reactive.function.OAuth2BodyExtractors.oauth2AccessTokenResponse; -import static org.springframework.security.web.http.SecurityHeaders.bearerToken; /** * Provides an easy mechanism for using an {@link OAuth2AuthorizedClient} to make OAuth2 requests by including the @@ -190,7 +189,7 @@ private boolean shouldRefresh(OAuth2AuthorizedClient authorizedClient) { private ClientRequest bearer(ClientRequest request, OAuth2AuthorizedClient authorizedClient) { return ClientRequest.from(request) - .headers(bearerToken(authorizedClient.getAccessToken().getTokenValue())) + .headers(headers -> headers.setBearerAuth(authorizedClient.getAccessToken().getTokenValue())) .build(); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java index bc10e7f2ed5..60969a5bb0a 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java @@ -56,7 +56,6 @@ import java.util.function.Consumer; import static org.springframework.security.oauth2.core.web.reactive.function.OAuth2BodyExtractors.oauth2AccessTokenResponse; -import static org.springframework.security.web.http.SecurityHeaders.bearerToken; /** * Provides an easy mechanism for using an {@link OAuth2AuthorizedClient} to make OAuth2 requests by including the @@ -338,7 +337,7 @@ private boolean shouldRefresh(OAuth2AuthorizedClient authorizedClient) { private ClientRequest bearer(ClientRequest request, OAuth2AuthorizedClient authorizedClient) { return ClientRequest.from(request) - .headers(bearerToken(authorizedClient.getAccessToken().getTokenValue())) + .headers(headers -> headers.setBearerAuth(authorizedClient.getAccessToken().getTokenValue())) .build(); } diff --git a/samples/boot/oauth2resourceserver-webflux/src/test/java/sample/ServerOauth2ResourceApplicationTests.java b/samples/boot/oauth2resourceserver-webflux/src/test/java/sample/ServerOauth2ResourceApplicationTests.java index 7c8dce252e9..a47be4787e6 100644 --- a/samples/boot/oauth2resourceserver-webflux/src/test/java/sample/ServerOauth2ResourceApplicationTests.java +++ b/samples/boot/oauth2resourceserver-webflux/src/test/java/sample/ServerOauth2ResourceApplicationTests.java @@ -25,8 +25,6 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.web.reactive.server.WebTestClient; -import static org.springframework.security.web.http.SecurityHeaders.bearerToken; - /** * @author Rob Winch * @since 5.1 @@ -42,7 +40,7 @@ public class ServerOauth2ResourceApplicationTests { public void getWhenValidTokenThenIsOk() { String token = "eyJhbGciOiJSUzI1NiJ9.eyJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6MzEwNjMyODEzMSwianRpIjoiOGY5ZjFiYzItOWVlMi00NTJkLThhMGEtODg3YmE4YmViYjYzIn0.CM_KulSsIrNXW1x6NFeN5VwKQiIW-LIAScJzakRFDox8Ql7o4WOb0ubY3CjWYnglwqYzBvH9McCFqVrUtzdfODY5tyEEJSxWndIGExOi2osrwRPsY3AGzNa23GMfC9I03BFP1IFCq4ZfL-L6yVcIjLke-rA40UG-r-oA7r-N_zsLc5poO7Azf29IQgQF0GSRp4AKQprYHF5Q-Nz9XkILMDz9CwPQ9cbdLCC9smvaGmEAjMUr-C1QgM-_ulb42gWtRDLorW_eArg8g-fmIP0_w82eNWCBjLTy-WaDMACnDVrrUVsUMCqx6jS6h8_uejKly2NFuhyueIHZTTySqCZoTA"; this.rest.get().uri("/") - .headers(bearerToken(token)) + .headers(headers -> headers.setBearerAuth(token)) .exchange() .expectStatus().isOk() .expectBody(String.class).isEqualTo("Hello, null!"); @@ -60,7 +58,7 @@ public void getWhenNoTokenThenIsUnauthorized() { public void getWhenNone() { String token = "ew0KICAiYWxnIjogIm5vbmUiLA0KICAidHlwIjogIkpXVCINCn0.ew0KICAic3ViIjogIjEyMzQ1Njc4OTAiLA0KICAibmFtZSI6ICJKb2huIERvZSIsDQogICJpYXQiOiAxNTE2MjM5MDIyDQp9."; this.rest.get().uri("/") - .headers(bearerToken(token)) + .headers(headers -> headers.setBearerAuth(token)) .exchange() .expectStatus().isUnauthorized() .expectHeader().valueEquals(HttpHeaders.WWW_AUTHENTICATE, "Bearer error=\"invalid_token\", error_description=\"Unsupported algorithm of none\", error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\""); @@ -70,7 +68,7 @@ public void getWhenNone() { public void getWhenInvalidToken() { String token = "a"; this.rest.get().uri("/") - .headers(bearerToken(token)) + .headers(headers -> headers.setBearerAuth(token)) .exchange() .expectStatus().isUnauthorized() .expectHeader().valueEquals(HttpHeaders.WWW_AUTHENTICATE, "Bearer error=\"invalid_token\", error_description=\"An error occurred while attempting to decode the Jwt: Invalid JWT serialization: Missing dot delimiter(s)\", error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\""); diff --git a/web/src/test/groovy/org/springframework/security/web/http/SecurityHeadersTests.java b/web/src/test/groovy/org/springframework/security/web/http/SecurityHeadersTests.java deleted file mode 100644 index 164a4ba39f6..00000000000 --- a/web/src/test/groovy/org/springframework/security/web/http/SecurityHeadersTests.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2002-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.web.http; - -import org.junit.Test; - -import static org.assertj.core.api.Assertions.*; - -/** - * @author Rob Winch - * @since 5.1 - */ -public class SecurityHeadersTests { - - @Test - public void bearerTokenWhenNullThenIllegalArgumentException() { - String bearerTokenValue = null; - assertThatThrownBy(() -> SecurityHeaders.bearerToken(bearerTokenValue)) - .isInstanceOf(IllegalArgumentException.class); - } - - @Test - public void bearerTokenWhenEmptyStringThenIllegalArgumentException() { - assertThatThrownBy(() -> SecurityHeaders.bearerToken("")) - .isInstanceOf(IllegalArgumentException.class); - } - -} From c3c196f15c73e2bdc9b645d2cae1cc68dc754f12 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 30 Jul 2018 14:45:14 -0500 Subject: [PATCH 199/226] Remove ExchangeFilterFunctions Issue: gh-5612 --- .../reactive/EnableWebFluxSecurityTests.java | 11 +++------ .../server/ExceptionHandlingSpecTests.java | 8 ++----- .../web/server/ServerHttpSecurityTests.java | 8 ++----- ...eAuthorizationCodeTokenResponseClient.java | 5 +--- .../HelloWebfluxMethodApplicationITests.java | 24 +++++++------------ .../HelloWebfluxMethodApplicationTests.java | 17 ++++++------- .../sample/HelloWebfluxApplicationITests.java | 24 +++++++------------ .../sample/HelloWebfluxApplicationTests.java | 17 ++++++------- .../HelloWebfluxFnApplicationITests.java | 16 ++++++------- .../HelloWebfluxFnApplicationTests.java | 17 ++++++------- .../AuthenticationWebFilterTests.java | 18 +++++++------- 11 files changed, 62 insertions(+), 103 deletions(-) diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java index 7d6ee069193..2654355755a 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java @@ -68,8 +68,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; /** * @author Rob Winch @@ -122,11 +120,10 @@ public void authenticateWhenBasicThenNoSession() { WebTestClient client = WebTestClientBuilder .bindToWebFilters(this.springSecurityFilterChain) - .filter(basicAuthentication()) .build(); FluxExchangeResult result = client.get() - .attributes(basicAuthenticationCredentials("user", "password")) + .headers(headers -> headers.setBasicAuth("user", "password")) .exchange() .expectStatus() .isOk() @@ -171,13 +168,12 @@ public void defaultPopulatesReactorContextWhenAuthenticating() { .flatMap( principal -> exchange.getResponse() .writeWith(Mono.just(toDataBuffer(principal.getName())))) ) - .filter(basicAuthentication()) .build(); client .get() .uri("/") - .attributes(basicAuthenticationCredentials("user", "password")) + .headers(headers -> headers.setBasicAuth("user", "password")) .exchange() .expectStatus().isOk() .expectBody(String.class).consumeWith( result -> assertThat(result.getResponseBody()).isEqualTo("user")); @@ -208,13 +204,12 @@ public void passwordEncoderBeanIsUsed() { .flatMap( principal -> exchange.getResponse() .writeWith(Mono.just(toDataBuffer(principal.getName())))) ) - .filter(basicAuthentication()) .build(); client .get() .uri("/") - .attributes(basicAuthenticationCredentials("user", "password")) + .headers(headers -> headers.setBasicAuth("user", "password")) .exchange() .expectStatus().isOk() .expectBody(String.class).consumeWith( result -> assertThat(result.getResponseBody()).isEqualTo("user")); diff --git a/config/src/test/java/org/springframework/security/config/web/server/ExceptionHandlingSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/ExceptionHandlingSpecTests.java index e88ab4848a0..d18e30fd464 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/ExceptionHandlingSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/ExceptionHandlingSpecTests.java @@ -25,8 +25,6 @@ import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler; import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler; import org.springframework.test.web.reactive.server.WebTestClient; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; /** * @author Denys Ivano @@ -96,13 +94,12 @@ public void defaultAccessDeniedHandler() { WebTestClient client = WebTestClientBuilder .bindToWebFilters(securityWebFilter) - .filter(basicAuthentication()) .build(); client .get() .uri("/admin") - .attributes(basicAuthenticationCredentials("user", "password")) + .headers(headers -> headers.setBasicAuth("user", "password")) .exchange() .expectStatus().isForbidden(); } @@ -122,13 +119,12 @@ public void customAccessDeniedHandler() { WebTestClient client = WebTestClientBuilder .bindToWebFilters(securityWebFilter) - .filter(basicAuthentication()) .build(); client .get() .uri("/admin") - .attributes(basicAuthenticationCredentials("user", "password")) + .headers(headers -> headers.setBasicAuth("user", "password")) .exchange() .expectStatus().isBadRequest(); } diff --git a/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java b/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java index 8926888bdb5..5e572c559ff 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java @@ -40,7 +40,6 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.any; import static org.mockito.Mockito.when; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; /** * @author Rob Winch @@ -92,12 +91,9 @@ public void basic() { WebTestClient client = buildClient(); - EntityExchangeResult result = client - .mutate() - .filter(basicAuthentication("rob", "rob")) - .build() - .get() + EntityExchangeResult result = client.get() .uri("/") + .headers(headers -> headers.setBasicAuth("rob", "rob")) .exchange() .expectStatus().isOk() .expectHeader().valueMatches(HttpHeaders.CACHE_CONTROL, ".+") diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java index 83119c6c6bc..6fdee0c9ce6 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java @@ -23,12 +23,10 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; import org.springframework.web.reactive.function.BodyInserters; -import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; import static org.springframework.security.oauth2.core.web.reactive.function.OAuth2BodyExtractors.oauth2AccessTokenResponse; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; /** * An implementation of an {@link ReactiveOAuth2AccessTokenResponseClient} that "exchanges" @@ -49,7 +47,6 @@ */ public class WebClientReactiveAuthorizationCodeTokenResponseClient implements ReactiveOAuth2AccessTokenResponseClient { private WebClient webClient = WebClient.builder() - .filter(ExchangeFilterFunctions.basicAuthentication()) .build(); @Override @@ -66,7 +63,7 @@ public Mono getTokenResponse(OAuth2AuthorizationCodeG return this.webClient.post() .uri(tokenUri) .accept(MediaType.APPLICATION_JSON) - .attributes(basicAuthenticationCredentials(clientRegistration.getClientId(), clientRegistration.getClientSecret())) + .headers(headers -> headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret())) .body(body) .exchange() .flatMap(response -> response.body(oauth2AccessTokenResponse())) diff --git a/samples/boot/hellowebflux-method/src/integration-test/java/sample/HelloWebfluxMethodApplicationITests.java b/samples/boot/hellowebflux-method/src/integration-test/java/sample/HelloWebfluxMethodApplicationITests.java index ff56fb8c1d8..f474e2c0a31 100644 --- a/samples/boot/hellowebflux-method/src/integration-test/java/sample/HelloWebfluxMethodApplicationITests.java +++ b/samples/boot/hellowebflux-method/src/integration-test/java/sample/HelloWebfluxMethodApplicationITests.java @@ -15,19 +15,16 @@ */ package sample; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; - -import java.util.Map; import java.util.function.Consumer; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; /** * @author Rob Winch @@ -37,13 +34,8 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class HelloWebfluxMethodApplicationITests { - WebTestClient rest; - @Autowired - public void setRest(WebTestClient rest) { - this.rest = rest - .mutateWith((b, h, c) -> b.filter(ExchangeFilterFunctions.basicAuthentication())); - } + WebTestClient rest; @Test @@ -60,7 +52,7 @@ public void messageWhenUserThenForbidden() throws Exception { this.rest .get() .uri("/message") - .attributes(robsCredentials()) + .headers(robsCredentials()) .exchange() .expectStatus().isEqualTo(HttpStatus.FORBIDDEN); } @@ -70,18 +62,18 @@ public void messageWhenAdminThenOk() throws Exception { this.rest .get() .uri("/message") - .attributes(adminCredentials()) + .headers(adminCredentials()) .exchange() .expectStatus().isOk() .expectBody(String.class).isEqualTo("Hello World!"); } - private Consumer> robsCredentials() { - return basicAuthenticationCredentials("rob", "rob"); + private Consumer robsCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("rob", "rob"); } - private Consumer> adminCredentials() { - return basicAuthenticationCredentials("admin", "admin"); + private Consumer adminCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("admin", "admin"); } } diff --git a/samples/boot/hellowebflux-method/src/test/java/sample/HelloWebfluxMethodApplicationTests.java b/samples/boot/hellowebflux-method/src/test/java/sample/HelloWebfluxMethodApplicationTests.java index a167da26ff4..635d9aa6d7d 100644 --- a/samples/boot/hellowebflux-method/src/test/java/sample/HelloWebfluxMethodApplicationTests.java +++ b/samples/boot/hellowebflux-method/src/test/java/sample/HelloWebfluxMethodApplicationTests.java @@ -17,10 +17,7 @@ import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser; import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; -import java.util.Map; import java.util.function.Consumer; import org.junit.Test; @@ -28,6 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit4.SpringRunner; @@ -48,7 +46,6 @@ public void setup(ApplicationContext context) { .bindToApplicationContext(context) .apply(springSecurity()) .configureClient() - .filter(basicAuthentication()) .build(); } @@ -68,7 +65,7 @@ public void messageWhenUserThenForbidden() throws Exception { this.rest .get() .uri("/message") - .attributes(robsCredentials()) + .headers(robsCredentials()) .exchange() .expectStatus().isEqualTo(HttpStatus.FORBIDDEN); } @@ -78,7 +75,7 @@ public void messageWhenAdminThenOk() throws Exception { this.rest .get() .uri("/message") - .attributes(adminCredentials()) + .headers(adminCredentials()) .exchange() .expectStatus().isOk() .expectBody(String.class).isEqualTo("Hello World!"); @@ -130,11 +127,11 @@ public void messageWhenMutateWithMockAdminThenOk() throws Exception { .expectBody(String.class).isEqualTo("Hello World!"); } - private Consumer> robsCredentials() { - return basicAuthenticationCredentials("rob", "rob"); + private Consumer robsCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("rob", "rob"); } - private Consumer> adminCredentials() { - return basicAuthenticationCredentials("admin", "admin"); + private Consumer adminCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("admin", "admin"); } } diff --git a/samples/boot/hellowebflux/src/integration-test/java/sample/HelloWebfluxApplicationITests.java b/samples/boot/hellowebflux/src/integration-test/java/sample/HelloWebfluxApplicationITests.java index 0d6be6b71b7..2629ab5a95d 100644 --- a/samples/boot/hellowebflux/src/integration-test/java/sample/HelloWebfluxApplicationITests.java +++ b/samples/boot/hellowebflux/src/integration-test/java/sample/HelloWebfluxApplicationITests.java @@ -15,18 +15,15 @@ */ package sample; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; - -import java.util.Map; import java.util.function.Consumer; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; /** * @author Rob Winch @@ -36,13 +33,8 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class HelloWebfluxApplicationITests { - WebTestClient rest; - @Autowired - public void setRest(WebTestClient rest) { - this.rest = rest - .mutateWith((b, h, c) -> b.filter(ExchangeFilterFunctions.basicAuthentication())); - } + WebTestClient rest; @Test public void basicWhenNoCredentialsThenUnauthorized() throws Exception { @@ -58,7 +50,7 @@ public void basicWhenValidCredentialsThenOk() throws Exception { this.rest .get() .uri("/") - .attributes(userCredentials()) + .headers(userCredentials()) .exchange() .expectStatus().isOk() .expectBody().json("{\"message\":\"Hello user!\"}"); @@ -69,17 +61,17 @@ public void basicWhenInvalidCredentialsThenUnauthorized() throws Exception { this.rest .get() .uri("/") - .attributes(invalidCredentials()) + .headers(invalidCredentials()) .exchange() .expectStatus().isUnauthorized() .expectBody().isEmpty(); } - private Consumer> userCredentials() { - return basicAuthenticationCredentials("user", "user"); + private Consumer userCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("user", "user"); } - private Consumer> invalidCredentials() { - return basicAuthenticationCredentials("user", "INVALID"); + private Consumer invalidCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("user", "INVALID"); } } diff --git a/samples/boot/hellowebflux/src/test/java/sample/HelloWebfluxApplicationTests.java b/samples/boot/hellowebflux/src/test/java/sample/HelloWebfluxApplicationTests.java index 0c755510f7e..eb3c8af20d4 100644 --- a/samples/boot/hellowebflux/src/test/java/sample/HelloWebfluxApplicationTests.java +++ b/samples/boot/hellowebflux/src/test/java/sample/HelloWebfluxApplicationTests.java @@ -17,10 +17,7 @@ import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser; import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; -import java.util.Map; import java.util.function.Consumer; import org.junit.Test; @@ -29,6 +26,7 @@ import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpHeaders; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; @@ -49,7 +47,6 @@ public void setup(ApplicationContext context) { .bindToApplicationContext(context) .apply(springSecurity()) .configureClient() - .filter(basicAuthentication()) .build(); } @@ -67,7 +64,7 @@ public void basicWhenValidCredentialsThenOk() throws Exception { this.rest .get() .uri("/") - .attributes(userCredentials()) + .headers(userCredentials()) .exchange() .expectStatus().isOk() .expectBody().json("{\"message\":\"Hello user!\"}"); @@ -78,7 +75,7 @@ public void basicWhenInvalidCredentialsThenUnauthorized() throws Exception { this.rest .get() .uri("/") - .attributes(invalidCredentials()) + .headers(invalidCredentials()) .exchange() .expectStatus().isUnauthorized() .expectBody().isEmpty(); @@ -106,11 +103,11 @@ public void mockSupportWhenWithMockUserThenOk() throws Exception { .expectBody().json("{\"message\":\"Hello user!\"}"); } - private Consumer> userCredentials() { - return basicAuthenticationCredentials("user", "user"); + private Consumer userCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("user", "user"); } - private Consumer> invalidCredentials() { - return basicAuthenticationCredentials("user", "INVALID"); + private Consumer invalidCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("user", "INVALID"); } } diff --git a/samples/boot/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationITests.java b/samples/boot/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationITests.java index c0e176f2b18..2ca6eca77c0 100644 --- a/samples/boot/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationITests.java +++ b/samples/boot/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationITests.java @@ -15,15 +15,13 @@ */ package sample; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; - -import java.util.Map; import java.util.function.Consumer; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; @@ -58,7 +56,7 @@ public void basicWhenValidCredentialsThenOk() throws Exception { this.rest .get() .uri("/") - .attributes(userCredentials()) + .headers(userCredentials()) .exchange() .expectStatus().isOk() .expectBody().json("{\"message\":\"Hello user!\"}"); @@ -69,17 +67,17 @@ public void basicWhenInvalidCredentialsThenUnauthorized() throws Exception { this.rest .get() .uri("/") - .attributes(invalidCredentials()) + .headers(invalidCredentials()) .exchange() .expectStatus().isUnauthorized() .expectBody().isEmpty(); } - private Consumer> userCredentials() { - return basicAuthenticationCredentials("user", "user"); + private Consumer userCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("user", "user"); } - private Consumer> invalidCredentials() { - return basicAuthenticationCredentials("user", "INVALID"); + private Consumer invalidCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("user", "INVALID"); } } diff --git a/samples/boot/hellowebfluxfn/src/test/java/sample/HelloWebfluxFnApplicationTests.java b/samples/boot/hellowebfluxfn/src/test/java/sample/HelloWebfluxFnApplicationTests.java index 0de4c718e12..b2783cdc76b 100644 --- a/samples/boot/hellowebfluxfn/src/test/java/sample/HelloWebfluxFnApplicationTests.java +++ b/samples/boot/hellowebfluxfn/src/test/java/sample/HelloWebfluxFnApplicationTests.java @@ -17,10 +17,7 @@ import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser; import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; -import java.util.Map; import java.util.function.Consumer; import org.junit.Test; @@ -29,6 +26,7 @@ import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpHeaders; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; @@ -50,7 +48,6 @@ public void setup(ApplicationContext context) { .bindToApplicationContext(context) .apply(springSecurity()) .configureClient() - .filter(basicAuthentication()) .build(); } @@ -68,7 +65,7 @@ public void basicWhenValidCredentialsThenOk() throws Exception { this.rest .get() .uri("/") - .attributes(userCredentials()) + .headers(userCredentials()) .exchange() .expectStatus().isOk() .expectBody().json("{\"message\":\"Hello user!\"}"); @@ -79,7 +76,7 @@ public void basicWhenInvalidCredentialsThenUnauthorized() throws Exception { this.rest .get() .uri("/") - .attributes(invalidCredentials()) + .headers(invalidCredentials()) .exchange() .expectStatus().isUnauthorized() .expectBody().isEmpty(); @@ -107,11 +104,11 @@ public void mockSupportWhenWithMockUserThenOk() throws Exception { .expectBody().json("{\"message\":\"Hello user!\"}"); } - private Consumer> userCredentials() { - return basicAuthenticationCredentials("user", "user"); + private Consumer userCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("user", "user"); } - private Consumer> invalidCredentials() { - return basicAuthenticationCredentials("user", "INVALID"); + private Consumer invalidCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("user", "INVALID"); } } diff --git a/web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationWebFilterTests.java b/web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationWebFilterTests.java index 137149e14ec..36839584d6b 100644 --- a/web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationWebFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationWebFilterTests.java @@ -16,18 +16,12 @@ package org.springframework.security.web.server.authentication; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.*; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import reactor.core.publisher.Mono; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.ReactiveAuthenticationManager; @@ -39,7 +33,15 @@ import org.springframework.test.web.reactive.server.EntityExchangeResult; import org.springframework.test.web.reactive.server.WebTestClient; -import reactor.core.publisher.Mono; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; /** From 3383ae56208e6e331e8663d1762ed4530b5993b7 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 30 Jul 2018 15:15:30 -0500 Subject: [PATCH 200/226] Use HttpHeaders.setBasicAuth Issue: gh-5612 --- ...uth2AuthorizedClientExchangeFilterFunction.java | 14 +------------- ...uth2AuthorizedClientExchangeFilterFunction.java | 14 +------------- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java index b8edf6f1336..1891dd4e3b5 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java @@ -37,11 +37,9 @@ import reactor.core.publisher.Mono; import java.net.URI; -import java.nio.charset.StandardCharsets; import java.time.Clock; import java.time.Duration; import java.time.Instant; -import java.util.Base64; import java.util.Collection; import java.util.Map; import java.util.Optional; @@ -148,7 +146,7 @@ private Mono refreshAuthorizedClient(ExchangeFunction ne .getProviderDetails().getTokenUri(); ClientRequest request = ClientRequest.create(HttpMethod.POST, URI.create(tokenUri)) .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) - .headers(httpBasic(clientRegistration.getClientId(), clientRegistration.getClientSecret())) + .headers(headers -> headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret())) .body(refreshTokenBody(authorizedClient.getRefreshToken().getTokenValue())) .build(); return next.exchange(request) @@ -161,16 +159,6 @@ private Mono refreshAuthorizedClient(ExchangeFunction ne .thenReturn(result)); } - private static Consumer httpBasic(String username, String password) { - return httpHeaders -> { - String credentialsString = username + ":" + password; - byte[] credentialBytes = credentialsString.getBytes(StandardCharsets.ISO_8859_1); - byte[] encodedBytes = Base64.getEncoder().encode(credentialBytes); - String encodedCredentials = new String(encodedBytes, StandardCharsets.ISO_8859_1); - httpHeaders.set(HttpHeaders.AUTHORIZATION, "Basic " + encodedCredentials); - }; - } - private boolean shouldRefresh(OAuth2AuthorizedClient authorizedClient) { if (this.authorizedClientService == null) { return false; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java index 60969a5bb0a..dfbcd64797a 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java @@ -45,11 +45,9 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.net.URI; -import java.nio.charset.StandardCharsets; import java.time.Clock; import java.time.Duration; import java.time.Instant; -import java.util.Base64; import java.util.Collection; import java.util.Map; import java.util.Optional; @@ -290,7 +288,7 @@ private Mono refreshAuthorizedClient(ClientRequest reque .getProviderDetails().getTokenUri(); ClientRequest refreshRequest = ClientRequest.create(HttpMethod.POST, URI.create(tokenUri)) .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) - .headers(httpBasic(clientRegistration.getClientId(), clientRegistration.getClientSecret())) + .headers(headers -> headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret())) .body(refreshTokenBody(authorizedClient.getRefreshToken().getTokenValue())) .build(); return next.exchange(refreshRequest) @@ -309,16 +307,6 @@ private Mono refreshAuthorizedClient(ClientRequest reque .publishOn(Schedulers.elastic()); } - private static Consumer httpBasic(String username, String password) { - return httpHeaders -> { - String credentialsString = username + ":" + password; - byte[] credentialBytes = credentialsString.getBytes(StandardCharsets.ISO_8859_1); - byte[] encodedBytes = Base64.getEncoder().encode(credentialBytes); - String encodedCredentials = new String(encodedBytes, StandardCharsets.ISO_8859_1); - httpHeaders.set(HttpHeaders.AUTHORIZATION, "Basic " + encodedCredentials); - }; - } - private boolean shouldRefresh(OAuth2AuthorizedClient authorizedClient) { if (this.authorizedClientRepository == null) { return false; From 4f0ce5af13528fe0b259b3f31ee54de33d573ec1 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 31 Jul 2018 09:00:34 -0500 Subject: [PATCH 201/226] BasicAuthenticationFilter case insenstive Fixes: gh-5586 --- .../www/BasicAuthenticationFilter.java | 2 +- .../www/BasicAuthenticationFilterTests.java | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java b/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java index dbd4fa8f172..28f7354068a 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java @@ -154,7 +154,7 @@ protected void doFilterInternal(HttpServletRequest request, String header = request.getHeader("Authorization"); - if (header == null || !header.startsWith("Basic ")) { + if (header == null || !header.toLowerCase().startsWith("basic ")) { chain.doFilter(request, response); return; } diff --git a/web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilterTests.java index d6beb9545c9..1c45cdfbfa0 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilterTests.java @@ -156,6 +156,26 @@ public void testNormalOperation() throws Exception { .isEqualTo("rod"); } + // gh-5586 + @Test + public void doFilterWhenSchemeLowercaseThenCaseInsensitveMatchWorks() throws Exception { + String token = "rod:koala"; + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", + "basic " + new String(Base64.encodeBase64(token.getBytes()))); + request.setServletPath("/some_file.html"); + + // Test + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + FilterChain chain = mock(FilterChain.class); + filter.doFilter(request, new MockHttpServletResponse(), chain); + + verify(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull(); + assertThat(SecurityContextHolder.getContext().getAuthentication().getName()) + .isEqualTo("rod"); + } + @Test public void testOtherAuthorizationSchemeIsIgnored() throws Exception { From 905427d1df1d40ed0d023c0a8a46af570f15395d Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 31 Jul 2018 09:05:04 -0500 Subject: [PATCH 202/226] ServerHttpBasicAuthenticationConverter Validates Scheme Name Fixes: gh-5414 --- .../ServerHttpBasicAuthenticationConverter.java | 2 +- ...verHttpBasicAuthenticationConverterTests.java | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/web/src/main/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverter.java b/web/src/main/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverter.java index 7f431f09e80..245ecfd2081 100644 --- a/web/src/main/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverter.java +++ b/web/src/main/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverter.java @@ -41,7 +41,7 @@ public Mono convert(ServerWebExchange exchange) { ServerHttpRequest request = exchange.getRequest(); String authorization = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); - if(authorization == null) { + if(authorization == null || !authorization.toLowerCase().startsWith("basic ")) { return Mono.empty(); } diff --git a/web/src/test/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverterTests.java b/web/src/test/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverterTests.java index 63f5c796f4d..01da39d2a00 100644 --- a/web/src/test/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverterTests.java @@ -79,6 +79,22 @@ public void applyWhenUserPasswordThenAuthentication() { assertThat(authentication.getCredentials()).isEqualTo("password"); } + @Test + public void applyWhenLowercaseSchemeThenAuthentication() { + Mono result = convert(this.request.header(HttpHeaders.AUTHORIZATION, "basic dXNlcjpwYXNzd29yZA==")); + + UsernamePasswordAuthenticationToken authentication = result.cast(UsernamePasswordAuthenticationToken.class).block(); + assertThat(authentication.getPrincipal()).isEqualTo("user"); + assertThat(authentication.getCredentials()).isEqualTo("password"); + } + + @Test + public void applyWhenWrongSchemeThenAuthentication() { + Mono result = convert(this.request.header(HttpHeaders.AUTHORIZATION, "token dXNlcjpwYXNzd29yZA==")); + + assertThat(result.block()).isNull(); + } + private Mono convert(MockServerHttpRequest.BaseBuilder request) { return this.converter.convert(MockServerWebExchange.from(this.request.build())); } From ca010db0d547f0d5b3a680ed4c2c756a891428b3 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Tue, 31 Jul 2018 23:49:29 +0900 Subject: [PATCH 203/226] Mention spring-security-data dependency for Spring Data in doc Closes #5556 --- docs/manual/src/docs/asciidoc/_includes/data/index.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manual/src/docs/asciidoc/_includes/data/index.adoc b/docs/manual/src/docs/asciidoc/_includes/data/index.adoc index f0cb0d9f2fd..cd4700ec9ac 100644 --- a/docs/manual/src/docs/asciidoc/_includes/data/index.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/data/index.adoc @@ -8,7 +8,7 @@ It is not only useful but necessary to include the user in the queries to suppor [[data-configuration]] == Spring Data & Spring Security Configuration -To use this support, provide a bean of type `SecurityEvaluationContextExtension`. +To use this support, add `org.springframework.security:spring-security-data` dependency and provide a bean of type `SecurityEvaluationContextExtension`. In Java Configuration, this would look like: [source,java] From 6646073235a1fae325004bd4bf9c5505e7adbbf3 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 31 Jul 2018 11:37:20 -0500 Subject: [PATCH 204/226] Add CORS WebFlux Support Fixes: gh-4832 --- .../web/server/SecurityWebFiltersOrder.java | 4 + .../config/web/server/ServerHttpSecurity.java | 83 +++++++++++++ .../config/web/server/CorsSpecTests.java | 117 ++++++++++++++++++ .../web/server/TestingServerHttpSecurity.java | 32 +++++ 4 files changed, 236 insertions(+) create mode 100644 config/src/test/java/org/springframework/security/config/web/server/CorsSpecTests.java create mode 100644 config/src/test/java/org/springframework/security/config/web/server/TestingServerHttpSecurity.java diff --git a/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java b/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java index fc68e28f7d0..3c3d8acf9c4 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java +++ b/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java @@ -23,6 +23,10 @@ public enum SecurityWebFiltersOrder { FIRST(Integer.MIN_VALUE), HTTP_HEADERS_WRITER, + /** + * {@link org.springframework.web.cors.reactive.CorsWebFilter} + */ + CORS, /** * {@link org.springframework.security.web.server.csrf.CsrfWebFilter} */ diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index dde83846af8..0d2e848269e 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -111,6 +111,10 @@ import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.web.cors.reactive.CorsConfigurationSource; +import org.springframework.web.cors.reactive.CorsProcessor; +import org.springframework.web.cors.reactive.CorsWebFilter; +import org.springframework.web.cors.reactive.DefaultCorsProcessor; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; @@ -181,6 +185,8 @@ public class ServerHttpSecurity { private CsrfSpec csrf = new CsrfSpec(); + private CorsSpec cors = new CorsSpec(); + private ExceptionHandlingSpec exceptionHandling = new ExceptionHandlingSpec(); private HttpBasicSpec httpBasic; @@ -299,6 +305,80 @@ public CsrfSpec csrf() { return this.csrf; } + /** + * Configures CORS headers. By default if a {@link CorsConfigurationSource} Bean is found, it will be used + * to create a {@link CorsWebFilter}. If {@link CorsSpec#configurationSource(CorsConfigurationSource)} is invoked + * it will be used instead. If neither has been configured, the Cors configuration will do nothing. + * @return the {@link CorsSpec} to customize + */ + public CorsSpec cors() { + if (this.cors == null) { + this.cors = new CorsSpec(); + } + return this.cors; + } + + /** + * Configures CORS support within Spring Security. This ensures that the {@link CorsWebFilter} is place in the + * correct order. + */ + public class CorsSpec { + private CorsWebFilter corsFilter; + + /** + * Configures the {@link CorsConfigurationSource} to be used + * @param source the source to use + * @return the {@link CorsSpec} for additional configuration + */ + public CorsSpec configurationSource(CorsConfigurationSource source) { + this.corsFilter = new CorsWebFilter(source); + return this; + } + + /** + * Disables CORS support within Spring Security. + * @return the {@link ServerHttpSecurity} to continue configuring + */ + public ServerHttpSecurity disable() { + ServerHttpSecurity.this.cors = null; + return ServerHttpSecurity.this; + } + + /** + * Allows method chaining to continue configuring the {@link ServerHttpSecurity} + * @return the {@link ServerHttpSecurity} to continue configuring + */ + public ServerHttpSecurity and() { + return ServerHttpSecurity.this; + } + + protected void configure(ServerHttpSecurity http) { + CorsWebFilter corsFilter = getCorsFilter(); + if (corsFilter != null) { + http.addFilterAt(this.corsFilter, SecurityWebFiltersOrder.CORS); + } + } + + private CorsWebFilter getCorsFilter() { + if (this.corsFilter != null) { + return this.corsFilter; + } + + CorsConfigurationSource source = getBeanOrNull(CorsConfigurationSource.class); + if (source == null) { + return null; + } + CorsProcessor processor = getBeanOrNull(CorsProcessor.class); + if (processor == null) { + processor = new DefaultCorsProcessor(); + } + this.corsFilter = new CorsWebFilter(source, processor); + return this.corsFilter; + } + + private CorsSpec() {} + } + /** * Configures HTTP Basic authentication. An example configuration is provided below: * @@ -782,6 +862,9 @@ public SecurityWebFilterChain build() { if(this.csrf != null) { this.csrf.configure(this); } + if (this.cors != null) { + this.cors.configure(this); + } if(this.httpBasic != null) { this.httpBasic.authenticationManager(this.authenticationManager); this.httpBasic.configure(this); diff --git a/config/src/test/java/org/springframework/security/config/web/server/CorsSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/CorsSpecTests.java new file mode 100644 index 00000000000..d777d60bc13 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/web/server/CorsSpecTests.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.core.ResolvableType; +import org.springframework.http.HttpHeaders; +import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; +import org.springframework.test.web.reactive.server.FluxExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.reactive.CorsConfigurationSource; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + * @since 5.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class CorsSpecTests { + @Mock + private CorsConfigurationSource source; + @Mock + private ApplicationContext context; + + ServerHttpSecurity http; + + HttpHeaders expectedHeaders = new HttpHeaders(); + + Set headerNamesNotPresent = new HashSet<>(); + + @Before + public void setup() { + this.http = new TestingServerHttpSecurity() + .applicationContext(this.context); + CorsConfiguration value = new CorsConfiguration(); + value.setAllowedOrigins(Arrays.asList("*")); + when(this.source.getCorsConfiguration(any())).thenReturn(value); + } + + @Test + public void corsWhenEnabledThenAccessControlAllowOriginAndSecurityHeaders() { + this.http.cors().configurationSource(this.source); + this.expectedHeaders.set("Access-Control-Allow-Origin", "*"); + this.expectedHeaders.set("X-Frame-Options", "DENY"); + assertHeaders(); + } + + @Test + public void corsWhenCorsConfigurationSourceBeanThenAccessControlAllowOriginAndSecurityHeaders() { + when(this.context.getBeanNamesForType(any(ResolvableType.class))).thenReturn(new String[] {"source"}, new String[0]); + when(this.context.getBean("source")).thenReturn(this.source); + this.expectedHeaders.set("Access-Control-Allow-Origin", "*"); + this.expectedHeaders.set("X-Frame-Options", "DENY"); + assertHeaders(); + } + + @Test + public void corsWhenNoConfigurationSourceThenNoCorsHeaders() { + when(this.context.getBeanNamesForType(any(ResolvableType.class))).thenReturn(new String[0]); + this.headerNamesNotPresent.add("Access-Control-Allow-Origin"); + assertHeaders(); + } + + private void assertHeaders() { + WebTestClient client = buildClient(); + FluxExchangeResult response = client.get() + .uri("https://example.com/") + .headers(h -> h.setOrigin("https://origin.example.com")) + .exchange() + .returnResult(String.class); + + Map> responseHeaders = response.getResponseHeaders(); + + if (!this.expectedHeaders.isEmpty()) { + assertThat(responseHeaders).describedAs(response.toString()) + .containsAllEntriesOf(this.expectedHeaders); + } + if (!this.headerNamesNotPresent.isEmpty()) { + assertThat(responseHeaders.keySet()).doesNotContainAnyElementsOf(this.headerNamesNotPresent); + } + } + + private WebTestClient buildClient() { + return WebTestClientBuilder + .bindToWebFilters(this.http.build()) + .build(); + } +} diff --git a/config/src/test/java/org/springframework/security/config/web/server/TestingServerHttpSecurity.java b/config/src/test/java/org/springframework/security/config/web/server/TestingServerHttpSecurity.java new file mode 100644 index 00000000000..c36d75e9d60 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/web/server/TestingServerHttpSecurity.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class TestingServerHttpSecurity extends ServerHttpSecurity { + public TestingServerHttpSecurity applicationContext(ApplicationContext applicationContext) + throws BeansException { + super.setApplicationContext(applicationContext); + return this; + } +} From 9f1a2999442dd88676d05061319017a850115750 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 31 Jul 2018 14:44:40 -0500 Subject: [PATCH 205/226] Add defaultOAuth2AuthorizedClient flag Fixes: gh-5619 --- ...uthorizedClientExchangeFilterFunction.java | 20 +++++++++++++++++-- ...izedClientExchangeFilterFunctionTests.java | 16 ++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java index dfbcd64797a..65d6c26c4b8 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java @@ -109,12 +109,25 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement private OAuth2AuthorizedClientRepository authorizedClientRepository; + private boolean defaultOAuth2AuthorizedClient; + public ServletOAuth2AuthorizedClientExchangeFilterFunction() {} public ServletOAuth2AuthorizedClientExchangeFilterFunction(OAuth2AuthorizedClientRepository authorizedClientRepository) { this.authorizedClientRepository = authorizedClientRepository; } + /** + * If true, a default {@link OAuth2AuthorizedClient} can be discovered from the current Authentication. It is + * recommended to be cautious with this feature since all HTTP requests will receive the access token if it can be + * resolved from the current Authentication. + * @param defaultOAuth2AuthorizedClient true if a default {@link OAuth2AuthorizedClient} should be used, else false. + * Default is false. + */ + public void setDefaultOAuth2AuthorizedClient(boolean defaultOAuth2AuthorizedClient) { + this.defaultOAuth2AuthorizedClient = defaultOAuth2AuthorizedClient; + } + /** * Configures the builder with {@link #defaultRequest()} and adds this as a {@link ExchangeFilterFunction} * @return the {@link Consumer} to configure the builder @@ -251,13 +264,16 @@ private void populateDefaultAuthentication(Map attrs) { } private void populateDefaultOAuth2AuthorizedClient(Map attrs) { - if (this.authorizedClientRepository == null || attrs.containsKey(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME)) { + if (this.authorizedClientRepository == null + || attrs.containsKey(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME)) { return; } Authentication authentication = getAuthentication(attrs); String clientRegistrationId = getClientRegistrationId(attrs); - if (clientRegistrationId == null && authentication instanceof OAuth2AuthenticationToken) { + if (clientRegistrationId == null + && this.defaultOAuth2AuthorizedClient + && authentication instanceof OAuth2AuthenticationToken) { clientRegistrationId = ((OAuth2AuthenticationToken) authentication).getAuthorizedClientRegistrationId(); } if (clientRegistrationId != null) { diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java index a8e2b93dfc4..378b8dcac6f 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java @@ -207,8 +207,9 @@ public void defaultRequestOAuth2AuthorizedClientWhenRepositoryNullThenOAuth2Auth } @Test - public void defaultRequestOAuth2AuthorizedClientWhenAuthenticationAndClientRegistrationIdNullThenOAuth2AuthorizedClient() { + public void defaultRequestOAuth2AuthorizedClientWhenDefaultTrueAndAuthenticationAndClientRegistrationIdNullThenOAuth2AuthorizedClient() { this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + this.function.setDefaultOAuth2AuthorizedClient(true); OAuth2User user = mock(OAuth2User.class); List authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(user, authorities, "id"); @@ -223,6 +224,19 @@ public void defaultRequestOAuth2AuthorizedClientWhenAuthenticationAndClientRegis verify(this.authorizedClientRepository).loadAuthorizedClient(eq(token.getAuthorizedClientRegistrationId()), any(), any()); } + @Test + public void defaultRequestOAuth2AuthorizedClientWhenDefaultFalseAndAuthenticationAndClientRegistrationIdNullThenOAuth2AuthorizedClient() { + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + OAuth2User user = mock(OAuth2User.class); + List authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); + OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(user, authorities, "id"); + authentication(token).accept(this.result); + + Map attrs = getDefaultRequestAttributes(); + + assertThat(getOAuth2AuthorizedClient(attrs)).isNull(); + } + @Test public void defaultRequestOAuth2AuthorizedClientWhenAuthenticationAndClientRegistrationIdThenIdIsExplicit() { this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); From 47c935fc206aabade1145ade8498ee70f02f3a47 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Fri, 3 Aug 2018 22:37:48 +0900 Subject: [PATCH 206/226] Add @Deprecation on removeAuthorizationRequest() (#5634) --- .../oauth2/client/web/AuthorizationRequestRepository.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthorizationRequestRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthorizationRequestRepository.java index 72226656c7a..1c7ed342dd5 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthorizationRequestRepository.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthorizationRequestRepository.java @@ -67,11 +67,12 @@ void saveAuthorizationRequest(T authorizationRequest, HttpServletRequest request * @param request the {@code HttpServletRequest} * @return the removed {@link OAuth2AuthorizationRequest} or {@code null} if not available */ + @Deprecated T removeAuthorizationRequest(HttpServletRequest request); /** * Removes and returns the {@link OAuth2AuthorizationRequest} associated to the - * provided {@code HttpServletRequest} or if not available returns {@code null}. + * provided {@code HttpServletRequest} and {@code HttpServletResponse} or if not available returns {@code null}. * * @since 5.1 * @param request the {@code HttpServletRequest} From dd1bd431d42b1bbabbbb234c87572f0b13ee7a87 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 3 Aug 2018 09:37:03 -0500 Subject: [PATCH 207/226] Add @Configuration to ServerHttpSecurityConfiguration Fixes: gh-5635 --- .../web/reactive/ServerHttpSecurityConfiguration.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java index 2596373e5fe..d5c63002a88 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java @@ -22,6 +22,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.core.ReactiveAdapterRegistry; @@ -39,6 +40,7 @@ * @author Rob Winch * @since 5.0 */ +@Configuration class ServerHttpSecurityConfiguration implements WebFluxConfigurer { private static final String BEAN_NAME_PREFIX = "org.springframework.security.config.annotation.web.reactive.HttpSecurityConfiguration."; private static final String HTTPSECURITY_BEAN_NAME = BEAN_NAME_PREFIX + "httpSecurity"; From d09c244de91e9b45945616bfc7d5c9c742651791 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Fri, 3 Aug 2018 01:14:36 +0900 Subject: [PATCH 208/226] Fix typo Closes #5579 --- .../security/crypto/password/LdapShaPasswordEncoder.java | 2 +- .../security/crypto/password/Md4PasswordEncoder.java | 2 +- .../security/crypto/password/MessageDigestPasswordEncoder.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/LdapShaPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/LdapShaPasswordEncoder.java index 6babace267c..84fd719c7e0 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/LdapShaPasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/LdapShaPasswordEncoder.java @@ -37,7 +37,7 @@ * * @author Luke Taylor * @deprecated Digest based password encoding is not considered secure. Instead use an - * adaptive one way funciton like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or + * adaptive one way function like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or * SCryptPasswordEncoder. Even better use {@link DelegatingPasswordEncoder} which supports * password upgrades. There are no plans to remove this support. It is deprecated to indicate * that this is a legacy implementation and using it is considered insecure. diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java index c8e3fe7333c..b1571ec9713 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java @@ -71,7 +71,7 @@ * @author Rob winch * @since 5.0 * @deprecated Digest based password encoding is not considered secure. Instead use an - * adaptive one way funciton like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or + * adaptive one way function like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or * SCryptPasswordEncoder. Even better use {@link DelegatingPasswordEncoder} which supports * password upgrades. There are no plans to remove this support. It is deprecated to indicate * that this is a legacy implementation and using it is considered insecure. diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoder.java index 250bbae5aae..735c5ae0505 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoder.java @@ -74,7 +74,7 @@ * @author Rob Winch * @since 5.0 * @deprecated Digest based password encoding is not considered secure. Instead use an - * adaptive one way funciton like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or + * adaptive one way function like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or * SCryptPasswordEncoder. Even better use {@link DelegatingPasswordEncoder} which supports * password upgrades. There are no plans to remove this support. It is deprecated to indicate * that this is a legacy implementation and using it is considered insecure. From 9395983779f216d290cb25712223f4a12732ed15 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 3 Aug 2018 10:54:56 -0500 Subject: [PATCH 209/226] Regenerate spring-security-5.1.xsd Commit 884fdbf9 performed some manual edits of this file which means running the rncToXsd task causes this file to change. This commit regenerates the file properly. Fixes: gh-5640 --- .../security/config/spring-security-5.1.xsd | 70 +++++++------------ 1 file changed, 27 insertions(+), 43 deletions(-) diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.1.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.xsd index 6434e496fa2..773d9e55af1 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.1.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.xsd @@ -1,20 +1,4 @@ - - - + @@ -411,7 +395,7 @@ - + @@ -491,7 +475,7 @@ - + @@ -544,7 +528,7 @@ - + @@ -788,13 +772,13 @@ - - - - - - - + + + + + + + @@ -1236,7 +1220,7 @@ - + @@ -1261,7 +1245,7 @@ - + @@ -1332,7 +1316,7 @@ - + @@ -1379,7 +1363,7 @@ - + @@ -1467,7 +1451,7 @@ - + Sets up an attribute exchange configuration to request specified attributes from the @@ -1666,7 +1650,7 @@ - + @@ -1682,7 +1666,7 @@ - + @@ -1738,7 +1722,7 @@ - + @@ -1785,7 +1769,7 @@ - + @@ -1883,7 +1867,7 @@ - + @@ -1916,8 +1900,8 @@ - - + + @@ -1934,7 +1918,7 @@ - + @@ -2071,7 +2055,7 @@ - + @@ -2123,7 +2107,7 @@ - + @@ -2735,4 +2719,4 @@ - + \ No newline at end of file From df387c8fedaa35cdd9adc00ca5253791e2cac808 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Wed, 8 Aug 2018 10:00:24 +0900 Subject: [PATCH 210/226] Fix typo --- .../web/server/ServerHttpBasicAuthenticationConverterTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/test/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverterTests.java b/web/src/test/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverterTests.java index 01da39d2a00..efc102a0dc4 100644 --- a/web/src/test/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverterTests.java @@ -89,7 +89,7 @@ public void applyWhenLowercaseSchemeThenAuthentication() { } @Test - public void applyWhenWrongSchemeThenAuthentication() { + public void applyWhenWrongSchemeThenEmpty() { Mono result = convert(this.request.header(HttpHeaders.AUTHORIZATION, "token dXNlcjpwYXNzd29yZA==")); assertThat(result.block()).isNull(); From db367840b7719fb85bc9b1e90c9323dff5b4c69e Mon Sep 17 00:00:00 2001 From: Daniel Meier Date: Tue, 7 Aug 2018 15:41:56 +0200 Subject: [PATCH 211/226] Made JwtConfigurer fluent Adjusted return type of #decoder(JwtDecoder) and #jwkSetUri(String) to return the JwtDecoder itself. Added new method #and() that returns the enclosing OAuth2ResourceServerConfigurer. Fixes gh-5595 --- .../resource/OAuth2ResourceServerConfigurer.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java index 4f252d7912f..d03662615e2 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java @@ -199,13 +199,17 @@ public class JwtConfigurer { this.context = context; } - public OAuth2ResourceServerConfigurer decoder(JwtDecoder decoder) { + public JwtConfigurer decoder(JwtDecoder decoder) { this.decoder = decoder; - return OAuth2ResourceServerConfigurer.this; + return this; } - public OAuth2ResourceServerConfigurer jwkSetUri(String uri) { + public JwtConfigurer jwkSetUri(String uri) { this.decoder = new NimbusJwtDecoderJwkSupport(uri); + return this; + } + + public OAuth2ResourceServerConfigurer and() { return OAuth2ResourceServerConfigurer.this; } From 82f0767bb1002e47e26b50b2b66c76cbc68b2743 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 17 Jul 2018 23:11:59 -0400 Subject: [PATCH 212/226] Add support for client_credentials grant Fixes gh-4982 --- .../OAuth2ClientConfiguration.java | 14 +- .../OAuth2ClientConfigurationTests.java | 5 + ...tClientCredentialsTokenResponseClient.java | 270 +++++++++++++++ .../OAuth2ClientCredentialsGrantRequest.java | 56 +++ .../registration/ClientRegistration.java | 14 +- ...Auth2AuthorizedClientArgumentResolver.java | 96 +++++- ...ntCredentialsTokenResponseClientTests.java | 326 ++++++++++++++++++ ...th2ClientCredentialsGrantRequestTests.java | 76 ++++ .../registration/ClientRegistrationTests.java | 88 +++++ ...AuthorizedClientArgumentResolverTests.java | 135 +++++++- .../oauth2/core/AuthorizationGrantType.java | 1 + .../core/endpoint/OAuth2ParameterNames.java | 32 +- 12 files changed, 1081 insertions(+), 32 deletions(-) create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClient.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2ClientCredentialsGrantRequest.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClientTests.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2ClientCredentialsGrantRequestTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java index 982f660f6e9..baa697a0098 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java @@ -20,6 +20,7 @@ import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.method.annotation.OAuth2AuthorizedClientArgumentResolver; import org.springframework.util.ClassUtils; @@ -57,17 +58,26 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { @Configuration static class OAuth2ClientWebMvcSecurityConfiguration implements WebMvcConfigurer { + private ClientRegistrationRepository clientRegistrationRepository; private OAuth2AuthorizedClientRepository authorizedClientRepository; @Override public void addArgumentResolvers(List argumentResolvers) { - if (this.authorizedClientRepository != null) { + if (this.clientRegistrationRepository != null && this.authorizedClientRepository != null) { OAuth2AuthorizedClientArgumentResolver authorizedClientArgumentResolver = - new OAuth2AuthorizedClientArgumentResolver(this.authorizedClientRepository); + new OAuth2AuthorizedClientArgumentResolver( + this.clientRegistrationRepository, this.authorizedClientRepository); argumentResolvers.add(authorizedClientArgumentResolver); } } + @Autowired(required = false) + public void setClientRegistrationRepository(List clientRegistrationRepositories) { + if (clientRegistrationRepositories.size() == 1) { + this.clientRegistrationRepository = clientRegistrationRepositories.get(0); + } + } + @Autowired(required = false) public void setAuthorizedClientRepository(List authorizedClientRepositories) { if (authorizedClientRepositories.size() == 1) { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java index 524f4c57d86..eb15b4ef512 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java @@ -98,6 +98,11 @@ public String authorizedClient(@RegisteredOAuth2AuthorizedClient("client1") OAut } } + @Bean + public ClientRegistrationRepository clientRegistrationRepository() { + return mock(ClientRegistrationRepository.class); + } + @Bean public OAuth2AuthorizedClientRepository authorizedClientRepository() { return AUTHORIZED_CLIENT_REPOSITORY; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClient.java new file mode 100644 index 00000000000..f99409a276c --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClient.java @@ -0,0 +1,270 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.client.endpoint; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.IOException; +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * The default implementation of an {@link OAuth2AccessTokenResponseClient} + * for the {@link AuthorizationGrantType#CLIENT_CREDENTIALS client_credentials} grant. + * This implementation uses a {@link RestOperations} when requesting + * an access token credential at the Authorization Server's Token Endpoint. + * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AccessTokenResponseClient + * @see OAuth2ClientCredentialsGrantRequest + * @see OAuth2AccessTokenResponse + * @see Section 4.4.2 Access Token Request (Client Credentials Grant) + * @see Section 4.4.3 Access Token Response (Client Credentials Grant) + */ +public class DefaultClientCredentialsTokenResponseClient implements OAuth2AccessTokenResponseClient { + private static final String INVALID_TOKEN_REQUEST_ERROR_CODE = "invalid_token_request"; + + private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response"; + + private static final String[] TOKEN_RESPONSE_PARAMETER_NAMES = { + OAuth2ParameterNames.ACCESS_TOKEN, + OAuth2ParameterNames.TOKEN_TYPE, + OAuth2ParameterNames.EXPIRES_IN, + OAuth2ParameterNames.SCOPE, + OAuth2ParameterNames.REFRESH_TOKEN + }; + + private RestOperations restOperations; + + public DefaultClientCredentialsTokenResponseClient() { + RestTemplate restTemplate = new RestTemplate(); + // Disable the ResponseErrorHandler as errors are handled directly within this class + restTemplate.setErrorHandler(new NoOpResponseErrorHandler()); + this.restOperations = restTemplate; + } + + @Override + public OAuth2AccessTokenResponse getTokenResponse(OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) + throws OAuth2AuthenticationException { + + Assert.notNull(clientCredentialsGrantRequest, "clientCredentialsGrantRequest cannot be null"); + + // Build request + RequestEntity> request = this.buildRequest(clientCredentialsGrantRequest); + + // Exchange + ResponseEntity> response; + try { + response = this.restOperations.exchange( + request, new ParameterizedTypeReference>() {}); + } catch (Exception ex) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_REQUEST_ERROR_CODE, + "An error occurred while sending the Access Token Request: " + ex.getMessage(), null); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex); + } + + Map responseParameters = response.getBody(); + + // Check for Error Response + if (response.getStatusCodeValue() != 200) { + OAuth2Error oauth2Error = this.parseErrorResponse(responseParameters); + if (oauth2Error == null) { + oauth2Error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR); + } + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + + // Success Response + OAuth2AccessTokenResponse tokenResponse; + try { + tokenResponse = this.parseTokenResponse(responseParameters); + } catch (Exception ex) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE, + "An error occurred parsing the Access Token response (200 OK): " + ex.getMessage(), null); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex); + } + + if (tokenResponse == null) { + // This should never happen as long as the provider + // implements a Successful Response as defined in Section 5.1 + // https://tools.ietf.org/html/rfc6749#section-5.1 + OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE, + "An error occurred parsing the Access Token response (200 OK). " + + "Missing required parameters: access_token and/or token_type", null); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + + if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) { + // As per spec, in Section 5.1 Successful Access Token Response + // https://tools.ietf.org/html/rfc6749#section-5.1 + // If AccessTokenResponse.scope is empty, then default to the scope + // originally requested by the client in the Token Request + tokenResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse) + .scopes(clientCredentialsGrantRequest.getClientRegistration().getScopes()) + .build(); + } + + return tokenResponse; + } + + private RequestEntity> buildRequest(OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) { + HttpHeaders headers = this.buildHeaders(clientCredentialsGrantRequest); + MultiValueMap formParameters = this.buildFormParameters(clientCredentialsGrantRequest); + URI uri = UriComponentsBuilder.fromUriString(clientCredentialsGrantRequest.getClientRegistration().getProviderDetails().getTokenUri()) + .build() + .toUri(); + + return new RequestEntity<>(formParameters, headers, HttpMethod.POST, uri); + } + + private HttpHeaders buildHeaders(OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) { + ClientRegistration clientRegistration = clientCredentialsGrantRequest.getClientRegistration(); + + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + if (ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) { + headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret()); + } + + return headers; + } + + private MultiValueMap buildFormParameters(OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) { + ClientRegistration clientRegistration = clientCredentialsGrantRequest.getClientRegistration(); + + MultiValueMap formParameters = new LinkedMultiValueMap<>(); + formParameters.add(OAuth2ParameterNames.GRANT_TYPE, clientCredentialsGrantRequest.getGrantType().getValue()); + if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) { + formParameters.add(OAuth2ParameterNames.SCOPE, + StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), " ")); + } + if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) { + formParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId()); + formParameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret()); + } + + return formParameters; + } + + private OAuth2Error parseErrorResponse(Map responseParameters) { + if (CollectionUtils.isEmpty(responseParameters) || + !responseParameters.containsKey(OAuth2ParameterNames.ERROR)) { + return null; + } + + String errorCode = responseParameters.get(OAuth2ParameterNames.ERROR); + String errorDescription = responseParameters.get(OAuth2ParameterNames.ERROR_DESCRIPTION); + String errorUri = responseParameters.get(OAuth2ParameterNames.ERROR_URI); + + return new OAuth2Error(errorCode, errorDescription, errorUri); + } + + private OAuth2AccessTokenResponse parseTokenResponse(Map responseParameters) { + if (CollectionUtils.isEmpty(responseParameters) || + !responseParameters.containsKey(OAuth2ParameterNames.ACCESS_TOKEN) || + !responseParameters.containsKey(OAuth2ParameterNames.TOKEN_TYPE)) { + return null; + } + + String accessToken = responseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN); + + OAuth2AccessToken.TokenType accessTokenType = null; + if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase( + responseParameters.get(OAuth2ParameterNames.TOKEN_TYPE))) { + accessTokenType = OAuth2AccessToken.TokenType.BEARER; + } + + long expiresIn = 0; + if (responseParameters.containsKey(OAuth2ParameterNames.EXPIRES_IN)) { + try { + expiresIn = Long.valueOf(responseParameters.get(OAuth2ParameterNames.EXPIRES_IN)); + } catch (NumberFormatException ex) { } + } + + Set scopes = Collections.emptySet(); + if (responseParameters.containsKey(OAuth2ParameterNames.SCOPE)) { + String scope = responseParameters.get(OAuth2ParameterNames.SCOPE); + scopes = Arrays.stream(StringUtils.delimitedListToStringArray(scope, " ")).collect(Collectors.toSet()); + } + + Map additionalParameters = new LinkedHashMap<>(); + Set tokenResponseParameterNames = Stream.of(TOKEN_RESPONSE_PARAMETER_NAMES).collect(Collectors.toSet()); + responseParameters.entrySet().stream() + .filter(e -> !tokenResponseParameterNames.contains(e.getKey())) + .forEach(e -> additionalParameters.put(e.getKey(), e.getValue())); + + return OAuth2AccessTokenResponse.withToken(accessToken) + .tokenType(accessTokenType) + .expiresIn(expiresIn) + .scopes(scopes) + .additionalParameters(additionalParameters) + .build(); + } + + /** + * Sets the {@link RestOperations} used when requesting the access token response. + * + * @param restOperations the {@link RestOperations} used when requesting the access token response + */ + public final void setRestOperations(RestOperations restOperations) { + Assert.notNull(restOperations, "restOperations cannot be null"); + this.restOperations = restOperations; + } + + private static class NoOpResponseErrorHandler implements ResponseErrorHandler { + + @Override + public boolean hasError(ClientHttpResponse response) throws IOException { + return false; + } + + @Override + public void handleError(ClientHttpResponse response) throws IOException { + } + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2ClientCredentialsGrantRequest.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2ClientCredentialsGrantRequest.java new file mode 100644 index 00000000000..9f62c8671c9 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2ClientCredentialsGrantRequest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.client.endpoint; + +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.util.Assert; + +/** + * An OAuth 2.0 Client Credentials Grant request that holds + * the client's credentials in {@link #getClientRegistration()}. + * + * @author Joe Grandja + * @since 5.1 + * @see AbstractOAuth2AuthorizationGrantRequest + * @see ClientRegistration + * @see Section 1.3.4 Client Credentials Grant + */ +public class OAuth2ClientCredentialsGrantRequest extends AbstractOAuth2AuthorizationGrantRequest { + private final ClientRegistration clientRegistration; + + /** + * Constructs an {@code OAuth2ClientCredentialsGrantRequest} using the provided parameters. + * + * @param clientRegistration the client registration + */ + public OAuth2ClientCredentialsGrantRequest(ClientRegistration clientRegistration) { + super(AuthorizationGrantType.CLIENT_CREDENTIALS); + Assert.notNull(clientRegistration, "clientRegistration cannot be null"); + Assert.isTrue(AuthorizationGrantType.CLIENT_CREDENTIALS.equals(clientRegistration.getAuthorizationGrantType()), + "clientRegistration.authorizationGrantType must be AuthorizationGrantType.CLIENT_CREDENTIALS"); + this.clientRegistration = clientRegistration; + } + + /** + * Returns the {@link ClientRegistration client registration}. + * + * @return the {@link ClientRegistration} + */ + public ClientRegistration getClientRegistration() { + return this.clientRegistration; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java index 9783ff44c1f..f6342fc06ae 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java @@ -448,7 +448,9 @@ public Builder clientName(String clientName) { */ public ClientRegistration build() { Assert.notNull(this.authorizationGrantType, "authorizationGrantType cannot be null"); - if (AuthorizationGrantType.IMPLICIT.equals(this.authorizationGrantType)) { + if (AuthorizationGrantType.CLIENT_CREDENTIALS.equals(this.authorizationGrantType)) { + this.validateClientCredentialsGrantType(); + } else if (AuthorizationGrantType.IMPLICIT.equals(this.authorizationGrantType)) { this.validateImplicitGrantType(); } else { this.validateAuthorizationCodeGrantType(); @@ -507,5 +509,15 @@ private void validateImplicitGrantType() { Assert.hasText(this.authorizationUri, "authorizationUri cannot be empty"); Assert.hasText(this.clientName, "clientName cannot be empty"); } + + private void validateClientCredentialsGrantType() { + Assert.isTrue(AuthorizationGrantType.CLIENT_CREDENTIALS.equals(this.authorizationGrantType), + () -> "authorizationGrantType must be " + AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()); + Assert.hasText(this.registrationId, "registrationId cannot be empty"); + Assert.hasText(this.clientId, "clientId cannot be empty"); + Assert.hasText(this.clientSecret, "clientSecret cannot be empty"); + Assert.notNull(this.clientAuthenticationMethod, "clientAuthenticationMethod cannot be null"); + Assert.hasText(this.tokenUri, "tokenUri cannot be empty"); + } } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java index 67daf2955be..91d7db916ba 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java @@ -25,7 +25,14 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -34,6 +41,7 @@ import org.springframework.web.method.support.ModelAndViewContainer; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; /** * An implementation of a {@link HandlerMethodArgumentResolver} that is capable @@ -56,15 +64,22 @@ * @see RegisteredOAuth2AuthorizedClient */ public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMethodArgumentResolver { + private final ClientRegistrationRepository clientRegistrationRepository; private final OAuth2AuthorizedClientRepository authorizedClientRepository; + private OAuth2AccessTokenResponseClient clientCredentialsTokenResponseClient = + new DefaultClientCredentialsTokenResponseClient(); /** * Constructs an {@code OAuth2AuthorizedClientArgumentResolver} using the provided parameters. * - * @param authorizedClientRepository the authorized client repository + * @param clientRegistrationRepository the repository of client registrations + * @param authorizedClientRepository the repository of authorized clients */ - public OAuth2AuthorizedClientArgumentResolver(OAuth2AuthorizedClientRepository authorizedClientRepository) { + public OAuth2AuthorizedClientArgumentResolver(ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository) { + Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null"); + this.clientRegistrationRepository = clientRegistrationRepository; this.authorizedClientRepository = authorizedClientRepository; } @@ -83,8 +98,43 @@ public Object resolveArgument(MethodParameter parameter, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + String clientRegistrationId = this.resolveClientRegistrationId(parameter); + if (StringUtils.isEmpty(clientRegistrationId)) { + throw new IllegalArgumentException("Unable to resolve the Client Registration Identifier. " + + "It must be provided via @RegisteredOAuth2AuthorizedClient(\"client1\") or " + + "@RegisteredOAuth2AuthorizedClient(registrationId = \"client1\")."); + } + + Authentication principal = SecurityContextHolder.getContext().getAuthentication(); + HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + + OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient( + clientRegistrationId, principal, servletRequest); + if (authorizedClient != null) { + return authorizedClient; + } + + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId); + if (clientRegistration == null) { + return null; + } + + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { + throw new ClientAuthorizationRequiredException(clientRegistrationId); + } + + if (AuthorizationGrantType.CLIENT_CREDENTIALS.equals(clientRegistration.getAuthorizationGrantType())) { + HttpServletResponse servletResponse = webRequest.getNativeResponse(HttpServletResponse.class); + authorizedClient = this.authorizeClientCredentialsClient(clientRegistration, servletRequest, servletResponse); + } + + return authorizedClient; + } + + private String resolveClientRegistrationId(MethodParameter parameter) { RegisteredOAuth2AuthorizedClient authorizedClientAnnotation = AnnotatedElementUtils.findMergedAnnotation( parameter.getParameter(), RegisteredOAuth2AuthorizedClient.class); + Authentication principal = SecurityContextHolder.getContext().getAuthentication(); String clientRegistrationId = null; @@ -95,17 +145,41 @@ public Object resolveArgument(MethodParameter parameter, } else if (principal != null && OAuth2AuthenticationToken.class.isAssignableFrom(principal.getClass())) { clientRegistrationId = ((OAuth2AuthenticationToken) principal).getAuthorizedClientRegistrationId(); } - if (StringUtils.isEmpty(clientRegistrationId)) { - throw new IllegalArgumentException("Unable to resolve the Client Registration Identifier. " + - "It must be provided via @RegisteredOAuth2AuthorizedClient(\"client1\") or @RegisteredOAuth2AuthorizedClient(registrationId = \"client1\")."); - } - OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient( - clientRegistrationId, principal, webRequest.getNativeRequest(HttpServletRequest.class)); - if (authorizedClient == null) { - throw new ClientAuthorizationRequiredException(clientRegistrationId); - } + return clientRegistrationId; + } + + private OAuth2AuthorizedClient authorizeClientCredentialsClient(ClientRegistration clientRegistration, + HttpServletRequest request, HttpServletResponse response) { + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(clientRegistration); + OAuth2AccessTokenResponse tokenResponse = + this.clientCredentialsTokenResponseClient.getTokenResponse(clientCredentialsGrantRequest); + + Authentication principal = SecurityContextHolder.getContext().getAuthentication(); + + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + clientRegistration, + (principal != null ? principal.getName() : "anonymousUser"), + tokenResponse.getAccessToken()); + + this.authorizedClientRepository.saveAuthorizedClient( + authorizedClient, + principal, + request, + response); return authorizedClient; } + + /** + * Sets the client used when requesting an access token credential at the Token Endpoint for the {@code client_credentials} grant. + * + * @param clientCredentialsTokenResponseClient the client used when requesting an access token credential at the Token Endpoint for the {@code client_credentials} grant + */ + public final void setClientCredentialsTokenResponseClient( + OAuth2AccessTokenResponseClient clientCredentialsTokenResponseClient) { + Assert.notNull(clientCredentialsTokenResponseClient, "clientCredentialsTokenResponseClient cannot be null"); + this.clientCredentialsTokenResponseClient = clientCredentialsTokenResponseClient; + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClientTests.java new file mode 100644 index 00000000000..117a17cb340 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClientTests.java @@ -0,0 +1,326 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.client.endpoint; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; + +import java.time.Instant; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link DefaultClientCredentialsTokenResponseClient}. + * + * @author Joe Grandja + */ +public class DefaultClientCredentialsTokenResponseClientTests { + private DefaultClientCredentialsTokenResponseClient tokenResponseClient = new DefaultClientCredentialsTokenResponseClient(); + private ClientRegistration clientRegistration; + private MockWebServer server; + + @Before + public void setup() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + + String tokenUri = this.server.url("/oauth2/token").toString(); + + this.clientRegistration = ClientRegistration.withRegistrationId("registration-1") + .clientId("client-1") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .scope("read", "write") + .tokenUri(tokenUri) + .build(); + } + + @After + public void cleanup() throws Exception { + this.server.shutdown(); + } + + @Test + public void setRestOperationsWhenRestOperationsIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.tokenResponseClient.setRestOperations(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void getTokenResponseWhenRequestIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void getTokenResponseWhenSuccessResponseThenReturnAccessTokenResponse() throws Exception { + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\",\n" + + " \"scope\": \"read write\",\n" + + " \"custom_parameter_1\": \"custom-value-1\",\n" + + " \"custom_parameter_2\": \"custom-value-2\"\n" + + "}\n"; + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + + Instant expiresAtBefore = Instant.now().plusSeconds(3600); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(this.clientRegistration); + + OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest); + + Instant expiresAtAfter = Instant.now().plusSeconds(3600); + + RecordedRequest recordedRequest = this.server.takeRequest(); + assertThat(recordedRequest.getMethod()).isEqualTo(HttpMethod.POST.toString()); + assertThat(recordedRequest.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON.toString()); + assertThat(recordedRequest.getHeader(HttpHeaders.CONTENT_TYPE)).startsWith(MediaType.APPLICATION_FORM_URLENCODED.toString()); + + String formParameters = recordedRequest.getBody().readUtf8(); + assertThat(formParameters).contains("grant_type=client_credentials"); + assertThat(formParameters).contains("scope=read+write"); + + assertThat(accessTokenResponse.getAccessToken().getTokenValue()).isEqualTo("access-token-1234"); + assertThat(accessTokenResponse.getAccessToken().getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER); + assertThat(accessTokenResponse.getAccessToken().getExpiresAt()).isBetween(expiresAtBefore, expiresAtAfter); + assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read", "write"); + assertThat(accessTokenResponse.getRefreshToken()).isNull(); + assertThat(accessTokenResponse.getAdditionalParameters().size()).isEqualTo(2); + assertThat(accessTokenResponse.getAdditionalParameters()).containsEntry("custom_parameter_1", "custom-value-1"); + assertThat(accessTokenResponse.getAdditionalParameters()).containsEntry("custom_parameter_2", "custom-value-2"); + } + + @Test + public void getTokenResponseWhenClientAuthenticationBasicThenAuthorizationHeaderIsSent() throws Exception { + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\"\n" + + "}\n"; + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(this.clientRegistration); + + this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest); + + RecordedRequest recordedRequest = this.server.takeRequest(); + assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).startsWith("Basic "); + } + + @Test + public void getTokenResponseWhenClientAuthenticationPostThenFormParametersAreSent() throws Exception { + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\"\n" + + "}\n"; + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + + ClientRegistration clientRegistration = this.from(this.clientRegistration) + .clientAuthenticationMethod(ClientAuthenticationMethod.POST) + .build(); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(clientRegistration); + + this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest); + + RecordedRequest recordedRequest = this.server.takeRequest(); + assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isNull(); + + String formParameters = recordedRequest.getBody().readUtf8(); + assertThat(formParameters).contains("client_id=client-1"); + assertThat(formParameters).contains("client_secret=secret"); + } + + @Test + public void getTokenResponseWhenSuccessResponseAndNotBearerTokenTypeThenThrowOAuth2AuthenticationException() { + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"not-bearer\",\n" + + " \"expires_in\": \"3600\"\n" + + "}\n"; + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(this.clientRegistration); + + assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("[invalid_token_response] An error occurred parsing the Access Token response (200 OK): tokenType cannot be null"); + } + + @Test + public void getTokenResponseWhenSuccessResponseAndMissingTokenTypeParameterThenThrowOAuth2AuthenticationException() { + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\"\n" + + "}\n"; + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(this.clientRegistration); + + assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("[invalid_token_response] An error occurred parsing the Access Token response (200 OK). Missing required parameters: access_token and/or token_type"); + } + + @Test + public void getTokenResponseWhenSuccessResponseIncludesScopeThenAccessTokenHasResponseScope() { + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\",\n" + + " \"scope\": \"read\"\n" + + "}\n"; + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(this.clientRegistration); + + OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest); + + assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read"); + } + + @Test + public void getTokenResponseWhenSuccessResponseDoesNotIncludeScopeThenAccessTokenHasDefaultScope() { + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\"\n" + + "}\n"; + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(this.clientRegistration); + + OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest); + + assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read", "write"); + } + + @Test + public void getTokenResponseWhenTokenUriMalformedThenThrowOAuth2AuthenticationException() { + String malformedTokenUri = "http:\\provider.com\\oauth2\\token"; + ClientRegistration clientRegistration = this.from(this.clientRegistration) + .tokenUri(malformedTokenUri) + .build(); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(clientRegistration); + + assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("[invalid_token_request] An error occurred while sending the Access Token Request:"); + } + + @Test + public void getTokenResponseWhenTokenUriInvalidThenThrowOAuth2AuthenticationException() { + String invalidTokenUri = "http://invalid-provider.com/oauth2/token"; + ClientRegistration clientRegistration = this.from(this.clientRegistration) + .tokenUri(invalidTokenUri) + .build(); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(clientRegistration); + + assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("[invalid_token_request] An error occurred while sending the Access Token Request:"); + } + + @Test + public void getTokenResponseWhenMalformedResponseThenThrowOAuth2AuthenticationException() { + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\",\n" + + " \"scope\": \"read write\",\n" + + " \"custom_parameter_1\": \"custom-value-1\",\n" + + " \"custom_parameter_2\": \"custom-value-2\"\n"; +// "}\n"; // Make the JSON invalid/malformed + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(this.clientRegistration); + + assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("[invalid_token_request] An error occurred while sending the Access Token Request:"); + } + + @Test + public void getTokenResponseWhenErrorResponseThenThrowOAuth2AuthenticationException() { + String accessTokenErrorResponse = "{\n" + + " \"error\": \"unauthorized_client\"\n" + + "}\n"; + this.server.enqueue(jsonResponse(accessTokenErrorResponse).setResponseCode(400)); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(this.clientRegistration); + + assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("[unauthorized_client]"); + } + + @Test + public void getTokenResponseWhenServerErrorResponseThenThrowOAuth2AuthenticationException() { + this.server.enqueue(new MockResponse().setResponseCode(500)); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(this.clientRegistration); + + assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("[server_error]"); + } + + private MockResponse jsonResponse(String json) { + return new MockResponse() + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .setBody(json); + } + + private ClientRegistration.Builder from(ClientRegistration registration) { + return ClientRegistration.withRegistrationId(registration.getRegistrationId()) + .clientId(registration.getClientId()) + .clientSecret(registration.getClientSecret()) + .clientAuthenticationMethod(registration.getClientAuthenticationMethod()) + .authorizationGrantType(registration.getAuthorizationGrantType()) + .scope(registration.getScopes()) + .tokenUri(registration.getProviderDetails().getTokenUri()); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2ClientCredentialsGrantRequestTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2ClientCredentialsGrantRequestTests.java new file mode 100644 index 00000000000..47e90132482 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2ClientCredentialsGrantRequestTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.client.endpoint; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Java6Assertions.assertThatThrownBy; + +/** + * Tests for {@link OAuth2ClientCredentialsGrantRequest}. + * + * @author Joe Grandja + */ +public class OAuth2ClientCredentialsGrantRequestTests { + private ClientRegistration clientRegistration; + + @Before + public void setup() { + this.clientRegistration = ClientRegistration.withRegistrationId("registration-1") + .clientId("client-1") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .scope("read", "write") + .tokenUri("https://provider.com/oauth2/token") + .build(); + } + + @Test + public void constructorWhenClientRegistrationIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2ClientCredentialsGrantRequest(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenClientRegistrationInvalidGrantTypeThenThrowIllegalArgumentException() { + ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("registration-1") + .clientId("client-1") + .authorizationGrantType(AuthorizationGrantType.IMPLICIT) + .redirectUriTemplate("https://localhost:8080/redirect-uri") + .authorizationUri("https://provider.com/oauth2/auth") + .clientName("Client 1") + .build(); + + assertThatThrownBy(() -> new OAuth2ClientCredentialsGrantRequest(clientRegistration)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("clientRegistration.authorizationGrantType must be AuthorizationGrantType.CLIENT_CREDENTIALS"); + } + + @Test + public void constructorWhenValidParametersProvidedThenCreated() { + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(this.clientRegistration); + + assertThat(clientCredentialsGrantRequest.getClientRegistration()).isEqualTo(this.clientRegistration); + assertThat(clientCredentialsGrantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.CLIENT_CREDENTIALS); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java index 93a30b0505d..b1218d32959 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java @@ -25,6 +25,7 @@ import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for {@link ClientRegistration}. @@ -411,4 +412,91 @@ public void buildWhenOverrideRegistrationIdThenOverridden() { assertThat(registration.getRegistrationId()).isEqualTo(overriddenId); } + + @Test + public void buildWhenClientCredentialsGrantAllAttributesProvidedThenAllAttributesAreSet() { + ClientRegistration registration = ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .clientSecret(CLIENT_SECRET) + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .scope(SCOPES.toArray(new String[0])) + .tokenUri(TOKEN_URI) + .clientName(CLIENT_NAME) + .build(); + + assertThat(registration.getRegistrationId()).isEqualTo(REGISTRATION_ID); + assertThat(registration.getClientId()).isEqualTo(CLIENT_ID); + assertThat(registration.getClientSecret()).isEqualTo(CLIENT_SECRET); + assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); + assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.CLIENT_CREDENTIALS); + assertThat(registration.getScopes()).isEqualTo(SCOPES); + assertThat(registration.getProviderDetails().getTokenUri()).isEqualTo(TOKEN_URI); + assertThat(registration.getClientName()).isEqualTo(CLIENT_NAME); + } + + @Test + public void buildWhenClientCredentialsGrantRegistrationIdIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> + ClientRegistration.withRegistrationId(null) + .clientId(CLIENT_ID) + .clientSecret(CLIENT_SECRET) + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .tokenUri(TOKEN_URI) + .build() + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void buildWhenClientCredentialsGrantClientIdIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> + ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(null) + .clientSecret(CLIENT_SECRET) + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .tokenUri(TOKEN_URI) + .build() + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void buildWhenClientCredentialsGrantClientSecretIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> + ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .clientSecret(null) + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .tokenUri(TOKEN_URI) + .build() + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void buildWhenClientCredentialsGrantClientAuthenticationMethodIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> + ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .clientSecret(CLIENT_SECRET) + .clientAuthenticationMethod(null) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .tokenUri(TOKEN_URI) + .build() + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void buildWhenClientCredentialsGrantTokenUriIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> + ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .clientSecret(CLIENT_SECRET) + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .tokenUri(null) + .build() + ).isInstanceOf(IllegalArgumentException.class); + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java index d67d527da01..b49a1b2c01a 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java @@ -20,6 +20,7 @@ import org.junit.Test; import org.springframework.core.MethodParameter; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; @@ -27,7 +28,16 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.util.ReflectionUtils; import org.springframework.web.context.request.ServletWebRequest; @@ -38,8 +48,8 @@ import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; /** * Tests for {@link OAuth2AuthorizedClientArgumentResolver}. @@ -47,22 +57,58 @@ * @author Joe Grandja */ public class OAuth2AuthorizedClientArgumentResolverTests { + private TestingAuthenticationToken authentication; + private String principalName = "principal-1"; + private ClientRegistration registration1; + private ClientRegistration registration2; + private ClientRegistrationRepository clientRegistrationRepository; + private OAuth2AuthorizedClient authorizedClient1; + private OAuth2AuthorizedClient authorizedClient2; private OAuth2AuthorizedClientRepository authorizedClientRepository; private OAuth2AuthorizedClientArgumentResolver argumentResolver; - private OAuth2AuthorizedClient authorizedClient; private MockHttpServletRequest request; @Before public void setup() { - this.authorizedClientRepository = mock(OAuth2AuthorizedClientRepository.class); - this.argumentResolver = new OAuth2AuthorizedClientArgumentResolver(this.authorizedClientRepository); - this.authorizedClient = mock(OAuth2AuthorizedClient.class); - this.request = new MockHttpServletRequest(); - when(this.authorizedClientRepository.loadAuthorizedClient(anyString(), any(), any(HttpServletRequest.class))) - .thenReturn(this.authorizedClient); + this.authentication = new TestingAuthenticationToken(this.principalName, "password"); SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); - securityContext.setAuthentication(mock(Authentication.class)); + securityContext.setAuthentication(this.authentication); SecurityContextHolder.setContext(securityContext); + + this.registration1 = ClientRegistration.withRegistrationId("client1") + .clientId("client-1") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("user") + .authorizationUri("https://provider.com/oauth2/authorize") + .tokenUri("https://provider.com/oauth2/token") + .userInfoUri("https://provider.com/oauth2/user") + .userNameAttributeName("id") + .clientName("client-1") + .build(); + this.registration2 = ClientRegistration.withRegistrationId("client2") + .clientId("client-2") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .scope("read", "write") + .tokenUri("https://provider.com/oauth2/token") + .build(); + this.clientRegistrationRepository = new InMemoryClientRegistrationRepository(this.registration1, this.registration2); + this.authorizedClientRepository = mock(OAuth2AuthorizedClientRepository.class); + this.argumentResolver = new OAuth2AuthorizedClientArgumentResolver( + this.clientRegistrationRepository, this.authorizedClientRepository); + this.authorizedClient1 = new OAuth2AuthorizedClient(this.registration1, this.principalName, mock(OAuth2AccessToken.class)); + when(this.authorizedClientRepository.loadAuthorizedClient( + eq(this.registration1.getRegistrationId()), any(Authentication.class), any(HttpServletRequest.class))) + .thenReturn(this.authorizedClient1); + this.authorizedClient2 = new OAuth2AuthorizedClient(this.registration2, this.principalName, mock(OAuth2AccessToken.class)); + when(this.authorizedClientRepository.loadAuthorizedClient( + eq(this.registration2.getRegistrationId()), any(Authentication.class), any(HttpServletRequest.class))) + .thenReturn(this.authorizedClient2); + this.request = new MockHttpServletRequest(); } @After @@ -71,8 +117,20 @@ public void cleanup() { } @Test - public void constructorWhenOAuth2AuthorizedClientServiceIsNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2AuthorizedClientArgumentResolver(null)) + public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2AuthorizedClientArgumentResolver(null, this.authorizedClientRepository)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenOAuth2AuthorizedClientRepositoryIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2AuthorizedClientArgumentResolver(this.clientRegistrationRepository, null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void setClientCredentialsTokenResponseClientWhenClientIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.argumentResolver.setClientCredentialsTokenResponseClient(null)) .isInstanceOf(IllegalArgumentException.class); } @@ -101,7 +159,7 @@ public void supportsParameterWhenParameterTypeUnsupportedWithoutAnnotationThenFa } @Test - public void resolveArgumentWhenRegistrationIdEmptyAndNotOAuth2AuthenticationThenThrowIllegalArgumentException() throws Exception { + public void resolveArgumentWhenRegistrationIdEmptyAndNotOAuth2AuthenticationThenThrowIllegalArgumentException() { MethodParameter methodParameter = this.getMethodParameter("registrationIdEmpty", OAuth2AuthorizedClient.class); assertThatThrownBy(() -> this.argumentResolver.resolveArgument(methodParameter, null, null, null)) .isInstanceOf(IllegalArgumentException.class) @@ -116,18 +174,26 @@ public void resolveArgumentWhenRegistrationIdEmptyAndOAuth2AuthenticationThenRes securityContext.setAuthentication(authentication); SecurityContextHolder.setContext(securityContext); MethodParameter methodParameter = this.getMethodParameter("registrationIdEmpty", OAuth2AuthorizedClient.class); - this.argumentResolver.resolveArgument(methodParameter, null, new ServletWebRequest(this.request), null); + assertThat(this.argumentResolver.resolveArgument( + methodParameter, null, new ServletWebRequest(this.request), null)).isSameAs(this.authorizedClient1); } @Test - public void resolveArgumentWhenOAuth2AuthorizedClientFoundThenResolves() throws Exception { + public void resolveArgumentWhenAuthorizedClientFoundThenResolves() throws Exception { MethodParameter methodParameter = this.getMethodParameter("paramTypeAuthorizedClient", OAuth2AuthorizedClient.class); assertThat(this.argumentResolver.resolveArgument( - methodParameter, null, new ServletWebRequest(this.request), null)).isSameAs(this.authorizedClient); + methodParameter, null, new ServletWebRequest(this.request), null)).isSameAs(this.authorizedClient1); + } + + @Test + public void resolveArgumentWhenRegistrationIdInvalidThenDoesNotResolve() throws Exception { + MethodParameter methodParameter = this.getMethodParameter("registrationIdInvalid", OAuth2AuthorizedClient.class); + assertThat(this.argumentResolver.resolveArgument( + methodParameter, null, new ServletWebRequest(this.request), null)).isNull(); } @Test - public void resolveArgumentWhenOAuth2AuthorizedClientNotFoundThenThrowClientAuthorizationRequiredException() throws Exception { + public void resolveArgumentWhenAuthorizedClientNotFoundForAuthorizationCodeClientThenThrowClientAuthorizationRequiredException() { when(this.authorizedClientRepository.loadAuthorizedClient(anyString(), any(), any(HttpServletRequest.class))) .thenReturn(null); MethodParameter methodParameter = this.getMethodParameter("paramTypeAuthorizedClient", OAuth2AuthorizedClient.class); @@ -135,6 +201,35 @@ public void resolveArgumentWhenOAuth2AuthorizedClientNotFoundThenThrowClientAuth .isInstanceOf(ClientAuthorizationRequiredException.class); } + @SuppressWarnings("unchecked") + @Test + public void resolveArgumentWhenAuthorizedClientNotFoundForClientCredentialsClientThenResolvesFromTokenResponseClient() throws Exception { + OAuth2AccessTokenResponseClient clientCredentialsTokenResponseClient = + mock(OAuth2AccessTokenResponseClient.class); + this.argumentResolver.setClientCredentialsTokenResponseClient(clientCredentialsTokenResponseClient); + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse + .withToken("access-token-1234") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(3600) + .build(); + when(clientCredentialsTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); + + when(this.authorizedClientRepository.loadAuthorizedClient(anyString(), any(), any(HttpServletRequest.class))) + .thenReturn(null); + MethodParameter methodParameter = this.getMethodParameter("clientCredentialsClient", OAuth2AuthorizedClient.class); + + OAuth2AuthorizedClient authorizedClient = (OAuth2AuthorizedClient) this.argumentResolver.resolveArgument( + methodParameter, null, new ServletWebRequest(this.request), null); + + assertThat(authorizedClient).isNotNull(); + assertThat(authorizedClient.getClientRegistration()).isSameAs(this.registration2); + assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principalName); + assertThat(authorizedClient.getAccessToken()).isSameAs(accessTokenResponse.getAccessToken()); + + verify(this.authorizedClientRepository).saveAuthorizedClient( + eq(authorizedClient), eq(this.authentication), any(HttpServletRequest.class), eq(null)); + } + private MethodParameter getMethodParameter(String methodName, Class... paramTypes) { Method method = ReflectionUtils.findMethod(TestController.class, methodName, paramTypes); return new MethodParameter(method, 0); @@ -155,5 +250,11 @@ void paramTypeUnsupportedWithoutAnnotation(String param) { void registrationIdEmpty(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) { } + + void registrationIdInvalid(@RegisteredOAuth2AuthorizedClient("invalid") OAuth2AuthorizedClient authorizedClient) { + } + + void clientCredentialsClient(@RegisteredOAuth2AuthorizedClient("client2") OAuth2AuthorizedClient authorizedClient) { + } } } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java index 4e58a2c6f12..1a0af578062 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java @@ -38,6 +38,7 @@ public final class AuthorizationGrantType implements Serializable { public static final AuthorizationGrantType AUTHORIZATION_CODE = new AuthorizationGrantType("authorization_code"); public static final AuthorizationGrantType IMPLICIT = new AuthorizationGrantType("implicit"); public static final AuthorizationGrantType REFRESH_TOKEN = new AuthorizationGrantType("refresh_token"); + public static final AuthorizationGrantType CLIENT_CREDENTIALS = new AuthorizationGrantType("client_credentials"); private final String value; /** diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java index eed944b0169..c1061e7c2e3 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,11 @@ */ public interface OAuth2ParameterNames { + /** + * {@code grant_type} - used in Access Token Request. + */ + String GRANT_TYPE = "grant_type"; + /** * {@code response_type} - used in Authorization Request. */ @@ -35,6 +40,11 @@ public interface OAuth2ParameterNames { */ String CLIENT_ID = "client_id"; + /** + * {@code client_secret} - used in Access Token Request. + */ + String CLIENT_SECRET = "client_secret"; + /** * {@code redirect_uri} - used in Authorization Request and Access Token Request. */ @@ -55,6 +65,26 @@ public interface OAuth2ParameterNames { */ String CODE = "code"; + /** + * {@code access_token} - used in Authorization Response and Access Token Response. + */ + String ACCESS_TOKEN = "access_token"; + + /** + * {@code token_type} - used in Authorization Response and Access Token Response. + */ + String TOKEN_TYPE = "token_type"; + + /** + * {@code expires_in} - used in Authorization Response and Access Token Response. + */ + String EXPIRES_IN = "expires_in"; + + /** + * {@code refresh_token} - used in Access Token Request and Access Token Response. + */ + String REFRESH_TOKEN = "refresh_token"; + /** * {@code error} - used in Authorization Response and Access Token Response. */ From 0f090b28f8a276f3963da92cec672867e7804987 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Wed, 8 Aug 2018 09:32:09 -0400 Subject: [PATCH 213/226] Add OidcUserService.setOauth2UserService() Fixes gh-5604 --- .../client/oidc/userinfo/OidcUserService.java | 21 ++++++++++++++----- .../oidc/userinfo/OidcUserServiceTests.java | 11 +++++++++- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java index ae3c6505a0a..f08c5ac2007 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java @@ -33,6 +33,7 @@ import org.springframework.util.StringUtils; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -51,14 +52,14 @@ public class OidcUserService implements OAuth2UserService userInfoScopes = new HashSet<>( Arrays.asList(OidcScopes.PROFILE, OidcScopes.EMAIL, OidcScopes.ADDRESS, OidcScopes.PHONE)); - private final OAuth2UserService defaultUserService = new DefaultOAuth2UserService(); + private OAuth2UserService oauth2UserService = new DefaultOAuth2UserService(); @Override public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { Assert.notNull(userRequest, "userRequest cannot be null"); OidcUserInfo userInfo = null; if (this.shouldRetrieveUserInfo(userRequest)) { - OAuth2User oauth2User = this.defaultUserService.loadUser(userRequest); + OAuth2User oauth2User = this.oauth2UserService.loadUser(userRequest); userInfo = new OidcUserInfo(oauth2User.getAttributes()); // http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse @@ -81,9 +82,8 @@ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2Authenticatio } } - GrantedAuthority authority = new OidcUserAuthority(userRequest.getIdToken(), userInfo); - Set authorities = new HashSet<>(); - authorities.add(authority); + Set authorities = Collections.singleton( + new OidcUserAuthority(userRequest.getIdToken(), userInfo)); OidcUser user; @@ -121,4 +121,15 @@ private boolean shouldRetrieveUserInfo(OidcUserRequest userRequest) { return false; } + + /** + * Sets the {@link OAuth2UserService} used when requesting the user info resource. + * + * @since 5.1 + * @param oauth2UserService the {@link OAuth2UserService} used when requesting the user info resource. + */ + public final void setOauth2UserService(OAuth2UserService oauth2UserService) { + Assert.notNull(oauth2UserService, "oauth2UserService cannot be null"); + this.oauth2UserService = oauth2UserService; + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java index 1586292af63..e388a800fdf 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java @@ -18,7 +18,6 @@ import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -32,6 +31,7 @@ import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; @@ -51,6 +51,7 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -96,6 +97,14 @@ public void setUp() throws Exception { idTokenClaims.put(IdTokenClaimNames.SUB, "subject1"); when(this.idToken.getClaims()).thenReturn(idTokenClaims); when(this.idToken.getSubject()).thenReturn("subject1"); + + this.userService.setOauth2UserService(new DefaultOAuth2UserService()); + } + + @Test + public void setOauth2UserServiceWhenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.userService.setOauth2UserService(null)) + .isInstanceOf(IllegalArgumentException.class); } @Test From 47a8edc007e99e9427f0fb87638ec86ddbc20294 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Wed, 8 Aug 2018 13:20:45 -0400 Subject: [PATCH 214/226] Expose RestOperations in NimbusJwtDecoderJwkSupport Fixes gh-5603 --- .../jwt/NimbusJwtDecoderJwkSupport.java | 77 ++++++++++++++----- .../jwt/NimbusJwtDecoderJwkSupportTests.java | 74 ++++++++++-------- 2 files changed, 102 insertions(+), 49 deletions(-) diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java index 96df899fda6..dff7d9b95ad 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java @@ -15,13 +15,6 @@ */ package org.springframework.security.oauth2.jwt; -import java.net.MalformedURLException; -import java.net.URL; -import java.text.ParseException; -import java.time.Instant; -import java.util.LinkedHashMap; -import java.util.Map; - import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.RemoteKeySourceException; import com.nimbusds.jose.jwk.source.JWKSource; @@ -29,7 +22,7 @@ import com.nimbusds.jose.proc.JWSKeySelector; import com.nimbusds.jose.proc.JWSVerificationKeySelector; import com.nimbusds.jose.proc.SecurityContext; -import com.nimbusds.jose.util.DefaultResourceRetriever; +import com.nimbusds.jose.util.Resource; import com.nimbusds.jose.util.ResourceRetriever; import com.nimbusds.jwt.JWT; import com.nimbusds.jwt.JWTClaimsSet; @@ -37,12 +30,27 @@ import com.nimbusds.jwt.SignedJWT; import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; import com.nimbusds.jwt.proc.DefaultJWTProcessor; - +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; import org.springframework.util.Assert; +import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.text.ParseException; +import java.time.Instant; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; /** - * An implementation of a {@link JwtDecoder} that "decodes" a + * An implementation of a {@link JwtDecoder} that "decodes" a * JSON Web Token (JWT) and additionally verifies it's digital signature if the JWT is a * JSON Web Signature (JWS). The public key used for verification is obtained from the * JSON Web Key (JWK) Set {@code URL} supplied via the constructor. @@ -63,9 +71,9 @@ public final class NimbusJwtDecoderJwkSupport implements JwtDecoder { private static final String DECODING_ERROR_MESSAGE_TEMPLATE = "An error occurred while attempting to decode the Jwt: %s"; - private final URL jwkSetUrl; private final JWSAlgorithm jwsAlgorithm; private final ConfigurableJWTProcessor jwtProcessor; + private final RestOperationsResourceRetriever jwkSetRetriever = new RestOperationsResourceRetriever(); /** * Constructs a {@code NimbusJwtDecoderJwkSupport} using the provided parameters. @@ -85,18 +93,15 @@ public NimbusJwtDecoderJwkSupport(String jwkSetUrl) { public NimbusJwtDecoderJwkSupport(String jwkSetUrl, String jwsAlgorithm) { Assert.hasText(jwkSetUrl, "jwkSetUrl cannot be empty"); Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty"); + JWKSource jwkSource; try { - this.jwkSetUrl = new URL(jwkSetUrl); + jwkSource = new RemoteJWKSet(new URL(jwkSetUrl), this.jwkSetRetriever); } catch (MalformedURLException ex) { - throw new IllegalArgumentException("Invalid JWK Set URL " + jwkSetUrl + " : " + ex.getMessage(), ex); + throw new IllegalArgumentException("Invalid JWK Set URL \"" + jwkSetUrl + "\" : " + ex.getMessage(), ex); } this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm); - - ResourceRetriever jwkSetRetriever = new DefaultResourceRetriever(30000, 30000); - JWKSource jwkSource = new RemoteJWKSet(this.jwkSetUrl, jwkSetRetriever); JWSKeySelector jwsKeySelector = new JWSVerificationKeySelector<>(this.jwsAlgorithm, jwkSource); - this.jwtProcessor = new DefaultJWTProcessor<>(); this.jwtProcessor.setJWSKeySelector(jwsKeySelector); } @@ -104,10 +109,9 @@ public NimbusJwtDecoderJwkSupport(String jwkSetUrl, String jwsAlgorithm) { @Override public Jwt decode(String token) throws JwtException { JWT jwt = this.parse(token); - if ( jwt instanceof SignedJWT ) { + if (jwt instanceof SignedJWT) { return this.createJwt(token, jwt); } - throw new JwtException("Unsupported algorithm of " + jwt.getHeader().getAlgorithm()); } @@ -158,4 +162,39 @@ private Jwt createJwt(String token, JWT parsedJwt) { return jwt; } + + /** + * Sets the {@link RestOperations} used when requesting the JSON Web Key (JWK) Set. + * + * @since 5.1 + * @param restOperations the {@link RestOperations} used when requesting the JSON Web Key (JWK) Set + */ + public final void setRestOperations(RestOperations restOperations) { + Assert.notNull(restOperations, "restOperations cannot be null"); + this.jwkSetRetriever.restOperations = restOperations; + } + + private static class RestOperationsResourceRetriever implements ResourceRetriever { + private RestOperations restOperations = new RestTemplate(); + + @Override + public Resource retrieveResource(URL url) throws IOException { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8)); + + ResponseEntity response; + try { + RequestEntity request = new RequestEntity<>(headers, HttpMethod.GET, url.toURI()); + response = this.restOperations.exchange(request, String.class); + } catch (Exception ex) { + throw new IOException(ex); + } + + if (response.getStatusCodeValue() != 200) { + throw new IOException(response.toString()); + } + + return new Resource(response.getBody(), "UTF-8"); + } + } } diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java index 675445f634f..ecd648f513f 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,23 +24,22 @@ import com.nimbusds.jwt.proc.DefaultJWTProcessor; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; +import org.assertj.core.api.Assertions; import org.junit.Test; import org.junit.runner.RunWith; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; - +import org.springframework.http.RequestEntity; import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; +import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.mock; -import static org.powermock.api.mockito.PowerMockito.mockStatic; -import static org.powermock.api.mockito.PowerMockito.when; -import static org.powermock.api.mockito.PowerMockito.whenNew; +import static org.mockito.Mockito.verify; +import static org.powermock.api.mockito.PowerMockito.*; /** * Tests for {@link NimbusJwtDecoderJwkSupport}. @@ -62,6 +61,8 @@ public class NimbusJwtDecoderJwkSupportTests { private static final String MALFORMED_JWT = "eyJhbGciOiJSUzI1NiJ9.eyJuYmYiOnt9LCJleHAiOjQ2ODQyMjUwODd9.guoQvujdWvd3xw7FYQEn4D6-gzM_WqFvXdmvAUNSLbxG7fv2_LLCNujPdrBHJoYPbOwS1BGNxIKQWS1tylvqzmr1RohQ-RZ2iAM1HYQzboUlkoMkcd8ENM__ELqho8aNYBfqwkNdUOyBFoy7Syu_w2SoJADw2RTjnesKO6CVVa05bW118pDS4xWxqC4s7fnBjmZoTn4uQ-Kt9YSQZQk8YQxkJSiyanozzgyfgXULA6mPu1pTNU3FVFaK1i1av_xtH_zAPgb647ZeaNe4nahgqC5h8nhOlm8W2dndXbwAt29nd2ZWBsru_QwZz83XSKLhTPFz-mPBByZZDsyBbIHf9A"; private static final String UNSIGNED_JWT = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOi0yMDMzMjI0OTcsImp0aSI6IjEyMyIsInR5cCI6IkpXVCJ9."; + private NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(JWK_SET_URL, JWS_ALGORITHM); + @Test public void constructorWhenJwkSetUrlIsNullThenThrowIllegalArgumentException() { assertThatThrownBy(() -> new NimbusJwtDecoderJwkSupport(null)) @@ -80,10 +81,15 @@ public void constructorWhenJwsAlgorithmIsNullThenThrowIllegalArgumentException() .isInstanceOf(IllegalArgumentException.class); } + @Test + public void setRestOperationsWhenNullThenThrowIllegalArgumentException() { + Assertions.assertThatThrownBy(() -> this.jwtDecoder.setRestOperations(null)) + .isInstanceOf(IllegalArgumentException.class); + } + @Test public void decodeWhenJwtInvalidThenThrowJwtException() { - NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(JWK_SET_URL, JWS_ALGORITHM); - assertThatThrownBy(() -> jwtDecoder.decode("invalid")) + assertThatThrownBy(() -> this.jwtDecoder.decode("invalid")) .isInstanceOf(JwtException.class); } @@ -103,16 +109,14 @@ public void decodeWhenExpClaimNullThenDoesNotThrowException() throws Exception { JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().audience("resource1").build(); when(jwtProcessor.process(any(JWT.class), eq(null))).thenReturn(jwtClaimsSet); - NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(JWK_SET_URL, JWS_ALGORITHM); + NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(JWK_SET_URL); assertThatCode(() -> jwtDecoder.decode("encoded-jwt")).doesNotThrowAnyException(); } // gh-5457 @Test - public void decodeWhenPlainJwtThenExceptionDoesNotMentionClass() throws Exception { - NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(JWK_SET_URL, JWS_ALGORITHM); - - assertThatCode(() -> jwtDecoder.decode(UNSIGNED_JWT)) + public void decodeWhenPlainJwtThenExceptionDoesNotMentionClass() { + assertThatCode(() -> this.jwtDecoder.decode(UNSIGNED_JWT)) .isInstanceOf(JwtException.class) .hasMessageContaining("Unsupported algorithm of none"); } @@ -122,12 +126,11 @@ public void decodeWhenJwtIsMalformedThenReturnsStockException() throws Exception try ( MockWebServer server = new MockWebServer() ) { server.enqueue(new MockResponse().setBody(JWK_SET)); String jwkSetUrl = server.url("/.well-known/jwks.json").toString(); - - NimbusJwtDecoderJwkSupport decoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl); - - assertThatCode(() -> decoder.decode(MALFORMED_JWT)) + NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl); + assertThatCode(() -> jwtDecoder.decode(MALFORMED_JWT)) .isInstanceOf(JwtException.class) .hasMessage("An error occurred while attempting to decode the Jwt: Malformed payload"); + server.shutdown(); } } @@ -136,28 +139,39 @@ public void decodeWhenJwkResponseIsMalformedThenReturnsStockException() throws E try ( MockWebServer server = new MockWebServer() ) { server.enqueue(new MockResponse().setBody(MALFORMED_JWK_SET)); String jwkSetUrl = server.url("/.well-known/jwks.json").toString(); - - NimbusJwtDecoderJwkSupport decoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl); - - assertThatCode(() -> decoder.decode(SIGNED_JWT)) + NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl); + assertThatCode(() -> jwtDecoder.decode(SIGNED_JWT)) .isInstanceOf(JwtException.class) .hasMessage("An error occurred while attempting to decode the Jwt: Malformed Jwk set"); + server.shutdown(); } } @Test - public void decodeWhenJwkEndpointIsUnresponsiveThenRetrunsJwtException() throws Exception { + public void decodeWhenJwkEndpointIsUnresponsiveThenReturnsJwtException() throws Exception { try ( MockWebServer server = new MockWebServer() ) { server.enqueue(new MockResponse().setBody(MALFORMED_JWK_SET)); String jwkSetUrl = server.url("/.well-known/jwks.json").toString(); - - NimbusJwtDecoderJwkSupport decoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl); - - server.shutdown(); - - assertThatCode(() -> decoder.decode(SIGNED_JWT)) + NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl); + assertThatCode(() -> jwtDecoder.decode(SIGNED_JWT)) .isInstanceOf(JwtException.class) .hasMessageContaining("An error occurred while attempting to decode the Jwt"); + server.shutdown(); + } + } + + // gh-5603 + @Test + public void decodeWhenCustomRestOperationsSetThenUsed() throws Exception { + try ( MockWebServer server = new MockWebServer() ) { + server.enqueue(new MockResponse().setBody(JWK_SET)); + String jwkSetUrl = server.url("/.well-known/jwks.json").toString(); + NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl); + RestTemplate restTemplate = spy(new RestTemplate()); + jwtDecoder.setRestOperations(restTemplate); + assertThatCode(() -> jwtDecoder.decode(SIGNED_JWT)).doesNotThrowAnyException(); + verify(restTemplate).exchange(any(RequestEntity.class), eq(String.class)); + server.shutdown(); } } } From c635492e82191c8c34b29bef5484a371b9283bf3 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 8 Aug 2018 14:29:35 -0500 Subject: [PATCH 215/226] Move OAuth2ClientConfigurer.configure to AuthorizationCodeGrantConfigurer Issue: gh-5654 --- .../oauth2/client/OAuth2ClientConfigurer.java | 80 +++++++++---------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java index 60b81499d0e..3d93750c82f 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java @@ -265,6 +265,45 @@ public AuthorizationCodeGrantConfigurer and() { public OAuth2ClientConfigurer and() { return OAuth2ClientConfigurer.this; } + + private void configure(B builder) { + OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter; + + if (this.authorizationEndpointConfig.authorizationRequestResolver != null) { + authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter( + this.authorizationEndpointConfig.authorizationRequestResolver); + } else { + String authorizationRequestBaseUri = this.authorizationEndpointConfig.authorizationRequestBaseUri; + if (authorizationRequestBaseUri == null) { + authorizationRequestBaseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; + } + authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter( + OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder), authorizationRequestBaseUri); + } + + if (this.authorizationEndpointConfig.authorizationRequestRepository != null) { + authorizationRequestFilter.setAuthorizationRequestRepository( + this.authorizationEndpointConfig.authorizationRequestRepository); + } + RequestCache requestCache = builder.getSharedObject(RequestCache.class); + if (requestCache != null) { + authorizationRequestFilter.setRequestCache(requestCache); + } + builder.addFilter(postProcess(authorizationRequestFilter)); + + AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class); + + OAuth2AuthorizationCodeGrantFilter authorizationCodeGrantFilter = new OAuth2AuthorizationCodeGrantFilter( + OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder), + OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(builder), + authenticationManager); + + if (this.authorizationEndpointConfig.authorizationRequestRepository != null) { + authorizationCodeGrantFilter.setAuthorizationRequestRepository( + this.authorizationEndpointConfig.authorizationRequestRepository); + } + builder.addFilter(postProcess(authorizationCodeGrantFilter)); + } } @Override @@ -277,7 +316,7 @@ public void init(B builder) throws Exception { @Override public void configure(B builder) throws Exception { if (this.authorizationCodeGrantConfigurer != null) { - this.configure(builder, this.authorizationCodeGrantConfigurer); + this.authorizationCodeGrantConfigurer.configure(builder); } } @@ -292,43 +331,4 @@ private void init(B builder, AuthorizationCodeGrantConfigurer authorizationCodeG new OAuth2AuthorizationCodeAuthenticationProvider(accessTokenResponseClient); builder.authenticationProvider(this.postProcess(authorizationCodeAuthenticationProvider)); } - - private void configure(B builder, AuthorizationCodeGrantConfigurer authorizationCodeGrantConfigurer) throws Exception { - OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter; - - if (authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestResolver != null) { - authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter( - authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestResolver); - } else { - String authorizationRequestBaseUri = authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestBaseUri; - if (authorizationRequestBaseUri == null) { - authorizationRequestBaseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; - } - authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter( - OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder), authorizationRequestBaseUri); - } - - if (authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestRepository != null) { - authorizationRequestFilter.setAuthorizationRequestRepository( - authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestRepository); - } - RequestCache requestCache = builder.getSharedObject(RequestCache.class); - if (requestCache != null) { - authorizationRequestFilter.setRequestCache(requestCache); - } - builder.addFilter(this.postProcess(authorizationRequestFilter)); - - AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class); - - OAuth2AuthorizationCodeGrantFilter authorizationCodeGrantFilter = new OAuth2AuthorizationCodeGrantFilter( - OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder), - OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(builder), - authenticationManager); - - if (authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestRepository != null) { - authorizationCodeGrantFilter.setAuthorizationRequestRepository( - authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestRepository); - } - builder.addFilter(this.postProcess(authorizationCodeGrantFilter)); - } } From d464d2b4015072de57405c64b1080a1e1504e5c4 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 8 Aug 2018 14:55:21 -0500 Subject: [PATCH 216/226] Create AuthorizationEndpointConfig.configure Issue: gh-5654 --- .../oauth2/client/OAuth2ClientConfigurer.java | 86 +++++++++++-------- 1 file changed, 49 insertions(+), 37 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java index 3d93750c82f..5ff7e2975c6 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java @@ -26,6 +26,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; @@ -160,7 +161,7 @@ public AuthorizationEndpointConfig authorizationEndpoint() { * Configuration options for the Authorization Server's Authorization Endpoint. */ public class AuthorizationEndpointConfig { - private String authorizationRequestBaseUri; + private String authorizationRequestBaseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; private OAuth2AuthorizationRequestResolver authorizationRequestResolver; private AuthorizationRequestRepository authorizationRequestRepository; @@ -213,6 +214,52 @@ public AuthorizationEndpointConfig authorizationRequestRepository( public AuthorizationCodeGrantConfigurer and() { return AuthorizationCodeGrantConfigurer.this; } + + private OAuth2AuthorizationRequestResolver getAuthorizationRequestResolver() { + if (this.authorizationRequestResolver != null) { + return this.authorizationRequestResolver; + } + ClientRegistrationRepository clientRegistrationRepository = OAuth2ClientConfigurerUtils + .getClientRegistrationRepository(getBuilder()); + return new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, + this.authorizationRequestBaseUri); + } + + private OAuth2AuthorizationRequestRedirectFilter createAuthorizationRequestRedirectFilter(B builder) { + OAuth2AuthorizationRequestResolver resolver = getAuthorizationRequestResolver(); + OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter(resolver); + + if (this.authorizationRequestRepository != null) { + authorizationRequestFilter.setAuthorizationRequestRepository( + this.authorizationRequestRepository); + } + RequestCache requestCache = builder.getSharedObject(RequestCache.class); + if (requestCache != null) { + authorizationRequestFilter.setRequestCache(requestCache); + } + return authorizationRequestFilter; + } + + private OAuth2AuthorizationCodeGrantFilter createAuthorizationCodeGrantFilter(B builder) { + AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class); + OAuth2AuthorizationCodeGrantFilter authorizationCodeGrantFilter = new OAuth2AuthorizationCodeGrantFilter( + OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder), + OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(builder), + authenticationManager); + + if (this.authorizationRequestRepository != null) { + authorizationCodeGrantFilter.setAuthorizationRequestRepository( + this.authorizationRequestRepository); + } + return authorizationCodeGrantFilter; + } + + private void configure(B builder) { + OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter = createAuthorizationRequestRedirectFilter(builder); + builder.addFilter(postProcess(authorizationRequestFilter)); + OAuth2AuthorizationCodeGrantFilter authorizationCodeGrantFilter = createAuthorizationCodeGrantFilter(builder); + builder.addFilter(postProcess(authorizationCodeGrantFilter)); + } } /** @@ -267,42 +314,7 @@ public OAuth2ClientConfigurer and() { } private void configure(B builder) { - OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter; - - if (this.authorizationEndpointConfig.authorizationRequestResolver != null) { - authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter( - this.authorizationEndpointConfig.authorizationRequestResolver); - } else { - String authorizationRequestBaseUri = this.authorizationEndpointConfig.authorizationRequestBaseUri; - if (authorizationRequestBaseUri == null) { - authorizationRequestBaseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; - } - authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter( - OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder), authorizationRequestBaseUri); - } - - if (this.authorizationEndpointConfig.authorizationRequestRepository != null) { - authorizationRequestFilter.setAuthorizationRequestRepository( - this.authorizationEndpointConfig.authorizationRequestRepository); - } - RequestCache requestCache = builder.getSharedObject(RequestCache.class); - if (requestCache != null) { - authorizationRequestFilter.setRequestCache(requestCache); - } - builder.addFilter(postProcess(authorizationRequestFilter)); - - AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class); - - OAuth2AuthorizationCodeGrantFilter authorizationCodeGrantFilter = new OAuth2AuthorizationCodeGrantFilter( - OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder), - OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(builder), - authenticationManager); - - if (this.authorizationEndpointConfig.authorizationRequestRepository != null) { - authorizationCodeGrantFilter.setAuthorizationRequestRepository( - this.authorizationEndpointConfig.authorizationRequestRepository); - } - builder.addFilter(postProcess(authorizationCodeGrantFilter)); + this.authorizationEndpointConfig.configure(builder); } } From e85e3070a199f8b4c7c92fc774283fa1fac59718 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Tue, 7 Aug 2018 01:26:57 +0900 Subject: [PATCH 217/226] Replace isEqualTo(null) with isNull() --- .../security/acls/jdbc/EhCacheBasedAclCacheTests.java | 4 ++-- .../cas/authentication/CasAuthenticationProviderTests.java | 4 ++-- .../security/config/FilterChainProxyConfigTests.java | 2 +- .../security/access/intercept/RunAsManagerImplTests.java | 2 +- .../authentication/jaas/SecurityContextLoginModuleTests.java | 2 +- .../security/oauth2/core/ClaimAccessorTests.java | 2 +- .../security/openid/OpenIDAuthenticationProviderTests.java | 2 +- .../security/taglibs/authz/AuthenticationTagTests.java | 2 +- .../SimpleUrlAuthenticationSuccessHandlerTests.java | 2 +- .../AbstractPreAuthenticatedProcessingFilterTests.java | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/acl/src/test/java/org/springframework/security/acls/jdbc/EhCacheBasedAclCacheTests.java b/acl/src/test/java/org/springframework/security/acls/jdbc/EhCacheBasedAclCacheTests.java index cb97097677f..365e882c498 100644 --- a/acl/src/test/java/org/springframework/security/acls/jdbc/EhCacheBasedAclCacheTests.java +++ b/acl/src/test/java/org/springframework/security/acls/jdbc/EhCacheBasedAclCacheTests.java @@ -154,11 +154,11 @@ public void testDiskSerializationOfMutableAclObjectInstance() throws Exception { Object retrieved1 = FieldUtils.getProtectedFieldValue("aclAuthorizationStrategy", retrieved); - assertThat(retrieved1).isEqualTo(null); + assertThat(retrieved1).isNull(); Object retrieved2 = FieldUtils.getProtectedFieldValue( "permissionGrantingStrategy", retrieved); - assertThat(retrieved2).isEqualTo(null); + assertThat(retrieved2).isNull(); } @Test diff --git a/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java b/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java index 389191fb2ce..c45cf91e438 100644 --- a/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java +++ b/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java @@ -353,7 +353,7 @@ public void ignoresClassesItDoesNotSupport() throws Exception { assertThat(cap.supports(TestingAuthenticationToken.class)).isFalse(); // Try it anyway - assertThat(cap.authenticate(token)).isEqualTo(null); + assertThat(cap.authenticate(token)).isNull(); } @Test @@ -370,7 +370,7 @@ public void ignoresUsernamePasswordAuthenticationTokensWithoutCasIdentifiersAsPr UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "some_normal_user", "password", AuthorityUtils.createAuthorityList("ROLE_A")); - assertThat(cap.authenticate(token)).isEqualTo(null); + assertThat(cap.authenticate(token)).isNull(); } @Test diff --git a/config/src/test/java/org/springframework/security/config/FilterChainProxyConfigTests.java b/config/src/test/java/org/springframework/security/config/FilterChainProxyConfigTests.java index 2ed1916a9e4..d18d95009d2 100644 --- a/config/src/test/java/org/springframework/security/config/FilterChainProxyConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/FilterChainProxyConfigTests.java @@ -108,7 +108,7 @@ public void normalOperationWithNewConfigNonNamespace() throws Exception { public void pathWithNoMatchHasNoFilters() throws Exception { FilterChainProxy filterChainProxy = appCtx.getBean( "newFilterChainProxyNoDefaultPath", FilterChainProxy.class); - assertThat(filterChainProxy.getFilters("/nomatch")).isEqualTo(null); + assertThat(filterChainProxy.getFilters("/nomatch")).isNull(); } // SEC-1235 diff --git a/core/src/test/java/org/springframework/security/access/intercept/RunAsManagerImplTests.java b/core/src/test/java/org/springframework/security/access/intercept/RunAsManagerImplTests.java index 1ea4dbb6733..fac5c1e8692 100644 --- a/core/src/test/java/org/springframework/security/access/intercept/RunAsManagerImplTests.java +++ b/core/src/test/java/org/springframework/security/access/intercept/RunAsManagerImplTests.java @@ -52,7 +52,7 @@ public void testDoesNotReturnAdditionalAuthoritiesIfCalledWithoutARunAsSetting() Authentication resultingToken = runAs.buildRunAs(inputToken, new Object(), SecurityConfig.createList("SOMETHING_WE_IGNORE")); - assertThat(resultingToken).isEqualTo(null); + assertThat(resultingToken).isNull(); } @Test diff --git a/core/src/test/java/org/springframework/security/authentication/jaas/SecurityContextLoginModuleTests.java b/core/src/test/java/org/springframework/security/authentication/jaas/SecurityContextLoginModuleTests.java index 9cd71b9923a..53beac791ac 100644 --- a/core/src/test/java/org/springframework/security/authentication/jaas/SecurityContextLoginModuleTests.java +++ b/core/src/test/java/org/springframework/security/authentication/jaas/SecurityContextLoginModuleTests.java @@ -103,7 +103,7 @@ public void testLogout() throws Exception { this.module.login(); assertThat(this.module.logout()).as("Should return true as it succeeds").isTrue(); assertThat(this.module.getAuthentication()).as("Authentication should be null") - .isEqualTo(null); + .isNull(); assertThat(this.subject.getPrincipals().contains(this.auth)) .withFailMessage( diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ClaimAccessorTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ClaimAccessorTests.java index 2db9e7df975..dc89d1dbe63 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ClaimAccessorTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ClaimAccessorTests.java @@ -99,6 +99,6 @@ public void getClaimAsStringWhenValueIsNullThenReturnNull() { String claimName = "claim-with-null-value"; this.claims.put(claimName, null); - assertThat(this.claimAccessor.getClaimAsString(claimName)).isEqualTo(null); + assertThat(this.claimAccessor.getClaimAsString(claimName)).isNull(); } } diff --git a/openid/src/test/java/org/springframework/security/openid/OpenIDAuthenticationProviderTests.java b/openid/src/test/java/org/springframework/security/openid/OpenIDAuthenticationProviderTests.java index fefb628c3b1..ba613a8cc7a 100644 --- a/openid/src/test/java/org/springframework/security/openid/OpenIDAuthenticationProviderTests.java +++ b/openid/src/test/java/org/springframework/security/openid/OpenIDAuthenticationProviderTests.java @@ -216,7 +216,7 @@ public void testIgnoresUserPassAuthToken() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( USERNAME, "password"); - assertThat(provider.authenticate(token)).isEqualTo(null); + assertThat(provider.authenticate(token)).isNull(); } /* diff --git a/taglibs/src/test/java/org/springframework/security/taglibs/authz/AuthenticationTagTests.java b/taglibs/src/test/java/org/springframework/security/taglibs/authz/AuthenticationTagTests.java index 3c016df31a1..a07548783b2 100644 --- a/taglibs/src/test/java/org/springframework/security/taglibs/authz/AuthenticationTagTests.java +++ b/taglibs/src/test/java/org/springframework/security/taglibs/authz/AuthenticationTagTests.java @@ -101,7 +101,7 @@ public void testOperationWhenSecurityContextIsNull() throws Exception { authenticationTag.setProperty("principal"); assertThat(authenticationTag.doStartTag()).isEqualTo(Tag.SKIP_BODY); assertThat(authenticationTag.doEndTag()).isEqualTo(Tag.EVAL_PAGE); - assertThat(authenticationTag.getLastMessage()).isEqualTo(null); + assertThat(authenticationTag.getLastMessage()).isNull(); } @Test diff --git a/web/src/test/java/org/springframework/security/web/authentication/SimpleUrlAuthenticationSuccessHandlerTests.java b/web/src/test/java/org/springframework/security/web/authentication/SimpleUrlAuthenticationSuccessHandlerTests.java index cde2a256045..ae999320150 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/SimpleUrlAuthenticationSuccessHandlerTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/SimpleUrlAuthenticationSuccessHandlerTests.java @@ -108,7 +108,7 @@ public void setTargetUrlParameterNullTargetUrlParameter() { SimpleUrlAuthenticationSuccessHandler ash = new SimpleUrlAuthenticationSuccessHandler(); ash.setTargetUrlParameter("targetUrl"); ash.setTargetUrlParameter(null); - assertThat(ash.getTargetUrlParameter()).isEqualTo(null); + assertThat(ash.getTargetUrlParameter()).isNull(); } @Test diff --git a/web/src/test/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilterTests.java index 2ce52fa04c9..8989d388dae 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilterTests.java @@ -147,7 +147,7 @@ public void nullPreAuthenticationClearsPreviousUser() throws Exception { filter.doFilter(new MockHttpServletRequest(), new MockHttpServletResponse(), new MockFilterChain()); - assertThat(SecurityContextHolder.getContext().getAuthentication()).isEqualTo(null); + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); } @Test From db13a2b97395207641c114a5e68bb4d2eacfd139 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 23 Jul 2018 14:59:25 -0600 Subject: [PATCH 218/226] RememberMeConfigTests groovy->java Issue: gh-4939 --- .../config/http/RememberMeConfigTests.groovy | 312 --------------- .../config/http/RememberMeConfigTests.java | 378 ++++++++++++++++++ .../RememberMeConfigTests-DefaultConfig.xml | 37 ++ ...berMeConfigTests-NegativeTokenValidity.xml | 39 ++ ...ts-NegativeTokenValidityWithDataSource.xml | 40 ++ ...eTokenValidityWithPersistentRepository.xml | 39 ++ .../http/RememberMeConfigTests-Sec1827.xml | 39 ++ .../http/RememberMeConfigTests-Sec2165.xml | 39 ++ .../http/RememberMeConfigTests-Sec742.xml | 40 ++ .../RememberMeConfigTests-SecureCookie.xml | 39 ++ .../RememberMeConfigTests-TokenValidity.xml | 39 ++ ...Tests-WithAuthenticationSuccessHandler.xml | 46 +++ .../RememberMeConfigTests-WithDataSource.xml | 41 ++ ...mberMeConfigTests-WithRememberMeCookie.xml | 33 ++ ...sts-WithRememberMeCookieAndServicesRef.xml | 37 ++ ...rMeConfigTests-WithRememberMeParameter.xml | 37 ++ ...-WithRememberMeParameterAndServicesRef.xml | 37 ++ .../RememberMeConfigTests-WithServicesRef.xml | 47 +++ ...emberMeConfigTests-WithTokenRepository.xml | 51 +++ ...erMeConfigTests-WithUserDetailsService.xml | 43 ++ .../security/config/http/userservice.xml | 2 +- 21 files changed, 1102 insertions(+), 313 deletions(-) delete mode 100644 config/src/test/groovy/org/springframework/security/config/http/RememberMeConfigTests.groovy create mode 100644 config/src/test/java/org/springframework/security/config/http/RememberMeConfigTests.java create mode 100644 config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-DefaultConfig.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-NegativeTokenValidity.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-NegativeTokenValidityWithDataSource.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-NegativeTokenValidityWithPersistentRepository.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-Sec1827.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-Sec2165.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-Sec742.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-SecureCookie.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-TokenValidity.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithAuthenticationSuccessHandler.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithDataSource.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeCookie.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeCookieAndServicesRef.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeParameter.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeParameterAndServicesRef.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithServicesRef.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithTokenRepository.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithUserDetailsService.xml diff --git a/config/src/test/groovy/org/springframework/security/config/http/RememberMeConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/RememberMeConfigTests.groovy deleted file mode 100644 index 7217cd11662..00000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/RememberMeConfigTests.groovy +++ /dev/null @@ -1,312 +0,0 @@ -/* - * Copyright 2002-2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.config.http - -import static org.springframework.security.config.ConfigTestUtils.AUTH_PROVIDER_XML - -import javax.sql.DataSource - -import org.springframework.beans.FatalBeanException -import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer -import org.springframework.beans.factory.parsing.BeanDefinitionParsingException -import org.springframework.security.TestDataSource -import org.springframework.security.authentication.ProviderManager -import org.springframework.security.authentication.RememberMeAuthenticationProvider -import org.springframework.security.core.userdetails.MockUserDetailsService -import org.springframework.security.util.FieldUtils -import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler -import org.springframework.security.web.authentication.logout.LogoutFilter -import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler -import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices -import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl -import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl -import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices -import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter -import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices - -/** - * - * @author Luke Taylor - * @author Rob Winch - * @author Oliver Becker - */ -class RememberMeConfigTests extends AbstractHttpConfigTests { - - def rememberMeServiceWorksWithTokenRepoRef() { - httpAutoConfig () { - 'remember-me'('token-repository-ref': 'tokenRepo') - } - bean('tokenRepo', CustomTokenRepository.class.name) - - createAppContext(AUTH_PROVIDER_XML) - - def rememberMeServices = rememberMeServices() - - expect: - rememberMeServices instanceof PersistentTokenBasedRememberMeServices - rememberMeServices.tokenRepository instanceof CustomTokenRepository - FieldUtils.getFieldValue(rememberMeServices, "useSecureCookie") == null - } - - def rememberMeServiceWorksWithDataSourceRef() { - httpAutoConfig () { - 'remember-me'('data-source-ref': 'ds') - } - bean('ds', TestDataSource.class.name, ['tokendb']) - - createAppContext(AUTH_PROVIDER_XML) - - def rememberMeServices = rememberMeServices() - - expect: - rememberMeServices instanceof PersistentTokenBasedRememberMeServices - rememberMeServices.tokenRepository instanceof JdbcTokenRepositoryImpl - } - - def rememberMeServiceWorksWithAuthenticationSuccessHandlerRef() { - httpAutoConfig () { - 'remember-me'('authentication-success-handler-ref': 'sh') - } - bean('sh', SimpleUrlAuthenticationSuccessHandler.class.name, ['/target']) - - createAppContext(AUTH_PROVIDER_XML) - - expect: - getFilter(RememberMeAuthenticationFilter.class).successHandler instanceof SimpleUrlAuthenticationSuccessHandler - } - - def rememberMeServiceWorksWithExternalServicesImpl() { - httpAutoConfig () { - 'remember-me'('key': "#{'our' + 'key'}", 'services-ref': 'rms') - csrf(disabled:true) - } - xml.'b:bean'(id: 'rms', 'class': TokenBasedRememberMeServices.class.name) { - 'b:constructor-arg'(value: 'ourKey') - 'b:constructor-arg'(ref: 'us') - 'b:property'(name: 'tokenValiditySeconds', value: '5000') - } - - createAppContext(AUTH_PROVIDER_XML) - - List logoutHandlers = FieldUtils.getFieldValue(getFilter(LogoutFilter.class), "handler").logoutHandlers; - Map ams = appContext.getBeansOfType(ProviderManager.class); - ProviderManager am = (ams.values() as List).find { it instanceof ProviderManager && it.providers.size() == 2} - RememberMeAuthenticationProvider rmp = am.providers.find { it instanceof RememberMeAuthenticationProvider} - - expect: - rmp != null - 5000 == FieldUtils.getFieldValue(rememberMeServices(), "tokenValiditySeconds") - // SEC-909 - logoutHandlers.size() == 2 - logoutHandlers.get(1) == rememberMeServices() - // SEC-1281 - rmp.key == "ourkey" - } - - def rememberMeAddsLogoutHandlerToLogoutFilter() { - httpAutoConfig () { - 'remember-me'() - csrf(disabled:true) - } - createAppContext(AUTH_PROVIDER_XML) - - def rememberMeServices = rememberMeServices() - List logoutHandlers = getFilter(LogoutFilter.class).handler.logoutHandlers - - expect: - rememberMeServices - logoutHandlers.size() == 2 - logoutHandlers.get(0) instanceof SecurityContextLogoutHandler - logoutHandlers.get(1) == rememberMeServices - } - - def rememberMeTokenValidityIsParsedCorrectly() { - httpAutoConfig () { - 'remember-me'('key': 'ourkey', 'token-validity-seconds':'10000') - } - - createAppContext(AUTH_PROVIDER_XML) - - def rememberMeServices = rememberMeServices() - def rememberMeFilter = getFilter(RememberMeAuthenticationFilter.class) - - expect: - rememberMeFilter.authenticationManager - rememberMeServices.key == 'ourkey' - rememberMeServices.tokenValiditySeconds == 10000 - rememberMeServices.userDetailsService - } - - def 'Remember-me token validity allows negative value for non-persistent implementation'() { - httpAutoConfig () { - 'remember-me'('key': 'ourkey', 'token-validity-seconds':'-1') - } - - createAppContext(AUTH_PROVIDER_XML) - expect: - rememberMeServices().tokenValiditySeconds == -1 - } - - def 'remember-me@token-validity-seconds denies for persistent implementation'() { - setup: - httpAutoConfig () { - 'remember-me'('key': 'ourkey', 'token-validity-seconds':'-1', 'dataSource' : 'dataSource') - } - mockBean(DataSource) - when: - createAppContext(AUTH_PROVIDER_XML) - then: - thrown(FatalBeanException) - } - - def 'SEC-2165: remember-me@token-validity-seconds allows property placeholders'() { - when: - httpAutoConfig () { - 'remember-me'('key': 'ourkey', 'token-validity-seconds':'${security.rememberme.ttl}') - } - xml.'b:bean'(class: PropertyPlaceholderConfigurer.name) { - 'b:property'(name:'properties', value:'security.rememberme.ttl=30') - } - - createAppContext(AUTH_PROVIDER_XML) - then: - rememberMeServices().tokenValiditySeconds == 30 - } - - def rememberMeSecureCookieAttributeIsSetCorrectly() { - httpAutoConfig () { - 'remember-me'('key': 'ourkey', 'use-secure-cookie':'true') - } - - createAppContext(AUTH_PROVIDER_XML) - expect: - FieldUtils.getFieldValue(rememberMeServices(), "useSecureCookie") - } - - // SEC-1827 - def rememberMeSecureCookieAttributeFalse() { - httpAutoConfig () { - 'remember-me'('key': 'ourkey', 'use-secure-cookie':'false') - } - - createAppContext(AUTH_PROVIDER_XML) - expect: 'useSecureCookie is false' - FieldUtils.getFieldValue(rememberMeServices(), "useSecureCookie") == Boolean.FALSE - } - - def 'Negative token-validity is rejected with persistent implementation'() { - when: - httpAutoConfig () { - 'remember-me'('key': 'ourkey', 'token-validity-seconds':'-1', 'token-repository-ref': 'tokenRepo') - } - bean('tokenRepo', InMemoryTokenRepositoryImpl.class.name) - createAppContext(AUTH_PROVIDER_XML) - - then: - BeanDefinitionParsingException e = thrown() - } - - def 'Custom user service is supported'() { - when: - httpAutoConfig () { - 'remember-me'('key': 'ourkey', 'token-validity-seconds':'-1', 'user-service-ref': 'userService') - } - bean('userService', MockUserDetailsService.class.name) - createAppContext(AUTH_PROVIDER_XML) - - then: "Parses OK" - notThrown BeanDefinitionParsingException - } - - // SEC-742 - def rememberMeWorksWithoutBasicProcessingFilter() { - when: - xml.http () { - 'form-login'('login-page': '/login.jsp', 'default-target-url': '/messageList.html' ) - logout('logout-success-url': '/login.jsp') - anonymous(username: 'guest', 'granted-authority': 'guest') - 'remember-me'() - } - createAppContext(AUTH_PROVIDER_XML) - - then: "Parses OK" - notThrown BeanDefinitionParsingException - } - - def 'Default remember-me-parameter is correct'() { - httpAutoConfig () { - 'remember-me'() - } - - createAppContext(AUTH_PROVIDER_XML) - expect: - rememberMeServices().parameter == AbstractRememberMeServices.DEFAULT_PARAMETER - } - - // SEC-2119 - def 'Custom remember-me-parameter is supported'() { - httpAutoConfig () { - 'remember-me'('remember-me-parameter': 'ourParam') - } - - createAppContext(AUTH_PROVIDER_XML) - expect: - rememberMeServices().parameter == 'ourParam' - } - - def 'remember-me-parameter cannot be used together with services-ref'() { - when: - httpAutoConfig () { - 'remember-me'('remember-me-parameter': 'ourParam', 'services-ref': 'ourService') - } - createAppContext(AUTH_PROVIDER_XML) - then: - BeanDefinitionParsingException e = thrown() - } - - // SEC-2826 - def 'Custom remember-me-cookie is supported'() { - httpAutoConfig () { - 'remember-me'('remember-me-cookie': 'ourCookie') - } - - createAppContext(AUTH_PROVIDER_XML) - expect: - rememberMeServices().cookieName == 'ourCookie' - } - - // SEC-2826 - def 'remember-me-cookie cannot be used together with services-ref'() { - when: - httpAutoConfig () { - 'remember-me'('remember-me-cookie': 'ourCookie', 'services-ref': 'ourService') - } - - createAppContext(AUTH_PROVIDER_XML) - then: - BeanDefinitionParsingException e = thrown() - expect: - e.message == 'Configuration problem: services-ref can\'t be used in combination with attributes token-repository-ref,data-source-ref, user-service-ref, token-validity-seconds, use-secure-cookie, remember-me-parameter or remember-me-cookie\nOffending resource: null' - } - - def rememberMeServices() { - getFilter(RememberMeAuthenticationFilter.class).getRememberMeServices() - } - - static class CustomTokenRepository extends InMemoryTokenRepositoryImpl { - - } -} diff --git a/config/src/test/java/org/springframework/security/config/http/RememberMeConfigTests.java b/config/src/test/java/org/springframework/security/config/http/RememberMeConfigTests.java new file mode 100644 index 00000000000..b1983250ab7 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/RememberMeConfigTests.java @@ -0,0 +1,378 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.http; + +import java.util.Collections; +import javax.servlet.http.Cookie; + +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.FatalBeanException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.TestDataSource; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices.DEFAULT_PARAMETER; +import static org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY; +import static org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl.CREATE_TABLE_SQL; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * + * @author Luke Taylor + * @author Rob Winch + * @author Oliver Becker + */ +public class RememberMeConfigTests { + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/RememberMeConfigTests"; + + @Autowired + MockMvc mvc; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Test + public void requestWithRememberMeWhenUsingCustomTokenRepositoryThenAutomaticallyReauthenticates() + throws Exception { + + this.spring.configLocations(this.xml("WithTokenRepository")).autowire(); + + MvcResult result = this.rememberAuthentication("user", "password") + .andExpect(cookie().secure(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, false)) + .andReturn(); + + Cookie cookie = rememberMeCookie(result); + + this.mvc.perform(get("/authenticated") + .cookie(cookie)) + .andExpect(status().isOk()); + + JdbcTemplate template = this.spring.getContext().getBean(JdbcTemplate.class); + int count = template.queryForObject("select count(*) from persistent_logins", int.class); + assertThat(count).isEqualTo(1); + } + + @Test + public void requestWithRememberMeWhenUsingCustomDataSourceThenAutomaticallyReauthenticates() + throws Exception { + + this.spring.configLocations(this.xml("WithDataSource")).autowire(); + + TestDataSource dataSource = this.spring.getContext().getBean(TestDataSource.class); + JdbcTemplate template = new JdbcTemplate(dataSource); + template.execute(CREATE_TABLE_SQL); + + MvcResult result = this.rememberAuthentication("user", "password") + .andExpect(cookie().secure(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, false)) + .andReturn(); + + Cookie cookie = rememberMeCookie(result); + + this.mvc.perform(get("/authenticated") + .cookie(cookie)) + .andExpect(status().isOk()); + + int count = template.queryForObject("select count(*) from persistent_logins", int.class); + assertThat(count).isEqualTo(1); + } + + @Test + public void requestWithRememberMeWhenUsingAuthenticationSuccessHandlerThenInvokesHandler() + throws Exception { + + this.spring.configLocations(this.xml("WithAuthenticationSuccessHandler")).autowire(); + + TestDataSource dataSource = this.spring.getContext().getBean(TestDataSource.class); + JdbcTemplate template = new JdbcTemplate(dataSource); + template.execute(CREATE_TABLE_SQL); + + MvcResult result = this.rememberAuthentication("user", "password") + .andExpect(cookie().secure(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, false)) + .andReturn(); + + Cookie cookie = rememberMeCookie(result); + + this.mvc.perform(get("/authenticated") + .cookie(cookie)) + .andExpect(redirectedUrl("/target")); + + int count = template.queryForObject("select count(*) from persistent_logins", int.class); + assertThat(count).isEqualTo(1); + } + + @Test + public void requestWithRememberMeWhenUsingCustomRememberMeServicesThenAuthenticates() + throws Exception { + // SEC-1281 - using key with external services + this.spring.configLocations(this.xml("WithServicesRef")).autowire(); + + MvcResult result = this.rememberAuthentication("user", "password") + .andExpect(cookie().secure(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, false)) + .andExpect(cookie().maxAge(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, 5000)) + .andReturn(); + + Cookie cookie = rememberMeCookie(result); + + this.mvc.perform(get("/authenticated") + .cookie(cookie)) + .andExpect(status().isOk()); + + // SEC-909 + this.mvc.perform(post("/logout") + .cookie(cookie) + .with(csrf())) + .andExpect(cookie().maxAge(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, 0)) + .andReturn(); + } + + @Test + public void logoutWhenUsingRememberMeDefaultsThenCookieIsCancelled() + throws Exception { + + this.spring.configLocations(this.xml("DefaultConfig")).autowire(); + + MvcResult result = this.rememberAuthentication("user", "password").andReturn(); + + Cookie cookie = rememberMeCookie(result); + + this.mvc.perform(post("/logout") + .cookie(cookie) + .with(csrf())) + .andExpect(cookie().maxAge(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, 0)); + } + + @Test + public void requestWithRememberMeWhenTokenValidityIsConfiguredThenCookieReflectsCorrectExpiration() + throws Exception { + + this.spring.configLocations(this.xml("TokenValidity")).autowire(); + + MvcResult result = this.rememberAuthentication("user", "password") + .andExpect(cookie().maxAge(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, 10000)) + .andReturn(); + + Cookie cookie = rememberMeCookie(result); + + this.mvc.perform(get("/authenticated") + .cookie(cookie)) + .andExpect(status().isOk()); + } + + @Test + public void requestWithRememberMeWhenTokenValidityIsNegativeThenCookieReflectsCorrectExpiration() + throws Exception { + + this.spring.configLocations(this.xml("NegativeTokenValidity")).autowire(); + + this.rememberAuthentication("user", "password") + .andExpect(cookie().maxAge(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, -1)); + } + + + @Test + public void configureWhenUsingDataSourceAndANegativeTokenValidityThenThrowsWiringException() { + assertThatCode(() -> this.spring.configLocations(this.xml("NegativeTokenValidityWithDataSource")).autowire()) + .isInstanceOf(FatalBeanException.class); + } + + @Test + public void requestWithRememberMeWhenTokenValidityIsResolvedByPropertyPlaceholderThenCookieReflectsCorrectExpiration() + throws Exception { + + this.spring.configLocations(this.xml("Sec2165")).autowire(); + + this.rememberAuthentication("user", "password") + .andExpect(cookie().maxAge(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, 30)); + } + + @Test + public void requestWithRememberMeWhenUseSecureCookieIsTrueThenCookieIsSecure() + throws Exception { + + this.spring.configLocations(this.xml("SecureCookie")).autowire(); + + this.rememberAuthentication("user", "password") + .andExpect(cookie().secure(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, true)); + } + + /** + * SEC-1827 + */ + @Test + public void requestWithRememberMeWhenUseSecureCookieIsFalseThenCookieIsNotSecure() + throws Exception { + + this.spring.configLocations(this.xml("Sec1827")).autowire(); + + this.rememberAuthentication("user", "password") + .andExpect(cookie().secure(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, false)); + } + + @Test + public void configureWhenUsingPersistentTokenRepositoryAndANegativeTokenValidityThenThrowsWiringException() { + assertThatCode(() -> this.spring.configLocations(this.xml("NegativeTokenValidityWithPersistentRepository")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class); + } + + @Test + public void requestWithRememberMeWhenUsingCustomUserDetailsServiceThenInvokesThisUserDetailsService() + throws Exception { + this.spring.configLocations(this.xml("WithUserDetailsService")).autowire(); + + UserDetailsService userDetailsService = this.spring.getContext().getBean(UserDetailsService.class); + when(userDetailsService.loadUserByUsername("user")).thenAnswer((invocation) -> + new User("user", "{noop}password", Collections.emptyList())); + + MvcResult result = this.rememberAuthentication("user", "password").andReturn(); + + Cookie cookie = rememberMeCookie(result); + + this.mvc.perform(get("/authenticated") + .cookie(cookie)) + .andExpect(status().isOk()); + + verify(userDetailsService, atLeastOnce()).loadUserByUsername("user"); + } + + /** + * SEC-742 + */ + @Test + public void requestWithRememberMeWhenExcludingBasicAuthenticationFilterThenStillReauthenticates() + throws Exception { + + this.spring.configLocations(this.xml("Sec742")).autowire(); + + MvcResult result = + this.mvc.perform(login("user", "password") + .param("remember-me", "true") + .with(csrf())) + .andExpect(redirectedUrl("/messageList.html")) + .andReturn(); + + Cookie cookie = rememberMeCookie(result); + + this.mvc.perform(get("/authenticated") + .cookie(cookie)) + .andExpect(status().isOk()); + } + + /** + * SEC-2119 + */ + @Test + public void requestWithRememberMeWhenUsingCustomRememberMeParameterThenReauthenticates() + throws Exception { + + this.spring.configLocations(this.xml("WithRememberMeParameter")).autowire(); + + MvcResult result = + this.mvc.perform(login("user", "password") + .param("custom-remember-me-parameter", "true") + .with(csrf())) + .andExpect(redirectedUrl("/")) + .andReturn(); + + Cookie cookie = rememberMeCookie(result); + + this.mvc.perform(get("/authenticated") + .cookie(cookie)) + .andExpect(status().isOk()); + } + + @Test + public void configureWhenUsingRememberMeParameterAndServicesRefThenThrowsWiringException() { + assertThatCode(() -> this.spring.configLocations(this.xml("WithRememberMeParameterAndServicesRef")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class); + } + + /** + * SEC-2826 + */ + @Test + public void authenticateWhenUsingCustomRememberMeCookieNameThenIssuesCookieWithThatName() + throws Exception { + + this.spring.configLocations(this.xml("WithRememberMeCookie")).autowire(); + + this.rememberAuthentication("user", "password") + .andExpect(cookie().exists("custom-remember-me-cookie")); + } + + /** + * SEC-2826 + */ + @Test + public void configureWhenUsingRememberMeCookieAndServicesRefThenThrowsWiringException() { + assertThatCode(() -> this.spring.configLocations(this.xml("WithRememberMeCookieAndServicesRef")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class) + .hasMessageContaining("Configuration problem: services-ref can't be used in combination with attributes " + + "token-repository-ref,data-source-ref, user-service-ref, token-validity-seconds, use-secure-cookie, " + + "remember-me-parameter or remember-me-cookie"); + } + + @RestController + static class BasicController { + @GetMapping("/authenticated") + String ok() { + return "ok"; + } + } + + private ResultActions rememberAuthentication(String username, String password) + throws Exception { + + return this.mvc.perform(login(username, password) + .param(DEFAULT_PARAMETER, "true") + .with(csrf())) + .andExpect(redirectedUrl("/")); + } + + private static MockHttpServletRequestBuilder login(String username, String password) { + return post("/login").param("username", username).param("password", password); + } + + private static Cookie rememberMeCookie(MvcResult result) { + return result.getResponse().getCookie("remember-me"); + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-DefaultConfig.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-DefaultConfig.xml new file mode 100644 index 00000000000..b59915244ad --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-DefaultConfig.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-NegativeTokenValidity.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-NegativeTokenValidity.xml new file mode 100644 index 00000000000..ced04be82d2 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-NegativeTokenValidity.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-NegativeTokenValidityWithDataSource.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-NegativeTokenValidityWithDataSource.xml new file mode 100644 index 00000000000..a650cda5f61 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-NegativeTokenValidityWithDataSource.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-NegativeTokenValidityWithPersistentRepository.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-NegativeTokenValidityWithPersistentRepository.xml new file mode 100644 index 00000000000..ff41dc27cbb --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-NegativeTokenValidityWithPersistentRepository.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-Sec1827.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-Sec1827.xml new file mode 100644 index 00000000000..76076025d93 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-Sec1827.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-Sec2165.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-Sec2165.xml new file mode 100644 index 00000000000..7874a9bfe35 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-Sec2165.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-Sec742.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-Sec742.xml new file mode 100644 index 00000000000..6d58d63ba50 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-Sec742.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-SecureCookie.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-SecureCookie.xml new file mode 100644 index 00000000000..1c59a2c6536 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-SecureCookie.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-TokenValidity.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-TokenValidity.xml new file mode 100644 index 00000000000..9517b3a791d --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-TokenValidity.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithAuthenticationSuccessHandler.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithAuthenticationSuccessHandler.xml new file mode 100644 index 00000000000..360d2004a7a --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithAuthenticationSuccessHandler.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithDataSource.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithDataSource.xml new file mode 100644 index 00000000000..1c0834a88b0 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithDataSource.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeCookie.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeCookie.xml new file mode 100644 index 00000000000..5e101d025b8 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeCookie.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeCookieAndServicesRef.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeCookieAndServicesRef.xml new file mode 100644 index 00000000000..bcc013e76dc --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeCookieAndServicesRef.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeParameter.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeParameter.xml new file mode 100644 index 00000000000..94d8cf8c71e --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeParameter.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeParameterAndServicesRef.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeParameterAndServicesRef.xml new file mode 100644 index 00000000000..46dff530c60 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeParameterAndServicesRef.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithServicesRef.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithServicesRef.xml new file mode 100644 index 00000000000..682cd2c3ead --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithServicesRef.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithTokenRepository.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithTokenRepository.xml new file mode 100644 index 00000000000..009506769cd --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithTokenRepository.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithUserDetailsService.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithUserDetailsService.xml new file mode 100644 index 00000000000..8b18a9d9664 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithUserDetailsService.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/userservice.xml b/config/src/test/resources/org/springframework/security/config/http/userservice.xml index e9eeeceb968..62a7b3f6abf 100644 --- a/config/src/test/resources/org/springframework/security/config/http/userservice.xml +++ b/config/src/test/resources/org/springframework/security/config/http/userservice.xml @@ -23,7 +23,7 @@ http://www.springframework.org/schema/security/spring-security.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> - + From 287c4b941fbe0c862a6d6d8efd595cc8653c88af Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 13 Aug 2018 07:51:06 -0400 Subject: [PATCH 219/226] Add additional parameters to OAuth2UserRequest Fixes gh-5368 --- .../OAuth2LoginAuthenticationProvider.java | 6 +- ...th2LoginReactiveAuthenticationManager.java | 5 +- ...thorizationCodeAuthenticationProvider.java | 9 ++- ...tionCodeReactiveAuthenticationManager.java | 6 +- .../client/oidc/userinfo/OidcUserRequest.java | 22 +++++- .../client/userinfo/OAuth2UserRequest.java | 34 +++++++++- ...Auth2LoginAuthenticationProviderTests.java | 58 +++++++++++++--- ...ginReactiveAuthenticationManagerTests.java | 26 ++++++- ...zationCodeAuthenticationProviderTests.java | 67 +++++++++++++++---- ...odeReactiveAuthenticationManagerTests.java | 34 ++++++++++ .../oidc/userinfo/OidcUserRequestTests.java | 64 +++++++++++++----- .../userinfo/OAuth2UserRequestTests.java | 51 ++++++++++---- 12 files changed, 311 insertions(+), 71 deletions(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java index 843424df63e..6c032fb073a 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java @@ -30,6 +30,7 @@ import org.springframework.util.Assert; import java.util.Collection; +import java.util.Map; /** * An implementation of an {@link AuthenticationProvider} for OAuth 2.0 Login, @@ -101,9 +102,10 @@ public Authentication authenticate(Authentication authentication) throws Authent authorizationCodeAuthentication.getAuthorizationExchange())); OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken(); + Map additionalParameters = accessTokenResponse.getAdditionalParameters(); - OAuth2User oauth2User = this.userService.loadUser( - new OAuth2UserRequest(authorizationCodeAuthentication.getClientRegistration(), accessToken)); + OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest( + authorizationCodeAuthentication.getClientRegistration(), accessToken, additionalParameters)); Collection mappedAuthorities = this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities()); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManager.java index 03aeca3397c..eb2f161c77d 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManager.java @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.client.authentication; import java.util.Collection; +import java.util.Map; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.core.Authentication; @@ -109,7 +110,9 @@ public Mono authenticate(Authentication authentication) { private Mono authenticationResult(OAuth2LoginAuthenticationToken authorizationCodeAuthentication, OAuth2AccessTokenResponse accessTokenResponse) { OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken(); - OAuth2UserRequest userRequest = new OAuth2UserRequest(authorizationCodeAuthentication.getClientRegistration(), accessToken); + Map additionalParameters = accessTokenResponse.getAdditionalParameters(); + OAuth2UserRequest userRequest = new OAuth2UserRequest( + authorizationCodeAuthentication.getClientRegistration(), accessToken, additionalParameters); return this.userService.loadUser(userRequest) .flatMap(oauth2User -> { Collection mappedAuthorities = diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java index ff1361f4d04..87a64227ead 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java @@ -139,19 +139,18 @@ public Authentication authenticate(Authentication authentication) throws Authent ClientRegistration clientRegistration = authorizationCodeAuthentication.getClientRegistration(); - if (!accessTokenResponse.getAdditionalParameters().containsKey(OidcParameterNames.ID_TOKEN)) { + Map additionalParameters = accessTokenResponse.getAdditionalParameters(); + if (!additionalParameters.containsKey(OidcParameterNames.ID_TOKEN)) { OAuth2Error invalidIdTokenError = new OAuth2Error( INVALID_ID_TOKEN_ERROR_CODE, "Missing (required) ID Token in Token Response for Client Registration: " + clientRegistration.getRegistrationId(), null); throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString()); } - OidcIdToken idToken = createOidcToken(clientRegistration, accessTokenResponse); - OidcUser oidcUser = this.userService.loadUser( - new OidcUserRequest(clientRegistration, accessTokenResponse.getAccessToken(), idToken)); - + OidcUser oidcUser = this.userService.loadUser(new OidcUserRequest( + clientRegistration, accessTokenResponse.getAccessToken(), idToken, additionalParameters)); Collection mappedAuthorities = this.authoritiesMapper.mapAuthorities(oidcUser.getAuthorities()); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java index 877e60e8676..8f9e4bcbbae 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java @@ -159,10 +159,10 @@ void setDecoderFactory( private Mono authenticationResult(OAuth2LoginAuthenticationToken authorizationCodeAuthentication, OAuth2AccessTokenResponse accessTokenResponse) { OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken(); - ClientRegistration clientRegistration = authorizationCodeAuthentication.getClientRegistration(); + Map additionalParameters = accessTokenResponse.getAdditionalParameters(); - if (!accessTokenResponse.getAdditionalParameters().containsKey(OidcParameterNames.ID_TOKEN)) { + if (!additionalParameters.containsKey(OidcParameterNames.ID_TOKEN)) { OAuth2Error invalidIdTokenError = new OAuth2Error( INVALID_ID_TOKEN_ERROR_CODE, "Missing (required) ID Token in Token Response for Client Registration: " + clientRegistration.getRegistrationId(), @@ -171,7 +171,7 @@ private Mono authenticationResult(OAuth2LoginAuthenti } return createOidcToken(clientRegistration, accessTokenResponse) - .map(idToken -> new OidcUserRequest(clientRegistration, accessToken, idToken)) + .map(idToken -> new OidcUserRequest(clientRegistration, accessToken, idToken, additionalParameters)) .flatMap(this.userService::loadUser) .flatMap(oauth2User -> { Collection mappedAuthorities = diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequest.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequest.java index 201ba577e2d..92158890b28 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequest.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,9 @@ import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.util.Assert; +import java.util.Collections; +import java.util.Map; + /** * Represents a request the {@link OidcUserService} uses * when initiating a request to the UserInfo Endpoint. @@ -45,7 +48,22 @@ public class OidcUserRequest extends OAuth2UserRequest { public OidcUserRequest(ClientRegistration clientRegistration, OAuth2AccessToken accessToken, OidcIdToken idToken) { - super(clientRegistration, accessToken); + this(clientRegistration, accessToken, idToken, Collections.emptyMap()); + } + + /** + * Constructs an {@code OidcUserRequest} using the provided parameters. + * + * @since 5.1 + * @param clientRegistration the client registration + * @param accessToken the access token credential + * @param idToken the ID Token + * @param additionalParameters the additional parameters, may be empty + */ + public OidcUserRequest(ClientRegistration clientRegistration, OAuth2AccessToken accessToken, + OidcIdToken idToken, Map additionalParameters) { + + super(clientRegistration, accessToken, additionalParameters); Assert.notNull(idToken, "idToken cannot be null"); this.idToken = idToken; } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequest.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequest.java index 949fb7699b9..b887c7aa440 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequest.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,11 @@ import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; /** * Represents a request the {@link OAuth2UserService} uses @@ -32,6 +37,7 @@ public class OAuth2UserRequest { private final ClientRegistration clientRegistration; private final OAuth2AccessToken accessToken; + private final Map additionalParameters; /** * Constructs an {@code OAuth2UserRequest} using the provided parameters. @@ -40,10 +46,26 @@ public class OAuth2UserRequest { * @param accessToken the access token */ public OAuth2UserRequest(ClientRegistration clientRegistration, OAuth2AccessToken accessToken) { + this(clientRegistration, accessToken, Collections.emptyMap()); + } + + /** + * Constructs an {@code OAuth2UserRequest} using the provided parameters. + * + * @since 5.1 + * @param clientRegistration the client registration + * @param accessToken the access token + * @param additionalParameters the additional parameters, may be empty + */ + public OAuth2UserRequest(ClientRegistration clientRegistration, OAuth2AccessToken accessToken, + Map additionalParameters) { Assert.notNull(clientRegistration, "clientRegistration cannot be null"); Assert.notNull(accessToken, "accessToken cannot be null"); this.clientRegistration = clientRegistration; this.accessToken = accessToken; + this.additionalParameters = Collections.unmodifiableMap( + CollectionUtils.isEmpty(additionalParameters) ? + Collections.emptyMap() : new LinkedHashMap<>(additionalParameters)); } /** @@ -63,4 +85,14 @@ public ClientRegistration getClientRegistration() { public OAuth2AccessToken getAccessToken() { return this.accessToken; } + + /** + * Returns the additional parameters that may be used in the request. + * + * @since 5.1 + * @return a {@code Map} of the additional parameters, may be empty. + */ + public Map getAdditionalParameters() { + return this.additionalParameters; + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProviderTests.java index 668f007f156..69949edb672 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProviderTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProviderTests.java @@ -20,6 +20,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.stubbing.Answer; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; @@ -35,17 +36,20 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; import org.springframework.security.oauth2.core.user.OAuth2User; +import java.time.Instant; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.containsString; @@ -164,11 +168,7 @@ public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizatio @Test public void authenticateWhenLoginSuccessThenReturnAuthentication() { - OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class); - OAuth2RefreshToken refreshToken = mock(OAuth2RefreshToken.class); - OAuth2AccessTokenResponse accessTokenResponse = mock(OAuth2AccessTokenResponse.class); - when(accessTokenResponse.getAccessToken()).thenReturn(accessToken); - when(accessTokenResponse.getRefreshToken()).thenReturn(refreshToken); + OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenSuccessResponse(); when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); OAuth2User principal = mock(OAuth2User.class); @@ -187,15 +187,13 @@ public void authenticateWhenLoginSuccessThenReturnAuthentication() { assertThat(authentication.getAuthorities()).isEqualTo(authorities); assertThat(authentication.getClientRegistration()).isEqualTo(this.clientRegistration); assertThat(authentication.getAuthorizationExchange()).isEqualTo(this.authorizationExchange); - assertThat(authentication.getAccessToken()).isEqualTo(accessToken); - assertThat(authentication.getRefreshToken()).isEqualTo(refreshToken); + assertThat(authentication.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); + assertThat(authentication.getRefreshToken()).isEqualTo(accessTokenResponse.getRefreshToken()); } @Test public void authenticateWhenAuthoritiesMapperSetThenReturnMappedAuthorities() { - OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class); - OAuth2AccessTokenResponse accessTokenResponse = mock(OAuth2AccessTokenResponse.class); - when(accessTokenResponse.getAccessToken()).thenReturn(accessToken); + OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenSuccessResponse(); when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); OAuth2User principal = mock(OAuth2User.class); @@ -216,4 +214,42 @@ public void authenticateWhenAuthoritiesMapperSetThenReturnMappedAuthorities() { assertThat(authentication.getAuthorities()).isEqualTo(mappedAuthorities); } + + // gh-5368 + @Test + public void authenticateWhenTokenSuccessResponseThenAdditionalParametersAddedToUserRequest() { + OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenSuccessResponse(); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); + + OAuth2User principal = mock(OAuth2User.class); + List authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); + when(principal.getAuthorities()).thenAnswer( + (Answer>) invocation -> authorities); + ArgumentCaptor userRequestArgCaptor = ArgumentCaptor.forClass(OAuth2UserRequest.class); + when(this.userService.loadUser(userRequestArgCaptor.capture())).thenReturn(principal); + + this.authenticationProvider.authenticate( + new OAuth2LoginAuthenticationToken(this.clientRegistration, this.authorizationExchange)); + + assertThat(userRequestArgCaptor.getValue().getAdditionalParameters()).containsAllEntriesOf( + accessTokenResponse.getAdditionalParameters()); + } + + private OAuth2AccessTokenResponse accessTokenSuccessResponse() { + Instant expiresAt = Instant.now().plusSeconds(5); + Set scopes = new LinkedHashSet<>(Arrays.asList("scope1", "scope2")); + Map additionalParameters = new HashMap<>(); + additionalParameters.put("param1", "value1"); + additionalParameters.put("param2", "value2"); + + return OAuth2AccessTokenResponse + .withToken("access-token-1234") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(expiresAt.getEpochSecond()) + .scopes(scopes) + .refreshToken("refresh-token-1234") + .additionalParameters(additionalParameters) + .build(); + + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManagerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManagerTests.java index cbc24b4606a..073d34230d7 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManagerTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManagerTests.java @@ -23,11 +23,14 @@ import static org.mockito.Mockito.when; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.security.authentication.TestingAuthenticationToken; @@ -164,7 +167,7 @@ public void authenticationWhenOAuth2UserNotFoundThenEmpty() { } @Test - public void authenticationWhenOAuth2UserNotFoundThenSuccess() { + public void authenticationWhenOAuth2UserFoundThenSuccess() { OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("foo") .tokenType(OAuth2AccessToken.TokenType.BEARER) .build(); @@ -179,6 +182,27 @@ public void authenticationWhenOAuth2UserNotFoundThenSuccess() { assertThat(result.isAuthenticated()).isTrue(); } + // gh-5368 + @Test + public void authenticateWhenTokenSuccessResponseThenAdditionalParametersAddedToUserRequest() { + Map additionalParameters = new HashMap<>(); + additionalParameters.put("param1", "value1"); + additionalParameters.put("param2", "value2"); + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("foo") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .additionalParameters(additionalParameters) + .build(); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse)); + DefaultOAuth2User user = new DefaultOAuth2User(AuthorityUtils.createAuthorityList("ROLE_USER"), Collections.singletonMap("user", "rob"), "user"); + ArgumentCaptor userRequestArgCaptor = ArgumentCaptor.forClass(OAuth2UserRequest.class); + when(this.userService.loadUser(userRequestArgCaptor.capture())).thenReturn(Mono.just(user)); + + this.manager.authenticate(loginToken()).block(); + + assertThat(userRequestArgCaptor.getValue().getAdditionalParameters()) + .containsAllEntriesOf(accessTokenResponse.getAdditionalParameters()); + } + private OAuth2LoginAuthenticationToken loginToken() { ClientRegistration clientRegistration = this.registration.build(); OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProviderTests.java index 6be263b1bd4..cc86577ce05 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProviderTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProviderTests.java @@ -20,6 +20,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.stubbing.Answer; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PrepareForTest; @@ -37,7 +38,6 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; @@ -55,6 +55,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.containsString; @@ -78,8 +79,6 @@ public class OidcAuthorizationCodeAuthenticationProviderTests { private OAuth2AuthorizationExchange authorizationExchange; private OAuth2AccessTokenResponseClient accessTokenResponseClient; private OAuth2AccessTokenResponse accessTokenResponse; - private OAuth2AccessToken accessToken; - private OAuth2RefreshToken refreshToken; private OAuth2UserService userService; private OidcAuthorizationCodeAuthenticationProvider authenticationProvider; @@ -95,9 +94,7 @@ public void setUp() throws Exception { this.authorizationResponse = mock(OAuth2AuthorizationResponse.class); this.authorizationExchange = new OAuth2AuthorizationExchange(this.authorizationRequest, this.authorizationResponse); this.accessTokenResponseClient = mock(OAuth2AccessTokenResponseClient.class); - this.accessTokenResponse = mock(OAuth2AccessTokenResponse.class); - this.accessToken = mock(OAuth2AccessToken.class); - this.refreshToken = mock(OAuth2RefreshToken.class); + this.accessTokenResponse = this.accessTokenSuccessResponse(); this.userService = mock(OAuth2UserService.class); this.authenticationProvider = PowerMockito.spy( new OidcAuthorizationCodeAuthenticationProvider(this.accessTokenResponseClient, this.userService)); @@ -111,11 +108,6 @@ public void setUp() throws Exception { when(this.authorizationResponse.getState()).thenReturn("12345"); when(this.authorizationRequest.getRedirectUri()).thenReturn("http://example.com"); when(this.authorizationResponse.getRedirectUri()).thenReturn("http://example.com"); - when(this.accessTokenResponse.getAccessToken()).thenReturn(this.accessToken); - when(this.accessTokenResponse.getRefreshToken()).thenReturn(this.refreshToken); - Map additionalParameters = new HashMap<>(); - additionalParameters.put(OidcParameterNames.ID_TOKEN, "id-token"); - when(this.accessTokenResponse.getAdditionalParameters()).thenReturn(additionalParameters); when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(this.accessTokenResponse); } @@ -194,7 +186,11 @@ public void authenticateWhenTokenResponseDoesNotContainIdTokenThenThrowOAuth2Aut this.exception.expect(OAuth2AuthenticationException.class); this.exception.expectMessage(containsString("invalid_id_token")); - when(this.accessTokenResponse.getAdditionalParameters()).thenReturn(Collections.emptyMap()); + OAuth2AccessTokenResponse accessTokenResponse = + OAuth2AccessTokenResponse.withResponse(this.accessTokenSuccessResponse()) + .additionalParameters(Collections.emptyMap()) + .build(); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); this.authenticationProvider.authenticate( new OAuth2LoginAuthenticationToken(this.clientRegistration, this.authorizationExchange)); @@ -368,8 +364,8 @@ public void authenticateWhenLoginSuccessThenReturnAuthentication() throws Except assertThat(authentication.getAuthorities()).isEqualTo(authorities); assertThat(authentication.getClientRegistration()).isEqualTo(this.clientRegistration); assertThat(authentication.getAuthorizationExchange()).isEqualTo(this.authorizationExchange); - assertThat(authentication.getAccessToken()).isEqualTo(this.accessToken); - assertThat(authentication.getRefreshToken()).isEqualTo(this.refreshToken); + assertThat(authentication.getAccessToken()).isEqualTo(this.accessTokenResponse.getAccessToken()); + assertThat(authentication.getRefreshToken()).isEqualTo(this.accessTokenResponse.getRefreshToken()); } @Test @@ -400,6 +396,30 @@ public void authenticateWhenAuthoritiesMapperSetThenReturnMappedAuthorities() th assertThat(authentication.getAuthorities()).isEqualTo(mappedAuthorities); } + // gh-5368 + @Test + public void authenticateWhenTokenSuccessResponseThenAdditionalParametersAddedToUserRequest() throws Exception { + Map claims = new HashMap<>(); + claims.put(IdTokenClaimNames.ISS, "https://provider.com"); + claims.put(IdTokenClaimNames.SUB, "subject1"); + claims.put(IdTokenClaimNames.AUD, Arrays.asList("client1", "client2")); + claims.put(IdTokenClaimNames.AZP, "client1"); + this.setUpIdToken(claims); + + OidcUser principal = mock(OidcUser.class); + List authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); + when(principal.getAuthorities()).thenAnswer( + (Answer>) invocation -> authorities); + ArgumentCaptor userRequestArgCaptor = ArgumentCaptor.forClass(OidcUserRequest.class); + when(this.userService.loadUser(userRequestArgCaptor.capture())).thenReturn(principal); + + this.authenticationProvider.authenticate(new OAuth2LoginAuthenticationToken( + this.clientRegistration, this.authorizationExchange)); + + assertThat(userRequestArgCaptor.getValue().getAdditionalParameters()).containsAllEntriesOf( + this.accessTokenResponse.getAdditionalParameters()); + } + private void setUpIdToken(Map claims) throws Exception { Instant issuedAt = Instant.now(); Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600); @@ -416,4 +436,23 @@ private void setUpIdToken(Map claims, Instant issuedAt, Instant when(jwtDecoder.decode(anyString())).thenReturn(idToken); PowerMockito.doReturn(jwtDecoder).when(this.authenticationProvider, "getJwtDecoder", any(ClientRegistration.class)); } + + private OAuth2AccessTokenResponse accessTokenSuccessResponse() { + Instant expiresAt = Instant.now().plusSeconds(5); + Set scopes = new LinkedHashSet<>(Arrays.asList("openid", "profile", "email")); + Map additionalParameters = new HashMap<>(); + additionalParameters.put("param1", "value1"); + additionalParameters.put("param2", "value2"); + additionalParameters.put(OidcParameterNames.ID_TOKEN, "id-token"); + + return OAuth2AccessTokenResponse + .withToken("access-token-1234") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(expiresAt.getEpochSecond()) + .scopes(scopes) + .refreshToken("refresh-token-1234") + .additionalParameters(additionalParameters) + .build(); + + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManagerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManagerTests.java index 362beed4da6..2b9bc3061e3 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManagerTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManagerTests.java @@ -19,6 +19,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.security.authentication.TestingAuthenticationToken; @@ -217,6 +218,39 @@ public void authenticationWhenOAuth2UserFoundThenSuccess() { assertThat(result.isAuthenticated()).isTrue(); } + // gh-5368 + @Test + public void authenticateWhenTokenSuccessResponseThenAdditionalParametersAddedToUserRequest() { + Map additionalParameters = new HashMap<>(); + additionalParameters.put(OidcParameterNames.ID_TOKEN, this.idToken.getTokenValue()); + additionalParameters.put("param1", "value1"); + additionalParameters.put("param2", "value2"); + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("foo") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .additionalParameters(additionalParameters) + .build(); + + Map claims = new HashMap<>(); + claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com"); + claims.put(IdTokenClaimNames.SUB, "rob"); + claims.put(IdTokenClaimNames.AUD, Arrays.asList("clientId")); + Instant issuedAt = Instant.now(); + Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600); + Jwt idToken = new Jwt("id-token", issuedAt, expiresAt, claims, claims); + + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse)); + DefaultOidcUser user = new DefaultOidcUser(AuthorityUtils.createAuthorityList("ROLE_USER"), this.idToken); + ArgumentCaptor userRequestArgCaptor = ArgumentCaptor.forClass(OidcUserRequest.class); + when(this.userService.loadUser(userRequestArgCaptor.capture())).thenReturn(Mono.just(user)); + when(this.jwtDecoder.decode(any())).thenReturn(Mono.just(idToken)); + this.manager.setDecoderFactory(c -> this.jwtDecoder); + + this.manager.authenticate(loginToken()).block(); + + assertThat(userRequestArgCaptor.getValue().getAdditionalParameters()) + .containsAllEntriesOf(accessTokenResponse.getAdditionalParameters()); + } + private OAuth2LoginAuthenticationToken loginToken() { ClientRegistration clientRegistration = this.registration.build(); OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestTests.java index ee43d5b1671..afb5a412955 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,57 +17,87 @@ import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; + import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for {@link OidcUserRequest}. * * @author Joe Grandja */ -@RunWith(PowerMockRunner.class) -@PrepareForTest(ClientRegistration.class) public class OidcUserRequestTests { private ClientRegistration clientRegistration; private OAuth2AccessToken accessToken; private OidcIdToken idToken; + private Map additionalParameters; @Before public void setUp() { - this.clientRegistration = mock(ClientRegistration.class); - this.accessToken = mock(OAuth2AccessToken.class); - this.idToken = mock(OidcIdToken.class); + this.clientRegistration = ClientRegistration.withRegistrationId("registration-1") + .clientId("client-1") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate("https://client.com") + .scope(new LinkedHashSet<>(Arrays.asList("openid", "profile"))) + .authorizationUri("https://provider.com/oauth2/authorization") + .tokenUri("https://provider.com/oauth2/token") + .jwkSetUri("https://provider.com/keys") + .clientName("Client 1") + .build(); + this.accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + "access-token-1234", Instant.now(), Instant.now().plusSeconds(60), + new LinkedHashSet<>(Arrays.asList("scope1", "scope2"))); + Map claims = new HashMap<>(); + claims.put(IdTokenClaimNames.ISS, "https://provider.com"); + claims.put(IdTokenClaimNames.SUB, "subject1"); + claims.put(IdTokenClaimNames.AZP, "client-1"); + this.idToken = new OidcIdToken("id-token-1234", Instant.now(), + Instant.now().plusSeconds(3600), claims); + this.additionalParameters = new HashMap<>(); + this.additionalParameters.put("param1", "value1"); + this.additionalParameters.put("param2", "value2"); } - @Test(expected = IllegalArgumentException.class) + @Test public void constructorWhenClientRegistrationIsNullThenThrowIllegalArgumentException() { - new OidcUserRequest(null, this.accessToken, this.idToken); + assertThatThrownBy(() -> new OidcUserRequest(null, this.accessToken, this.idToken)) + .isInstanceOf(IllegalArgumentException.class); } - @Test(expected = IllegalArgumentException.class) + @Test public void constructorWhenAccessTokenIsNullThenThrowIllegalArgumentException() { - new OidcUserRequest(this.clientRegistration, null, this.idToken); + assertThatThrownBy(() -> new OidcUserRequest(this.clientRegistration, null, this.idToken)) + .isInstanceOf(IllegalArgumentException.class); } - @Test(expected = IllegalArgumentException.class) + @Test public void constructorWhenIdTokenIsNullThenThrowIllegalArgumentException() { - new OidcUserRequest(this.clientRegistration, this.accessToken, null); + assertThatThrownBy(() -> new OidcUserRequest(this.clientRegistration, this.accessToken, null)) + .isInstanceOf(IllegalArgumentException.class); } @Test public void constructorWhenAllParametersProvidedAndValidThenCreated() { OidcUserRequest userRequest = new OidcUserRequest( - this.clientRegistration, this.accessToken, this.idToken); + this.clientRegistration, this.accessToken, this.idToken, this.additionalParameters); assertThat(userRequest.getClientRegistration()).isEqualTo(this.clientRegistration); assertThat(userRequest.getAccessToken()).isEqualTo(this.accessToken); assertThat(userRequest.getIdToken()).isEqualTo(this.idToken); + assertThat(userRequest.getAdditionalParameters()).containsAllEntriesOf(this.additionalParameters); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequestTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequestTests.java index 59a054aaa76..6415721604d 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequestTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,47 +17,70 @@ import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; + import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for {@link OAuth2UserRequest}. * * @author Joe Grandja */ -@RunWith(PowerMockRunner.class) -@PrepareForTest(ClientRegistration.class) public class OAuth2UserRequestTests { private ClientRegistration clientRegistration; private OAuth2AccessToken accessToken; + private Map additionalParameters; @Before public void setUp() { - this.clientRegistration = mock(ClientRegistration.class); - this.accessToken = mock(OAuth2AccessToken.class); + this.clientRegistration = ClientRegistration.withRegistrationId("registration-1") + .clientId("client-1") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate("https://client.com") + .scope(new LinkedHashSet<>(Arrays.asList("scope1", "scope2"))) + .authorizationUri("https://provider.com/oauth2/authorization") + .tokenUri("https://provider.com/oauth2/token") + .clientName("Client 1") + .build(); + this.accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + "access-token-1234", Instant.now(), Instant.now().plusSeconds(60), + new LinkedHashSet<>(Arrays.asList("scope1", "scope2"))); + this.additionalParameters = new HashMap<>(); + this.additionalParameters.put("param1", "value1"); + this.additionalParameters.put("param2", "value2"); } - @Test(expected = IllegalArgumentException.class) + @Test public void constructorWhenClientRegistrationIsNullThenThrowIllegalArgumentException() { - new OAuth2UserRequest(null, this.accessToken); + assertThatThrownBy(() -> new OAuth2UserRequest(null, this.accessToken)) + .isInstanceOf(IllegalArgumentException.class); } - @Test(expected = IllegalArgumentException.class) + @Test public void constructorWhenAccessTokenIsNullThenThrowIllegalArgumentException() { - new OAuth2UserRequest(this.clientRegistration, null); + assertThatThrownBy(() -> new OAuth2UserRequest(this.clientRegistration, null)) + .isInstanceOf(IllegalArgumentException.class); } @Test public void constructorWhenAllParametersProvidedAndValidThenCreated() { - OAuth2UserRequest userRequest = new OAuth2UserRequest(this.clientRegistration, this.accessToken); + OAuth2UserRequest userRequest = new OAuth2UserRequest( + this.clientRegistration, this.accessToken, this.additionalParameters); assertThat(userRequest.getClientRegistration()).isEqualTo(this.clientRegistration); assertThat(userRequest.getAccessToken()).isEqualTo(this.accessToken); + assertThat(userRequest.getAdditionalParameters()).containsAllEntriesOf(this.additionalParameters); } } From 16b0784429058df3355db8500a0532d6341def80 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 14 Aug 2018 13:32:51 -0400 Subject: [PATCH 220/226] Make ClientRegistration.clientSecret optional Fixes gh-5652 --- .../registration/ClientRegistration.java | 5 +- .../registration/ClientRegistrationTests.java | 50 +++++++++---------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java index f6342fc06ae..6b3df65ac51 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java @@ -20,6 +20,7 @@ import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import java.util.Arrays; import java.util.Collection; @@ -463,7 +464,7 @@ private ClientRegistration create() { clientRegistration.registrationId = this.registrationId; clientRegistration.clientId = this.clientId; - clientRegistration.clientSecret = this.clientSecret; + clientRegistration.clientSecret = StringUtils.hasText(this.clientSecret) ? this.clientSecret : ""; clientRegistration.clientAuthenticationMethod = this.clientAuthenticationMethod; clientRegistration.authorizationGrantType = this.authorizationGrantType; clientRegistration.redirectUriTemplate = this.redirectUriTemplate; @@ -488,7 +489,6 @@ private void validateAuthorizationCodeGrantType() { () -> "authorizationGrantType must be " + AuthorizationGrantType.AUTHORIZATION_CODE.getValue()); Assert.hasText(this.registrationId, "registrationId cannot be empty"); Assert.hasText(this.clientId, "clientId cannot be empty"); - Assert.hasText(this.clientSecret, "clientSecret cannot be empty"); Assert.notNull(this.clientAuthenticationMethod, "clientAuthenticationMethod cannot be null"); Assert.hasText(this.redirectUriTemplate, "redirectUriTemplate cannot be empty"); Assert.hasText(this.authorizationUri, "authorizationUri cannot be empty"); @@ -515,7 +515,6 @@ private void validateClientCredentialsGrantType() { () -> "authorizationGrantType must be " + AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()); Assert.hasText(this.registrationId, "registrationId cannot be empty"); Assert.hasText(this.clientId, "clientId cannot be empty"); - Assert.hasText(this.clientSecret, "clientSecret cannot be empty"); Assert.notNull(this.clientAuthenticationMethod, "clientAuthenticationMethod cannot be null"); Assert.hasText(this.tokenUri, "tokenUri cannot be empty"); } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java index b1218d32959..7c5758b4bde 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java @@ -124,21 +124,22 @@ public void buildWhenAuthorizationCodeGrantClientIdIsNullThenThrowIllegalArgumen .build(); } - @Test(expected = IllegalArgumentException.class) - public void buildWhenAuthorizationCodeGrantClientSecretIsNullThenThrowIllegalArgumentException() { - ClientRegistration.withRegistrationId(REGISTRATION_ID) - .clientId(CLIENT_ID) - .clientSecret(null) - .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUriTemplate(REDIRECT_URI) - .scope(SCOPES.toArray(new String[0])) - .authorizationUri(AUTHORIZATION_URI) - .tokenUri(TOKEN_URI) - .userInfoAuthenticationMethod(AuthenticationMethod.FORM) - .jwkSetUri(JWK_SET_URI) - .clientName(CLIENT_NAME) - .build(); + @Test + public void buildWhenAuthorizationCodeGrantClientSecretIsNullThenDefaultToEmpty() { + ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .clientSecret(null) + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate(REDIRECT_URI) + .scope(SCOPES.toArray(new String[0])) + .authorizationUri(AUTHORIZATION_URI) + .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) + .jwkSetUri(JWK_SET_URI) + .clientName(CLIENT_NAME) + .build(); + assertThat(clientRegistration.getClientSecret()).isEqualTo(""); } @Test(expected = IllegalArgumentException.class) @@ -462,16 +463,15 @@ public void buildWhenClientCredentialsGrantClientIdIsNullThenThrowIllegalArgumen } @Test - public void buildWhenClientCredentialsGrantClientSecretIsNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> - ClientRegistration.withRegistrationId(REGISTRATION_ID) - .clientId(CLIENT_ID) - .clientSecret(null) - .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) - .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) - .tokenUri(TOKEN_URI) - .build() - ).isInstanceOf(IllegalArgumentException.class); + public void buildWhenClientCredentialsGrantClientSecretIsNullThenDefaultToEmpty() { + ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .clientSecret(null) + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .tokenUri(TOKEN_URI) + .build(); + assertThat(clientRegistration.getClientSecret()).isEqualTo(""); } @Test From 6b890fff61fcf7aad2fadf6edd123fe78e54bb78 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 14 Aug 2018 14:05:45 -0400 Subject: [PATCH 221/226] Relax validation on ClientRegistration Fixes gh-5667 --- .../registration/ClientRegistration.java | 12 +- .../registration/ClientRegistrationTests.java | 117 ++++++++---------- 2 files changed, 51 insertions(+), 78 deletions(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java index 6b3df65ac51..411cd36712a 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java @@ -18,7 +18,6 @@ import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; -import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -479,7 +478,8 @@ private ClientRegistration create() { providerDetails.jwkSetUri = this.jwkSetUri; clientRegistration.providerDetails = providerDetails; - clientRegistration.clientName = this.clientName; + clientRegistration.clientName = StringUtils.hasText(this.clientName) ? + this.clientName : this.registrationId; return clientRegistration; } @@ -489,15 +489,9 @@ private void validateAuthorizationCodeGrantType() { () -> "authorizationGrantType must be " + AuthorizationGrantType.AUTHORIZATION_CODE.getValue()); Assert.hasText(this.registrationId, "registrationId cannot be empty"); Assert.hasText(this.clientId, "clientId cannot be empty"); - Assert.notNull(this.clientAuthenticationMethod, "clientAuthenticationMethod cannot be null"); Assert.hasText(this.redirectUriTemplate, "redirectUriTemplate cannot be empty"); Assert.hasText(this.authorizationUri, "authorizationUri cannot be empty"); Assert.hasText(this.tokenUri, "tokenUri cannot be empty"); - if (this.scopes != null && this.scopes.contains(OidcScopes.OPENID)) { - // OIDC Clients need to verify/validate the ID Token - Assert.hasText(this.jwkSetUri, "jwkSetUri cannot be empty"); - } - Assert.hasText(this.clientName, "clientName cannot be empty"); } private void validateImplicitGrantType() { @@ -507,7 +501,6 @@ private void validateImplicitGrantType() { Assert.hasText(this.clientId, "clientId cannot be empty"); Assert.hasText(this.redirectUriTemplate, "redirectUriTemplate cannot be empty"); Assert.hasText(this.authorizationUri, "authorizationUri cannot be empty"); - Assert.hasText(this.clientName, "clientName cannot be empty"); } private void validateClientCredentialsGrantType() { @@ -515,7 +508,6 @@ private void validateClientCredentialsGrantType() { () -> "authorizationGrantType must be " + AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()); Assert.hasText(this.registrationId, "registrationId cannot be empty"); Assert.hasText(this.clientId, "clientId cannot be empty"); - Assert.notNull(this.clientAuthenticationMethod, "clientAuthenticationMethod cannot be null"); Assert.hasText(this.tokenUri, "tokenUri cannot be empty"); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java index 7c5758b4bde..438894fab5c 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java @@ -142,21 +142,21 @@ public void buildWhenAuthorizationCodeGrantClientSecretIsNullThenDefaultToEmpty( assertThat(clientRegistration.getClientSecret()).isEqualTo(""); } - @Test(expected = IllegalArgumentException.class) - public void buildWhenAuthorizationCodeGrantClientAuthenticationMethodIsNullThenThrowIllegalArgumentException() { - ClientRegistration.withRegistrationId(REGISTRATION_ID) - .clientId(CLIENT_ID) - .clientSecret(CLIENT_SECRET) - .clientAuthenticationMethod(null) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUriTemplate(REDIRECT_URI) - .scope(SCOPES.toArray(new String[0])) - .authorizationUri(AUTHORIZATION_URI) - .tokenUri(TOKEN_URI) - .userInfoAuthenticationMethod(AuthenticationMethod.FORM) - .jwkSetUri(JWK_SET_URI) - .clientName(CLIENT_NAME) - .build(); + @Test + public void buildWhenAuthorizationCodeGrantClientAuthenticationMethodNotProvidedThenDefaultToBasic() { + ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .clientSecret(CLIENT_SECRET) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate(REDIRECT_URI) + .scope(SCOPES.toArray(new String[0])) + .authorizationUri(AUTHORIZATION_URI) + .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) + .jwkSetUri(JWK_SET_URI) + .clientName(CLIENT_NAME) + .build(); + assertThat(clientRegistration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); } @Test(expected = IllegalArgumentException.class) @@ -228,38 +228,21 @@ public void buildWhenAuthorizationCodeGrantTokenUriIsNullThenThrowIllegalArgumen .build(); } - @Test(expected = IllegalArgumentException.class) - public void buildWhenAuthorizationCodeGrantJwkSetUriIsNullThenThrowIllegalArgumentException() { - ClientRegistration.withRegistrationId(REGISTRATION_ID) - .clientId(CLIENT_ID) - .clientSecret(CLIENT_SECRET) - .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUriTemplate(REDIRECT_URI) - .scope(SCOPES.toArray(new String[0])) - .authorizationUri(AUTHORIZATION_URI) - .tokenUri(TOKEN_URI) - .userInfoAuthenticationMethod(AuthenticationMethod.FORM) - .jwkSetUri(null) - .clientName(CLIENT_NAME) - .build(); - } - - @Test(expected = IllegalArgumentException.class) - public void buildWhenAuthorizationCodeGrantClientNameIsNullThenThrowIllegalArgumentException() { - ClientRegistration.withRegistrationId(REGISTRATION_ID) - .clientId(CLIENT_ID) - .clientSecret(CLIENT_SECRET) - .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUriTemplate(REDIRECT_URI) - .scope(SCOPES.toArray(new String[0])) - .authorizationUri(AUTHORIZATION_URI) - .tokenUri(TOKEN_URI) - .userInfoAuthenticationMethod(AuthenticationMethod.FORM) - .jwkSetUri(JWK_SET_URI) - .clientName(null) - .build(); + @Test + public void buildWhenAuthorizationCodeGrantClientNameNotProvidedThenDefaultToRegistrationId() { + ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .clientSecret(CLIENT_SECRET) + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate(REDIRECT_URI) + .scope(SCOPES.toArray(new String[0])) + .authorizationUri(AUTHORIZATION_URI) + .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) + .jwkSetUri(JWK_SET_URI) + .build(); + assertThat(clientRegistration.getClientName()).isEqualTo(clientRegistration.getRegistrationId()); } @Test @@ -381,17 +364,17 @@ public void buildWhenImplicitGrantAuthorizationUriIsNullThenThrowIllegalArgument .build(); } - @Test(expected = IllegalArgumentException.class) - public void buildWhenImplicitGrantClientNameIsNullThenThrowIllegalArgumentException() { - ClientRegistration.withRegistrationId(REGISTRATION_ID) - .clientId(CLIENT_ID) - .authorizationGrantType(AuthorizationGrantType.IMPLICIT) - .redirectUriTemplate(REDIRECT_URI) - .scope(SCOPES.toArray(new String[0])) - .authorizationUri(AUTHORIZATION_URI) - .userInfoAuthenticationMethod(AuthenticationMethod.FORM) - .clientName(null) - .build(); + @Test + public void buildWhenImplicitGrantClientNameNotProvidedThenDefaultToRegistrationId() { + ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .authorizationGrantType(AuthorizationGrantType.IMPLICIT) + .redirectUriTemplate(REDIRECT_URI) + .scope(SCOPES.toArray(new String[0])) + .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) + .build(); + assertThat(clientRegistration.getClientName()).isEqualTo(clientRegistration.getRegistrationId()); } @Test @@ -475,16 +458,14 @@ public void buildWhenClientCredentialsGrantClientSecretIsNullThenDefaultToEmpty( } @Test - public void buildWhenClientCredentialsGrantClientAuthenticationMethodIsNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> - ClientRegistration.withRegistrationId(REGISTRATION_ID) - .clientId(CLIENT_ID) - .clientSecret(CLIENT_SECRET) - .clientAuthenticationMethod(null) - .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) - .tokenUri(TOKEN_URI) - .build() - ).isInstanceOf(IllegalArgumentException.class); + public void buildWhenClientCredentialsGrantClientAuthenticationMethodNotProvidedThenDefaultToBasic() { + ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .clientSecret(CLIENT_SECRET) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .tokenUri(TOKEN_URI) + .build(); + assertThat(clientRegistration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); } @Test From 742716fae3695317f4587ca41b91710ebce40a56 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 14 Aug 2018 13:13:19 -0600 Subject: [PATCH 222/226] OIDC Provider Configuration - ClientRegistrations OIDC Provider Configuration is now being used to create more than just ClientRegistration instances. Also, the endpoint is being addressed in more contexts than just the client. To that end, this refactors OidcConfigurationProvider in the config project to ClientRegistrations in the oauth2-client project. Fixes: gh-5647 --- .../registration/ClientRegistrations.java | 23 ++++++++++--------- .../registration/ClientRegistrationsTest.java | 22 +++++++++--------- 2 files changed, 23 insertions(+), 22 deletions(-) rename config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java => oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java (93%) rename config/src/test/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProviderTests.java => oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTest.java (95%) diff --git a/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java similarity index 93% rename from config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java rename to oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java index fdaaf91fa22..3f97322e158 100644 --- a/config/src/main/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java @@ -14,32 +14,32 @@ * limitations under the License. */ -package org.springframework.security.config.oauth2.client.oidc; +package org.springframework.security.oauth2.client.registration; import java.net.URI; import java.util.Collections; import java.util.List; -import org.springframework.security.oauth2.client.registration.ClientRegistration; +import com.nimbusds.oauth2.sdk.GrantType; +import com.nimbusds.oauth2.sdk.ParseException; +import com.nimbusds.oauth2.sdk.Scope; +import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; + import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.web.client.RestTemplate; -import com.nimbusds.oauth2.sdk.GrantType; -import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.Scope; -import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; - /** * Allows creating a {@link ClientRegistration.Builder} from an * OpenID Provider Configuration. * * @author Rob Winch + * @author Josh Cummings * @since 5.1 */ -public final class OidcConfigurationProvider { +public class ClientRegistrations { /** * Creates a {@link ClientRegistration.Builder} using the provided @@ -59,7 +59,7 @@ public final class OidcConfigurationProvider { * Example usage: *

      *
      -	 * ClientRegistration registration = OidcConfigurationProvider.issuer("https://example.com")
      +	 * ClientRegistration registration = ClientRegistrations.fromOidcIssuerLocation("https://example.com")
       	 *     .clientId("client-id")
       	 *     .clientSecret("client-secret")
       	 *     .build();
      @@ -67,7 +67,7 @@ public final class OidcConfigurationProvider {
       	 * @param issuer the Issuer
       	 * @return a {@link ClientRegistration.Builder} that was initialized by the OpenID Provider Configuration.
       	 */
      -	public static ClientRegistration.Builder issuer(String issuer) {
      +	public static ClientRegistration.Builder fromOidcIssuerLocation(String issuer) {
       		String openidConfiguration = getOpenidConfiguration(issuer);
       		OIDCProviderMetadata metadata = parse(openidConfiguration);
       		String metadataIssuer = metadata.getIssuer().getValue();
      @@ -135,5 +135,6 @@ private static OIDCProviderMetadata parse(String body) {
       		}
       	}
       
      -	private OidcConfigurationProvider() {}
      +	private ClientRegistrations() {}
      +
       }
      diff --git a/config/src/test/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTest.java
      similarity index 95%
      rename from config/src/test/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProviderTests.java
      rename to oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTest.java
      index 5b27c451343..eb2e29fb1fa 100644
      --- a/config/src/test/java/org/springframework/security/config/oauth2/client/oidc/OidcConfigurationProviderTests.java
      +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTest.java
      @@ -14,7 +14,10 @@
        * limitations under the License.
        */
       
      -package org.springframework.security.config.oauth2.client.oidc;
      +package org.springframework.security.oauth2.client.registration;
      +
      +import java.util.Arrays;
      +import java.util.Map;
       
       import com.fasterxml.jackson.core.type.TypeReference;
       import com.fasterxml.jackson.databind.ObjectMapper;
      @@ -23,22 +26,20 @@
       import org.junit.After;
       import org.junit.Before;
       import org.junit.Test;
      +
       import org.springframework.http.HttpHeaders;
       import org.springframework.http.MediaType;
      -import org.springframework.security.oauth2.client.registration.ClientRegistration;
       import org.springframework.security.oauth2.core.AuthorizationGrantType;
       import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
       
      -import java.util.Arrays;
      -import java.util.Map;
      -
      -import static org.assertj.core.api.Assertions.*;
      +import static org.assertj.core.api.Assertions.assertThat;
      +import static org.assertj.core.api.Assertions.assertThatThrownBy;
       
       /**
        * @author Rob Winch
        * @since 5.1
        */
      -public class OidcConfigurationProviderTests {
      +public class ClientRegistrationsTest {
       
       	/**
       	 * Contains all optional parameters that are found in ClientRegistration
      @@ -162,7 +163,6 @@ public void issuerWhenGrantTypesSupportedNullThenDefaulted() throws Exception {
       	 * We currently only support authorization_code, so verify we have a meaningful error until we add support.
       	 * @throws Exception
       	 */
      -	@Test
       	public void issuerWhenGrantTypesSupportedInvalidThenException() throws Exception {
       		this.response.put("grant_types_supported", Arrays.asList("implicit"));
       
      @@ -204,7 +204,7 @@ public void issuerWhenTokenEndpointAuthMethodsInvalidThenException() throws Exce
       
       	@Test
       	public void issuerWhenEmptyStringThenMeaningfulErrorMessage() {
      -		assertThatThrownBy(() -> OidcConfigurationProvider.issuer(""))
      +		assertThatThrownBy(() -> ClientRegistrations.fromOidcIssuerLocation(""))
       				.hasMessageContaining("Unable to resolve the OpenID Configuration with the provided Issuer of \"\"");
       	}
       
      @@ -216,7 +216,7 @@ public void issuerWhenOpenIdConfigurationDoesNotMatchThenMeaningfulErrorMessage(
       				.setBody(body)
       				.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
       		this.server.enqueue(mockResponse);
      -		assertThatThrownBy(() -> OidcConfigurationProvider.issuer(this.issuer))
      +		assertThatThrownBy(() -> ClientRegistrations.fromOidcIssuerLocation(this.issuer))
       				.hasMessageContaining("The Issuer \"https://example.com\" provided in the OpenID Configuration did not match the requested issuer \"" + this.issuer + "\"");
       	}
       
      @@ -229,7 +229,7 @@ private ClientRegistration registration(String path) throws Exception {
       				.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
       		this.server.enqueue(mockResponse);
       
      -		return OidcConfigurationProvider.issuer(this.issuer)
      +		return ClientRegistrations.fromOidcIssuerLocation(this.issuer)
       			.clientId("client-id")
       			.clientSecret("client-secret")
       			.build();
      
      From 43ee29a74942ffb849a601a7129c13520282538b Mon Sep 17 00:00:00 2001
      From: fdesu 
      Date: Thu, 16 Aug 2018 14:00:10 +0200
      Subject: [PATCH 223/226] Fix the broken link in the WebSocket documentation
      
      Changeset 46bb855 (#4094) removed websocket chat
      sample in favor of spring-session one. This commit
      updates spring-security documentation link to
      point to the up-to-date sample location
      ---
       docs/manual/src/docs/asciidoc/_includes/web/websocket.adoc | 2 +-
       1 file changed, 1 insertion(+), 1 deletion(-)
      
      diff --git a/docs/manual/src/docs/asciidoc/_includes/web/websocket.adoc b/docs/manual/src/docs/asciidoc/_includes/web/websocket.adoc
      index 2d1d603d5ea..d01a38fe228 100644
      --- a/docs/manual/src/docs/asciidoc/_includes/web/websocket.adoc
      +++ b/docs/manual/src/docs/asciidoc/_includes/web/websocket.adoc
      @@ -4,7 +4,7 @@
       Spring Security 4 added support for securing http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html[Spring's WebSocket support].
       This section describes how to use Spring Security's WebSocket support.
       
      -NOTE: You can find a complete working sample of WebSocket security in samples/javaconfig/chat.
      +NOTE: You can find a complete working sample of WebSocket security at https://github.com/spring-projects/spring-session/tree/master/samples/boot/websocket.
       
       .Direct JSR-356 Support
       ****
      
      From 301e59869be7860651a71095af1766c67b8c897b Mon Sep 17 00:00:00 2001
      From: Vedran Pavic 
      Date: Wed, 15 Aug 2018 22:05:10 +0200
      Subject: [PATCH 224/226] Add support for Feature-Policy security header
      
      ---
       .../web/configurers/HeadersConfigurer.java    | 47 +++++++++++-
       .../http/HeadersBeanDefinitionParser.java     | 33 +++++++-
       .../security/config/spring-security-5.1.rnc   |  9 ++-
       .../security/config/spring-security-5.1.xsd   | 18 ++++-
       .../configurers/HeadersConfigurerTests.groovy | 47 +++++++++++-
       .../_includes/appendix/namespace.adoc         | 20 +++++
       .../docs/asciidoc/_includes/web/headers.adoc  | 50 ++++++++++++
       .../writers/FeaturePolicyHeaderWriter.java    | 76 +++++++++++++++++++
       .../FeaturePolicyHeaderWriterTests.java       | 73 ++++++++++++++++++
       9 files changed, 368 insertions(+), 5 deletions(-)
       create mode 100644 web/src/main/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriter.java
       create mode 100644 web/src/test/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriterTests.java
      
      diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java
      index 992529ab57a..ba8652840cf 100644
      --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java
      +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java
      @@ -1,5 +1,5 @@
       /*
      - * Copyright 2002-2016 the original author or authors.
      + * Copyright 2002-2018 the original author or authors.
        *
        * Licensed under the Apache License, Version 2.0 (the "License");
        * you may not use this file except in compliance with the License.
      @@ -13,6 +13,7 @@
        * See the License for the specific language governing permissions and
        * limitations under the License.
        */
      +
       package org.springframework.security.config.annotation.web.configurers;
       
       import java.net.URI;
      @@ -58,6 +59,7 @@
        * @author Tim Ysewyn
        * @author Joe Grandja
        * @author Eddú Meléndez
      + * @author Vedran Pavic
        * @since 3.2
        */
       public class HeadersConfigurer> extends
      @@ -82,6 +84,8 @@ public class HeadersConfigurer> extends
       
       	private final ReferrerPolicyConfig referrerPolicy = new ReferrerPolicyConfig();
       
      +	private final FeaturePolicyConfig featurePolicy = new FeaturePolicyConfig();
      +
       	/**
       	 * Creates a new instance
       	 *
      @@ -775,6 +779,7 @@ private List getHeaderWriters() {
       		addIfNotNull(writers, hpkp.writer);
       		addIfNotNull(writers, contentSecurityPolicy.writer);
       		addIfNotNull(writers, referrerPolicy.writer);
      +		addIfNotNull(writers, featurePolicy.writer);
       		writers.addAll(headerWriters);
       		return writers;
       	}
      @@ -848,4 +853,44 @@ public HeadersConfigurer and() {
       		}
       
       	}
      +
      +	/**
      +	 * Allows configuration for Feature
      +	 * Policy.
      +	 * 

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

      + * Configuration is provided to the {@link FeaturePolicyHeaderWriter} which is + * responsible for writing the header. + * + * @see FeaturePolicyHeaderWriter + * @since 5.1 + * @return the {@link FeaturePolicyHeaderWriter} for additional configuration + * @throws IllegalArgumentException if policyDirectives is {@code null} or empty + */ + public FeaturePolicyConfig featurePolicy(String policyDirectives) { + this.featurePolicy.writer = new FeaturePolicyHeaderWriter(policyDirectives); + return featurePolicy; + } + + public final class FeaturePolicyConfig { + + private FeaturePolicyHeaderWriter writer; + + private FeaturePolicyConfig() { + } + + /** + * Allows completing configuration of Feature Policy and continuing configuration + * of headers. + * + * @return the {@link HeadersConfigurer} for additional configuration + */ + public HeadersConfigurer and() { + return HeadersConfigurer.this; + } + + } + } diff --git a/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java index 20ac10dab7e..dffeab94a09 100644 --- a/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.security.config.http; import java.net.URI; @@ -47,6 +48,7 @@ * @author Marten Deinum * @author Tim Ysewyn * @author Eddú Meléndez + * @author Vedran Pavic * @since 3.2 */ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { @@ -85,6 +87,7 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { private static final String CONTENT_SECURITY_POLICY_ELEMENT = "content-security-policy"; private static final String REFERRER_POLICY_ELEMENT = "referrer-policy"; + private static final String FEATURE_POLICY_ELEMENT = "feature-policy"; private static final String ALLOW_FROM = "ALLOW-FROM"; @@ -114,6 +117,8 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { parseReferrerPolicyElement(element, parserContext); + parseFeaturePolicyElement(element, parserContext); + parseHeaderElements(element); boolean noWriters = headerWriters.isEmpty(); @@ -313,6 +318,32 @@ private void addReferrerPolicy(Element referrerPolicyElement, ParserContext cont headerWriters.add(headersWriter.getBeanDefinition()); } + private void parseFeaturePolicyElement(Element element, ParserContext context) { + Element featurePolicyElement = (element == null) ? null + : DomUtils.getChildElementByTagName(element, FEATURE_POLICY_ELEMENT); + if (featurePolicyElement != null) { + addFeaturePolicy(featurePolicyElement, context); + } + } + + private void addFeaturePolicy(Element featurePolicyElement, ParserContext context) { + BeanDefinitionBuilder headersWriter = BeanDefinitionBuilder + .genericBeanDefinition(FeaturePolicyHeaderWriter.class); + + String policyDirectives = featurePolicyElement + .getAttribute(ATT_POLICY_DIRECTIVES); + if (!StringUtils.hasText(policyDirectives)) { + context.getReaderContext().error( + ATT_POLICY_DIRECTIVES + " requires a 'value' to be set.", + featurePolicyElement); + } + else { + headersWriter.addConstructorArgValue(policyDirectives); + } + + headerWriters.add(headersWriter.getBeanDefinition()); + } + private void attrNotAllowed(ParserContext context, String attrName, String otherAttrName, Element element) { context.getReaderContext().error( diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.1.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.rnc index f3b75156ad4..af761497c96 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.1.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.rnc @@ -743,7 +743,7 @@ csrf-options.attlist &= headers = ## Element for configuration of the HeaderWritersFilter. Enables easy setting for the X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. -element headers { headers-options.attlist, (cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & hpkp? & content-security-policy? & referrer-policy? & header*)} +element headers { headers-options.attlist, (cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & hpkp? & content-security-policy? & referrer-policy? & feature-policy? & header*)} headers-options.attlist &= ## Specifies if the default headers should be disabled. Default false. attribute defaults-disabled {xsd:boolean}? @@ -821,6 +821,13 @@ referrer-options.attlist &= ## The policies for the Referrer-Policy header. attribute policy {"no-referrer","no-referrer-when-downgrade","same-origin","origin","strict-origin","origin-when-cross-origin","strict-origin-when-cross-origin","unsafe-url"}? +feature-policy = + ## Adds support for Feature Policy + element feature-policy {feature-options.attlist} +feature-options.attlist &= + ## The security policy directive(s) for the Feature-Policy header. + attribute policy-directives {xsd:token}? + cache-control = ## Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for every request element cache-control {cache-control.attlist} diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.1.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.xsd index 773d9e55af1..e6c0f5a52ac 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.1.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.xsd @@ -2253,6 +2253,7 @@ + @@ -2464,6 +2465,21 @@ + + + Adds support for Feature Policy + + + + + + + + + The security policy directive(s) for the Feature-Policy header. + + + Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for @@ -2719,4 +2735,4 @@ - \ No newline at end of file + diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.groovy index 07fa2821144..07feb338f12 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.security.config.annotation.web.configurers import org.springframework.beans.factory.BeanCreationException @@ -20,14 +21,17 @@ import org.springframework.security.config.annotation.BaseSpringSpec import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter + import static org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy /** + * Tests for {@link HeadersConfigurer}. * * @author Rob Winch * @author Tim Ysewyn * @author Joe Grandja * @author Eddú Meléndez + * @author Vedran Pavic */ class HeadersConfigurerTests extends BaseSpringSpec { @@ -497,4 +501,45 @@ class HeadersConfigurerTests extends BaseSpringSpec { } } + def "headers.featurePolicy default header"() { + setup: + loadConfig(FeaturePolicyDefaultConfig) + request.secure = true + when: + springSecurityFilterChain.doFilter(request, response, chain) + then: + responseHeaders == ['Feature-Policy': 'geolocation \'self\''] + } + + @EnableWebSecurity + static class FeaturePolicyDefaultConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .headers() + .defaultsDisabled() + .featurePolicy("geolocation 'self'"); + } + } + + def "headers.featurePolicy empty policyDirectives"() { + when: + loadConfig(FeaturePolicyInvalidConfig) + then: + thrown(BeanCreationException) + } + + @EnableWebSecurity + static class FeaturePolicyInvalidConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .headers() + .defaultsDisabled() + .featurePolicy(""); + } + } + } diff --git a/docs/manual/src/docs/asciidoc/_includes/appendix/namespace.adoc b/docs/manual/src/docs/asciidoc/_includes/appendix/namespace.adoc index cc6c3a2bb86..462330aa8f0 100644 --- a/docs/manual/src/docs/asciidoc/_includes/appendix/namespace.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/appendix/namespace.adoc @@ -241,6 +241,7 @@ This allows HTTPS websites to resist impersonation by attackers using mis-issued ** `Content-Security-Policy` or `Content-Security-Policy-Report-Only` - Can be set using the <> element. https://www.w3.org/TR/CSP2/[Content Security Policy (CSP)] is a mechanism that web applications can leverage to mitigate content injection vulnerabilities, such as cross-site scripting (XSS). ** `Referrer-Policy` - Can be set using the <> element, https://www.w3.org/TR/referrer-policy/[Referrer-Policy] is a mechanism that web applications can leverage to manage the referrer field, which contains the last page the user was on. +** `Feature-Policy` - Can be set using the <> element, https://wicg.github.io/feature-policy/[Feature-Policy] is a mechanism that allows web developers to selectively enable, disable, and modify the behavior of certain APIs and web features in the browser. [[nsa-headers-attributes]] ===== Attributes @@ -272,6 +273,7 @@ The default is false (the headers are enabled). * <> * <> * <> +* <> * <> * <> * <> @@ -459,6 +461,24 @@ Default "no-referrer". +[[nsa-feature-policy]] +==== +When enabled adds the https://wicg.github.io/feature-policy/[Feature Policy] header to the response. + +[[nsa-feature-policy-attributes]] +===== Attributes + +[[nsa-feature-policy-policy-directives]] +* **policy-directives** +The security policy directive(s) for the Feature-Policy header. + +[[nsa-feature-policy-parents]] +===== Parent Elements of + +* <> + + + [[nsa-frame-options]] ==== When enabled adds the http://tools.ietf.org/html/draft-ietf-websec-x-frame-options[X-Frame-Options header] to the response, this allows newer browsers to do some security checks and prevent http://en.wikipedia.org/wiki/Clickjacking[clickjacking] attacks. diff --git a/docs/manual/src/docs/asciidoc/_includes/web/headers.adoc b/docs/manual/src/docs/asciidoc/_includes/web/headers.adoc index fd5bb146686..3873dc2df75 100644 --- a/docs/manual/src/docs/asciidoc/_includes/web/headers.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/web/headers.adoc @@ -714,6 +714,56 @@ protected void configure(HttpSecurity http) throws Exception { ---- +[[headers-feature]] +==== Feature Policy + +https://wicg.github.io/feature-policy/[Feature Policy] is a mechanism that allows web developers to selectively enable, disable, and modify the behavior of certain APIs and web features in the browser. + +[source] +---- +Feature-Policy: geolocation 'self' +---- + +With Feature Policy, developers can opt-in to a set of "policies" for the browser to enforce on specific features used throughout your site. +These policies restrict what APIs the site can access or modify the browser's default behavior for certain features. + +[[headers-feature-configure]] +===== Configuring Feature Policy + +Spring Security *_doesn't add_* Feature Policy header by default. + +You can enable the Feature-Policy header using XML configuration with the <>> element as shown below: + +[source,xml] +---- + + + + + + + +---- + +Similarly, you can enable the Feature Policy header using Java configuration as shown below: + +[source,java] +---- +@EnableWebSecurity +public class WebSecurityConfig extends +WebSecurityConfigurerAdapter { + +@Override +protected void configure(HttpSecurity http) throws Exception { + http + // ... + .headers() + .featurePolicy("geolocation 'self'"); +} +} +---- + + [[headers-custom]] === Custom Headers Spring Security has mechanisms to make it convenient to add the more common security headers to your application. diff --git a/web/src/main/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriter.java b/web/src/main/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriter.java new file mode 100644 index 00000000000..1c5528b8979 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriter.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.header.writers; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.web.header.HeaderWriter; +import org.springframework.util.Assert; + +/** + * Provides support for Feature + * Policy. + *

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

      + * A declaration of a feature policy contains a set of security policy directives, each + * responsible for declaring the restrictions for a particular feature type. + * + * @author Vedran Pavic + * @since 5.1 + */ +public final class FeaturePolicyHeaderWriter implements HeaderWriter { + + private static final String FEATURE_POLICY_HEADER = "Feature-Policy"; + + private String policyDirectives; + + /** + * Create a new instance of {@link FeaturePolicyHeaderWriter} with supplied security + * policy directive(s). + * + * @param policyDirectives the security policy directive(s) + * @throws IllegalArgumentException if policyDirectives is {@code null} or empty + */ + public FeaturePolicyHeaderWriter(String policyDirectives) { + setPolicyDirectives(policyDirectives); + } + + @Override + public void writeHeaders(HttpServletRequest request, HttpServletResponse response) { + response.setHeader(FEATURE_POLICY_HEADER, this.policyDirectives); + } + + /** + * Set the security policy directive(s) to be used in the response header. + * + * @param policyDirectives the security policy directive(s) + * @throws IllegalArgumentException if policyDirectives is {@code null} or empty + */ + public void setPolicyDirectives(String policyDirectives) { + Assert.hasLength(policyDirectives, "policyDirectives must not be null or empty"); + this.policyDirectives = policyDirectives; + } + + @Override + public String toString() { + return getClass().getName() + " [policyDirectives=" + this.policyDirectives + "]"; + } + +} diff --git a/web/src/test/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriterTests.java b/web/src/test/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriterTests.java new file mode 100644 index 00000000000..cf8b1fdeccd --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriterTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.header.writers; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link FeaturePolicyHeaderWriter}. + * + * @author Vedran Pavic + */ +public class FeaturePolicyHeaderWriterTests { + + private static final String DEFAULT_POLICY_DIRECTIVES = "geolocation 'self'"; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + private FeaturePolicyHeaderWriter writer; + + @Before + public void setUp() { + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + this.writer = new FeaturePolicyHeaderWriter(DEFAULT_POLICY_DIRECTIVES); + } + + @Test + public void writeHeadersFeaturePolicyDefault() { + writer.writeHeaders(this.request, this.response); + + assertThat(this.response.getHeaderNames()).hasSize(1); + assertThat(this.response.getHeader("Feature-Policy")) + .isEqualTo(DEFAULT_POLICY_DIRECTIVES); + } + + @Test + public void createWriterWithNullDirectivesShouldThrowException() { + assertThatThrownBy(() -> new FeaturePolicyHeaderWriter(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("policyDirectives must not be null or empty"); + } + + @Test + public void createWriterWithEmptyDirectivesShouldThrowException() { + assertThatThrownBy(() -> new FeaturePolicyHeaderWriter("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("policyDirectives must not be null or empty"); + } + +} From 5144c03913ed1b5f87089cdd6a2249f54aef9269 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 24 Jul 2018 11:47:30 -0600 Subject: [PATCH 225/226] Jwt Claim Validation This introduces OAuth2TokenValidator which allows the customization of validation steps that need to be performing when decoding a string token to a Jwt. At this point, two validators, JwtTimestampValidator and JwtIssuerValidator, are available for use. Fixes: gh-5133 --- .../OAuth2ResourceServerConfigurerTests.java | 133 ++++++++++ ...rConfigurerTests-ExpiresAt4687177990.token | 1 + .../core/DelegatingOAuth2TokenValidator.java | 56 +++++ .../oauth2/core/OAuth2TokenValidator.java | 35 +++ .../core/OAuth2TokenValidatorResult.java | 92 +++++++ .../DelegatingOAuth2TokenValidatorTests.java | 123 ++++++++++ .../core/OAuth2TokenValidatorResultTests.java | 55 +++++ .../oauth2/jwt/JwtIssuerValidator.java | 72 ++++++ .../oauth2/jwt/JwtTimestampValidator.java | 109 +++++++++ .../oauth2/jwt/JwtValidationException.java | 68 ++++++ .../security/oauth2/jwt/JwtValidators.java | 46 ++++ .../jwt/NimbusJwtDecoderJwkSupport.java | 51 +++- .../oauth2/jwt/JwtIssuerValidatorTests.java | 92 +++++++ .../jwt/JwtTimestampValidatorTests.java | 230 ++++++++++++++++++ .../jwt/NimbusJwtDecoderJwkSupportTests.java | 58 ++++- .../JwtAuthenticationProvider.java | 29 ++- 16 files changed, 1225 insertions(+), 25 deletions(-) create mode 100644 config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ExpiresAt4687177990.token create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/DelegatingOAuth2TokenValidator.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenValidator.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenValidatorResult.java create mode 100644 oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/DelegatingOAuth2TokenValidatorTests.java create mode 100644 oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/OAuth2TokenValidatorResultTests.java create mode 100644 oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtIssuerValidator.java create mode 100644 oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtTimestampValidator.java create mode 100644 oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidationException.java create mode 100644 oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidators.java create mode 100644 oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtIssuerValidatorTests.java create mode 100644 oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index cae0e334237..d592f0664ca 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -20,7 +20,10 @@ import java.io.FileReader; import java.io.IOException; import java.lang.reflect.Field; +import java.time.Clock; +import java.time.Duration; import java.time.Instant; +import java.time.ZoneId; import java.util.Collections; import java.util.Map; import java.util.stream.Collectors; @@ -60,12 +63,16 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtClaimNames; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtException; import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport; +import org.springframework.security.oauth2.jwt.JwtTimestampValidator; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint; import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; @@ -92,6 +99,7 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.core.StringStartsWith.startsWith; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -839,6 +847,57 @@ public void accessDeniedHandlerWhenGivenNullThenThrowsException() { .isInstanceOf(IllegalArgumentException.class); } + // -- token validator + + @Test + public void requestWhenCustomJwtValidatorFailsThenCorrespondingErrorMessage() + throws Exception { + + this.spring.register(WebServerConfig.class, CustomJwtValidatorConfig.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidNoScopes"); + + OAuth2TokenValidator jwtValidator = + this.spring.getContext().getBean(CustomJwtValidatorConfig.class) + .getJwtValidator(); + + OAuth2Error error = new OAuth2Error("custom-error", "custom-description", "custom-uri"); + + when(jwtValidator.validate(any(Jwt.class))).thenReturn(OAuth2TokenValidatorResult.failure(error)); + + this.mvc.perform(get("/") + .with(bearerToken(token))) + .andExpect(status().isUnauthorized()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, containsString("custom-description"))); + } + + @Test + public void requestWhenClockSkewSetThenTimestampWindowRelaxedAccordingly() + throws Exception { + + this.spring.register(WebServerConfig.class, UnexpiredJwtClockSkewConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ExpiresAt4687177990"); + + this.mvc.perform(get("/") + .with(bearerToken(token))) + .andExpect(status().isOk()); + } + + @Test + public void requestWhenClockSkewSetButJwtStillTooLateThenReportsExpired() + throws Exception { + + this.spring.register(WebServerConfig.class, ExpiredJwtClockSkewConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ExpiresAt4687177990"); + + this.mvc.perform(get("/") + .with(bearerToken(token))) + .andExpect(status().isUnauthorized()) + .andExpect(invalidTokenHeader("Jwt expired at")); + } + // -- In combination with other authentication providers @Test @@ -1266,6 +1325,80 @@ public JwtDecoder decoder() { } } + @EnableWebSecurity + static class CustomJwtValidatorConfig extends WebSecurityConfigurerAdapter { + @Value("${mock.jwk-set-uri}") String uri; + + private final OAuth2TokenValidator jwtValidator = mock(OAuth2TokenValidator.class); + + @Override + protected void configure(HttpSecurity http) throws Exception { + NimbusJwtDecoderJwkSupport jwtDecoder = + new NimbusJwtDecoderJwkSupport(this.uri); + jwtDecoder.setJwtValidator(this.jwtValidator); + + // @formatter:off + http + .oauth2() + .resourceServer() + .jwt() + .decoder(jwtDecoder); + // @formatter:on + } + + public OAuth2TokenValidator getJwtValidator() { + return this.jwtValidator; + } + } + + @EnableWebSecurity + static class UnexpiredJwtClockSkewConfig extends WebSecurityConfigurerAdapter { + @Value("${mock.jwk-set-uri}") String uri; + + @Override + protected void configure(HttpSecurity http) throws Exception { + Clock nearlyAnHourFromTokenExpiry = + Clock.fixed(Instant.ofEpochMilli(4687181540000L), ZoneId.systemDefault()); + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(Duration.ofHours(1)); + jwtValidator.setClock(nearlyAnHourFromTokenExpiry); + + NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(this.uri); + jwtDecoder.setJwtValidator(jwtValidator); + + // @formatter:off + http + .oauth2() + .resourceServer() + .jwt() + .decoder(jwtDecoder); + // @formatter:on + } + } + + @EnableWebSecurity + static class ExpiredJwtClockSkewConfig extends WebSecurityConfigurerAdapter { + @Value("${mock.jwk-set-uri}") String uri; + + @Override + protected void configure(HttpSecurity http) throws Exception { + Clock justOverOneHourAfterExpiry = + Clock.fixed(Instant.ofEpochMilli(4687181595000L), ZoneId.systemDefault()); + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(Duration.ofHours(1)); + jwtValidator.setClock(justOverOneHourAfterExpiry); + + NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(this.uri); + jwtDecoder.setJwtValidator(jwtValidator); + + // @formatter:off + http + .oauth2() + .resourceServer() + .jwt() + .decoder(jwtDecoder); + // @formatter:on + } + } + @Configuration static class JwtDecoderConfig { @Bean diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ExpiresAt4687177990.token b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ExpiresAt4687177990.token new file mode 100644 index 00000000000..df5ab8ac23a --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ExpiresAt4687177990.token @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjQ2ODcxNzc5OTB9.RRQvqIZzLweq0iwWUZk1Dpiz6iUmT4bAVhGWqvWNWK3UwJ6aBIYsCRhdVeKQp-g1TxXovMALeAu_2oPmV0wOEEanesAKxjKYcJZQIe8HnVqgug6Ibs04uQ1mJ4RgfntPM-ebsJs-2tjFFkLEYJSkpq2o6SEFW9jBJyW8b8C5UJJahqynonA-Dw5GH1nin5bhhliLuFOmu0Ityt0uJ1Y_vuGsSA-ltVcY52jE4x6GH9NQxLX4ceO1bHSOmdspBoGsE_yo9-zsQw0g1_Iy7uqEjos3xrrboH6Z_u7pRL7AQJ7GNzZlinjYYPANQbYknieZD6beddTK7lvr4DYiPBmXzA diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/DelegatingOAuth2TokenValidator.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/DelegatingOAuth2TokenValidator.java new file mode 100644 index 00000000000..dd6ae3d8a52 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/DelegatingOAuth2TokenValidator.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.core; + +import java.util.ArrayList; +import java.util.Collection; + +import org.springframework.util.Assert; + +/** + * A composite validator + * + * @param the type of {@link AbstractOAuth2Token} this validator validates + * + * @author Josh Cummings + * @since 5.1 + */ +public final class DelegatingOAuth2TokenValidator + implements OAuth2TokenValidator { + + private final Collection> tokenValidators; + + public DelegatingOAuth2TokenValidator(Collection> tokenValidators) { + Assert.notNull(tokenValidators, "tokenValidators cannot be null"); + + this.tokenValidators = new ArrayList<>(tokenValidators); + } + + /** + * {@inheritDoc} + */ + @Override + public OAuth2TokenValidatorResult validate(T token) { + Collection errors = new ArrayList<>(); + + for ( OAuth2TokenValidator validator : this.tokenValidators) { + errors.addAll(validator.validate(token).getErrors()); + } + + return OAuth2TokenValidatorResult.failure(errors); + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenValidator.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenValidator.java new file mode 100644 index 00000000000..769f351a7ba --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenValidator.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.core; + +/** + * Implementations of this interface are responsible for "verifying" + * the validity and/or constraints of the attributes contained in an OAuth 2.0 Token. + * + * @author Joe Grandja + * @author Josh Cummings + * @since 5.1 + */ +public interface OAuth2TokenValidator { + + /** + * Verify the validity and/or constraints of the provided OAuth 2.0 Token. + * + * @param token an OAuth 2.0 token + * @return OAuth2TokenValidationResult the success or failure detail of the validation + */ + OAuth2TokenValidatorResult validate(T token); +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenValidatorResult.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenValidatorResult.java new file mode 100644 index 00000000000..247fbe391be --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenValidatorResult.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.core; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import org.springframework.util.Assert; + +/** + * A result emitted from an {@link OAuth2TokenValidator} validation attempt + * + * @author Josh Cummings + * @since 5.1 + */ +public final class OAuth2TokenValidatorResult { + static final OAuth2TokenValidatorResult NO_ERRORS = new OAuth2TokenValidatorResult(Collections.emptyList()); + + private final Collection errors; + + private OAuth2TokenValidatorResult(Collection errors) { + Assert.notNull(errors, "errors cannot be null"); + this.errors = new ArrayList<>(errors); + } + + /** + * Say whether this result indicates success + * + * @return whether this result has errors + */ + public boolean hasErrors() { + return !this.errors.isEmpty(); + } + + /** + * Return error details regarding the validation attempt + * + * @return the collection of results in this result, if any; returns an empty list otherwise + */ + public Collection getErrors() { + return this.errors; + } + + /** + * Construct a successful {@link OAuth2TokenValidatorResult} + * + * @return an {@link OAuth2TokenValidatorResult} with no errors + */ + public static OAuth2TokenValidatorResult success() { + return NO_ERRORS; + } + + /** + * Construct a failure {@link OAuth2TokenValidatorResult} with the provided detail + * + * @param errors the list of errors + * @return an {@link OAuth2TokenValidatorResult} with the errors specified + */ + public static OAuth2TokenValidatorResult failure(OAuth2Error... errors) { + return failure(Arrays.asList(errors)); + } + + /** + * Construct a failure {@link OAuth2TokenValidatorResult} with the provided detail + * + * @param errors the list of errors + * @return an {@link OAuth2TokenValidatorResult} with the errors specified + */ + public static OAuth2TokenValidatorResult failure(Collection errors) { + if (errors.isEmpty()) { + return NO_ERRORS; + } + + return new OAuth2TokenValidatorResult(errors); + } +} diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/DelegatingOAuth2TokenValidatorTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/DelegatingOAuth2TokenValidatorTests.java new file mode 100644 index 00000000000..3203b66932e --- /dev/null +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/DelegatingOAuth2TokenValidatorTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.core; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Test; + +import org.springframework.security.oauth2.core.AbstractOAuth2Token; +import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for verifying {@link DelegatingOAuth2TokenValidator} + * + * @author Josh Cummings + */ +public class DelegatingOAuth2TokenValidatorTests { + private static final OAuth2Error DETAIL = new OAuth2Error( + "error", "description", "uri"); + + @Test + public void validateWhenNoValidatorsConfiguredThenReturnsSuccessfulResult() { + DelegatingOAuth2TokenValidator tokenValidator = + new DelegatingOAuth2TokenValidator<>(Collections.emptyList()); + AbstractOAuth2Token token = mock(AbstractOAuth2Token.class); + + assertThat(tokenValidator.validate(token).hasErrors()).isFalse(); + } + + @Test + public void validateWhenAnyValidatorFailsThenReturnsFailureResultContainingDetailFromFailingValidator() { + OAuth2TokenValidator success = mock(OAuth2TokenValidator.class); + OAuth2TokenValidator failure = mock(OAuth2TokenValidator.class); + + when(success.validate(any(AbstractOAuth2Token.class))) + .thenReturn(OAuth2TokenValidatorResult.success()); + when(failure.validate(any(AbstractOAuth2Token.class))) + .thenReturn(OAuth2TokenValidatorResult.failure(DETAIL)); + + DelegatingOAuth2TokenValidator tokenValidator = + new DelegatingOAuth2TokenValidator<>(Arrays.asList(success, failure)); + AbstractOAuth2Token token = mock(AbstractOAuth2Token.class); + + OAuth2TokenValidatorResult result = + tokenValidator.validate(token); + + assertThat(result.hasErrors()).isTrue(); + assertThat(result.getErrors()).containsExactly(DETAIL); + } + + @Test + public void validateWhenMultipleValidatorsFailThenReturnsFailureResultContainingAllDetails() { + OAuth2TokenValidator firstFailure = mock(OAuth2TokenValidator.class); + OAuth2TokenValidator secondFailure = mock(OAuth2TokenValidator.class); + + OAuth2Error otherDetail = new OAuth2Error("another-error"); + + when(firstFailure.validate(any(AbstractOAuth2Token.class))) + .thenReturn(OAuth2TokenValidatorResult.failure(DETAIL)); + when(secondFailure.validate(any(AbstractOAuth2Token.class))) + .thenReturn(OAuth2TokenValidatorResult.failure(otherDetail)); + + DelegatingOAuth2TokenValidator tokenValidator = + new DelegatingOAuth2TokenValidator<>(Arrays.asList(firstFailure, secondFailure)); + AbstractOAuth2Token token = mock(AbstractOAuth2Token.class); + + OAuth2TokenValidatorResult result = + tokenValidator.validate(token); + + assertThat(result.hasErrors()).isTrue(); + assertThat(result.getErrors()).containsExactly(DETAIL, otherDetail); + } + + @Test + public void validateWhenAllValidatorsSucceedThenReturnsSuccessfulResult() { + OAuth2TokenValidator firstSuccess = mock(OAuth2TokenValidator.class); + OAuth2TokenValidator secondSuccess = mock(OAuth2TokenValidator.class); + + when(firstSuccess.validate(any(AbstractOAuth2Token.class))) + .thenReturn(OAuth2TokenValidatorResult.success()); + when(secondSuccess.validate(any(AbstractOAuth2Token.class))) + .thenReturn(OAuth2TokenValidatorResult.success()); + + DelegatingOAuth2TokenValidator tokenValidator = + new DelegatingOAuth2TokenValidator<>(Arrays.asList(firstSuccess, secondSuccess)); + AbstractOAuth2Token token = mock(AbstractOAuth2Token.class); + + OAuth2TokenValidatorResult result = + tokenValidator.validate(token); + + assertThat(result.hasErrors()).isFalse(); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void constructorWhenInvokedWithNullValidatorListThenThrowsIllegalArgumentException() { + assertThatCode(() -> new DelegatingOAuth2TokenValidator<>(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/OAuth2TokenValidatorResultTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/OAuth2TokenValidatorResultTests.java new file mode 100644 index 00000000000..dd43a31da32 --- /dev/null +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/OAuth2TokenValidatorResultTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.core; + +import org.junit.Test; + +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for verifying {@link OAuth2TokenValidatorResult} + * + * @author Josh Cummings + */ +public class OAuth2TokenValidatorResultTests { + private static final OAuth2Error DETAIL = new OAuth2Error( + "error", "description", "uri"); + + @Test + public void successWhenInvokedThenReturnsSuccessfulResult() { + OAuth2TokenValidatorResult success = OAuth2TokenValidatorResult.success(); + assertThat(success.hasErrors()).isFalse(); + } + + @Test + public void failureWhenInvokedWithDetailReturnsFailureResultIncludingDetail() { + OAuth2TokenValidatorResult failure = OAuth2TokenValidatorResult.failure(DETAIL); + + assertThat(failure.hasErrors()).isTrue(); + assertThat(failure.getErrors()).containsExactly(DETAIL); + } + + @Test + public void failureWhenInvokedWithMultipleDetailsReturnsFailureResultIncludingAll() { + OAuth2TokenValidatorResult failure = OAuth2TokenValidatorResult.failure(DETAIL, DETAIL); + + assertThat(failure.hasErrors()).isTrue(); + assertThat(failure.getErrors()).containsExactly(DETAIL, DETAIL); + } +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtIssuerValidator.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtIssuerValidator.java new file mode 100644 index 00000000000..6abd9a49459 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtIssuerValidator.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.jwt; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.util.Assert; + +/** + * Validates the "iss" claim in a {@link Jwt}, that is matches a configured value + * + * @author Josh Cummings + * @since 5.1 + */ +public final class JwtIssuerValidator implements OAuth2TokenValidator { + private static OAuth2Error INVALID_ISSUER = + new OAuth2Error( + OAuth2ErrorCodes.INVALID_REQUEST, + "This iss claim is not equal to the configured issuer", + "https://tools.ietf.org/html/rfc6750#section-3.1"); + + private final URL issuer; + + /** + * Constructs a {@link JwtIssuerValidator} using the provided parameters + * + * @param issuer - The issuer that each {@link Jwt} should have. + */ + public JwtIssuerValidator(String issuer) { + Assert.notNull(issuer, "issuer cannot be null"); + + try { + this.issuer = new URL(issuer); + } catch (MalformedURLException ex) { + throw new IllegalArgumentException( + "Invalid Issuer URL " + issuer + " : " + ex.getMessage(), + ex); + } + } + + /** + * {@inheritDoc} + */ + @Override + public OAuth2TokenValidatorResult validate(Jwt token) { + Assert.notNull(token, "token cannot be null"); + + if (this.issuer.equals(token.getIssuer())) { + return OAuth2TokenValidatorResult.success(); + } else { + return OAuth2TokenValidatorResult.failure(INVALID_ISSUER); + } + } +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtTimestampValidator.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtTimestampValidator.java new file mode 100644 index 00000000000..84ae6eb94d4 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtTimestampValidator.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.jwt; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.util.Assert; + +/** + * An implementation of {@see OAuth2TokenValidator} for verifying claims in a Jwt-based access token + * + *

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

      +	 * 	if ( result.hasErrors() ) {
      +	 *  	Collection errors = result.getErrors();
      +	 *  	throw new JwtValidationException(errors.iterator().next().getDescription(), errors);
      +	 * 	}
      +	 * 
      + * + * @param message - the exception message + * @param errors - a list of {@link OAuth2Error}s with extra detail about the validation result + */ + public JwtValidationException(String message, Collection errors) { + super(message); + + Assert.notEmpty(errors, "errors cannot be empty"); + this.errors = new ArrayList<>(errors); + } + + /** + * Return the list of {@link OAuth2Error}s associated with this exception + * @return the list of {@link OAuth2Error}s associated with this exception + */ + public Collection getErrors() { + return this.errors; + } +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidators.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidators.java new file mode 100644 index 00000000000..3e8782130a8 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidators.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.jwt; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; + +/** + * @author Josh Cummings + * @since 5.1 + */ +public final class JwtValidators { + + /** + * Create a {@link Jwt} Validator that contains all standard validators as well as + * any supplied in the parameter list. + * + * @param jwtValidators - additional validators to include in the delegating validator + * @return - a delegating validator containing all standard validators as well as any supplied + */ + public static OAuth2TokenValidator createDelegatingJwtValidator(OAuth2TokenValidator... jwtValidators) { + Collection> validators = new ArrayList<>(); + validators.add(new JwtTimestampValidator()); + validators.addAll(Arrays.asList(jwtValidators)); + return new DelegatingOAuth2TokenValidator<>(validators); + } + + private JwtValidators() {} +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java index dff7d9b95ad..1bbc126fa9a 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java @@ -15,6 +15,15 @@ */ package org.springframework.security.oauth2.jwt; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.text.ParseException; +import java.time.Instant; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.RemoteKeySourceException; import com.nimbusds.jose.jwk.source.JWKSource; @@ -30,25 +39,19 @@ import com.nimbusds.jwt.SignedJWT; import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; import com.nimbusds.jwt.proc.DefaultJWTProcessor; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; import org.springframework.util.Assert; import org.springframework.web.client.RestOperations; import org.springframework.web.client.RestTemplate; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.text.ParseException; -import java.time.Instant; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; - /** * An implementation of a {@link JwtDecoder} that "decodes" a * JSON Web Token (JWT) and additionally verifies it's digital signature if the JWT is a @@ -75,6 +78,8 @@ public final class NimbusJwtDecoderJwkSupport implements JwtDecoder { private final ConfigurableJWTProcessor jwtProcessor; private final RestOperationsResourceRetriever jwkSetRetriever = new RestOperationsResourceRetriever(); + private OAuth2TokenValidator jwtValidator = JwtValidators.createDelegatingJwtValidator(); + /** * Constructs a {@code NimbusJwtDecoderJwkSupport} using the provided parameters. * @@ -104,17 +109,31 @@ public NimbusJwtDecoderJwkSupport(String jwkSetUrl, String jwsAlgorithm) { new JWSVerificationKeySelector<>(this.jwsAlgorithm, jwkSource); this.jwtProcessor = new DefaultJWTProcessor<>(); this.jwtProcessor.setJWSKeySelector(jwsKeySelector); + + // Spring Security validates the claim set independent from Nimbus + this.jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {}); } @Override public Jwt decode(String token) throws JwtException { JWT jwt = this.parse(token); if (jwt instanceof SignedJWT) { - return this.createJwt(token, jwt); + Jwt createdJwt = this.createJwt(token, jwt); + return this.validateJwt(createdJwt); } throw new JwtException("Unsupported algorithm of " + jwt.getHeader().getAlgorithm()); } + /** + * Use this {@link Jwt} Validator + * + * @param jwtValidator - the Jwt Validator to use + */ + public void setJwtValidator(OAuth2TokenValidator jwtValidator) { + Assert.notNull(jwtValidator, "jwtValidator cannot be null"); + this.jwtValidator = jwtValidator; + } + private JWT parse(String token) { try { return JWTParser.parse(token); @@ -163,6 +182,18 @@ private Jwt createJwt(String token, JWT parsedJwt) { return jwt; } + private Jwt validateJwt(Jwt jwt){ + OAuth2TokenValidatorResult result = this.jwtValidator.validate(jwt); + if (result.hasErrors()) { + String description = result.getErrors().iterator().next().getDescription(); + throw new JwtValidationException( + String.format(DECODING_ERROR_MESSAGE_TEMPLATE, description), + result.getErrors()); + } + + return jwt; + } + /** * Sets the {@link RestOperations} used when requesting the JSON Web Key (JWK) Set. * diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtIssuerValidatorTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtIssuerValidatorTests.java new file mode 100644 index 00000000000..7a01da149e2 --- /dev/null +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtIssuerValidatorTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.jwt; + +import java.time.Instant; +import java.util.Collections; +import java.util.Map; + +import org.junit.Test; + +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.jwt.JwtIssuerValidator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * @author Josh Cummings + * @since 5.1 + */ +public class JwtIssuerValidatorTests { + private static final String MOCK_TOKEN = "token"; + private static final Instant MOCK_ISSUED_AT = Instant.MIN; + private static final Instant MOCK_EXPIRES_AT = Instant.MAX; + private static final Map MOCK_HEADERS = + Collections.singletonMap("alg", JwsAlgorithms.RS256); + + private static final String ISSUER = "https://issuer"; + + private final JwtIssuerValidator validator = new JwtIssuerValidator(ISSUER); + + @Test + public void validateWhenIssuerMatchesThenReturnsSuccess() { + Jwt jwt = new Jwt( + MOCK_TOKEN, + MOCK_ISSUED_AT, + MOCK_EXPIRES_AT, + MOCK_HEADERS, + Collections.singletonMap("iss", ISSUER)); + + assertThat(this.validator.validate(jwt)) + .isEqualTo(OAuth2TokenValidatorResult.success()); + } + + @Test + public void validateWhenIssuerMismatchesThenReturnsError() { + Jwt jwt = new Jwt( + MOCK_TOKEN, + MOCK_ISSUED_AT, + MOCK_EXPIRES_AT, + MOCK_HEADERS, + Collections.singletonMap(JwtClaimNames.ISS, "https://other")); + + OAuth2TokenValidatorResult result = this.validator.validate(jwt); + + assertThat(result.getErrors()).isNotEmpty(); + } + + @Test + public void validateWhenJwtIsNullThenThrowsIllegalArgumentException() { + assertThatCode(() -> this.validator.validate(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenMalformedIssuerIsGivenThenThrowsIllegalArgumentException() { + assertThatCode(() -> new JwtIssuerValidator("issuer")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenNullIssuerIsGivenThenThrowsIllegalArgumentException() { + assertThatCode(() -> new JwtIssuerValidator(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java new file mode 100644 index 00000000000..57fe14b1257 --- /dev/null +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java @@ -0,0 +1,230 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.jwt; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.Test; + +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.jwt.JwtTimestampValidator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * Tests verifying {@link JwtTimestampValidator} + * + * @author Josh Cummings + */ +public class JwtTimestampValidatorTests { + private static final Clock MOCK_NOW = Clock.fixed(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + private static final String MOCK_TOKEN_VALUE = "token"; + private static final Instant MOCK_ISSUED_AT = Instant.MIN; + private static final Map MOCK_HEADER = Collections.singletonMap("alg", JwsAlgorithms.RS256); + private static final Map MOCK_CLAIM_SET = Collections.singletonMap("some", "claim"); + + @Test + public void validateWhenJwtIsExpiredThenErrorMessageIndicatesExpirationTime() { + Instant oneHourAgo = Instant.now().minusSeconds(3600); + + Jwt jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + oneHourAgo, + MOCK_HEADER, + MOCK_CLAIM_SET); + + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); + + Collection details = jwtValidator.validate(jwt).getErrors(); + Collection messages = details.stream().map(OAuth2Error::getDescription).collect(Collectors.toList()); + + assertThat(messages).contains("Jwt expired at " + oneHourAgo); + } + + @Test + public void validateWhenJwtIsTooEarlyThenErrorMessageIndicatesNotBeforeTime() { + Instant oneHourFromNow = Instant.now().plusSeconds(3600); + + Jwt jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + null, + MOCK_HEADER, + Collections.singletonMap(JwtClaimNames.NBF, oneHourFromNow)); + + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); + + Collection details = jwtValidator.validate(jwt).getErrors(); + Collection messages = details.stream().map(OAuth2Error::getDescription).collect(Collectors.toList()); + + assertThat(messages).contains("Jwt used before " + oneHourFromNow); + } + + @Test + public void validateWhenConfiguredWithClockSkewThenValidatesUsingThatSkew() { + Duration oneDayOff = Duration.ofDays(1); + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(oneDayOff); + + Instant now = Instant.now(); + Instant almostOneDayAgo = now.minus(oneDayOff).plusSeconds(10); + Instant almostOneDayFromNow = now.plus(oneDayOff).minusSeconds(10); + Instant justOverOneDayAgo = now.minus(oneDayOff).minusSeconds(10); + Instant justOverOneDayFromNow = now.plus(oneDayOff).plusSeconds(10); + + Jwt jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + almostOneDayAgo, + MOCK_HEADER, + Collections.singletonMap(JwtClaimNames.NBF, almostOneDayFromNow)); + + assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); + + jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + justOverOneDayAgo, + MOCK_HEADER, + MOCK_CLAIM_SET); + + OAuth2TokenValidatorResult result = jwtValidator.validate(jwt); + Collection messages = + result.getErrors().stream().map(OAuth2Error::getDescription).collect(Collectors.toList()); + + assertThat(result.hasErrors()).isTrue(); + assertThat(messages).contains("Jwt expired at " + justOverOneDayAgo); + + jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + null, + MOCK_HEADER, + Collections.singletonMap(JwtClaimNames.NBF, justOverOneDayFromNow)); + + result = jwtValidator.validate(jwt); + messages = + result.getErrors().stream().map(OAuth2Error::getDescription).collect(Collectors.toList()); + + assertThat(result.hasErrors()).isTrue(); + assertThat(messages).contains("Jwt used before " + justOverOneDayFromNow); + + } + + @Test + public void validateWhenConfiguredWithFixedClockThenValidatesUsingFixedTime() { + Jwt jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + Instant.now(MOCK_NOW), + MOCK_HEADER, + Collections.singletonMap("some", "claim")); + + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(Duration.ofNanos(0)); + jwtValidator.setClock(MOCK_NOW); + + assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); + + jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + null, + MOCK_HEADER, + Collections.singletonMap(JwtClaimNames.NBF, Instant.now(MOCK_NOW))); + + assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); + } + + @Test + public void validateWhenNeitherExpiryNorNotBeforeIsSpecifiedThenReturnsSuccessfulResult() { + Jwt jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + null, + MOCK_HEADER, + MOCK_CLAIM_SET); + + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); + assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); + } + + @Test + public void validateWhenNotBeforeIsValidAndExpiryIsNotSpecifiedThenReturnsSuccessfulResult() { + Jwt jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + null, + MOCK_HEADER, + Collections.singletonMap(JwtClaimNames.NBF, Instant.MIN)); + + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); + assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); + } + + @Test + public void validateWhenExpiryIsValidAndNotBeforeIsNotSpecifiedThenReturnsSuccessfulResult() { + Jwt jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + Instant.MAX, + MOCK_HEADER, + MOCK_CLAIM_SET); + + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); + assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); + } + + @Test + public void validateWhenBothExpiryAndNotBeforeAreValidThenReturnsSuccessfulResult() { + Jwt jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + Instant.now(MOCK_NOW), + MOCK_HEADER, + Collections.singletonMap(JwtClaimNames.NBF, Instant.now(MOCK_NOW))); + + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(Duration.ofNanos(0)); + jwtValidator.setClock(MOCK_NOW); + + assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); + } + + @Test + public void setClockWhenInvokedWithNullThenThrowsIllegalArgumentException() { + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); + + assertThatCode(() -> jwtValidator.setClock(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenInvokedWithNullDurationThenThrowsIllegalArgumentException() { + assertThatCode(() -> new JwtTimestampValidator(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java index ecd648f513f..bc3f36932e6 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java @@ -15,6 +15,8 @@ */ package org.springframework.security.oauth2.jwt; +import java.util.Arrays; + import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; import com.nimbusds.jwt.JWT; @@ -30,16 +32,25 @@ import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; + import org.springframework.http.RequestEntity; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -import static org.powermock.api.mockito.PowerMockito.*; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.spy; +import static org.powermock.api.mockito.PowerMockito.when; +import static org.powermock.api.mockito.PowerMockito.whenNew; /** * Tests for {@link NimbusJwtDecoderJwkSupport}. @@ -174,4 +185,47 @@ public void decodeWhenCustomRestOperationsSetThenUsed() throws Exception { server.shutdown(); } } + + @Test + public void decodeWhenJwtFailsValidationThenReturnsCorrespondingErrorMessage() throws Exception { + try ( MockWebServer server = new MockWebServer() ) { + server.enqueue(new MockResponse().setBody(JWK_SET)); + String jwkSetUrl = server.url("/.well-known/jwks.json").toString(); + + NimbusJwtDecoderJwkSupport decoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl); + + OAuth2Error failure = new OAuth2Error("mock-error", "mock-description", "mock-uri"); + + OAuth2TokenValidator jwtValidator = mock(OAuth2TokenValidator.class); + when(jwtValidator.validate(any(Jwt.class))).thenReturn(OAuth2TokenValidatorResult.failure(failure)); + decoder.setJwtValidator(jwtValidator); + + assertThatCode(() -> decoder.decode(SIGNED_JWT)) + .isInstanceOf(JwtValidationException.class) + .hasMessageContaining("mock-description"); + } + } + + @Test + public void decodeWhenJwtValidationHasTwoErrorsThenJwtExceptionMessageShowsFirstError() throws Exception { + try ( MockWebServer server = new MockWebServer() ) { + server.enqueue(new MockResponse().setBody(JWK_SET)); + String jwkSetUrl = server.url("/.well-known/jwks.json").toString(); + + NimbusJwtDecoderJwkSupport decoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl); + + OAuth2Error firstFailure = new OAuth2Error("mock-error", "mock-description", "mock-uri"); + OAuth2Error secondFailure = new OAuth2Error("another-error", "another-description", "another-uri"); + OAuth2TokenValidatorResult result = OAuth2TokenValidatorResult.failure(firstFailure, secondFailure); + + OAuth2TokenValidator jwtValidator = mock(OAuth2TokenValidator.class); + when(jwtValidator.validate(any(Jwt.class))).thenReturn(result); + decoder.setJwtValidator(jwtValidator); + + assertThatCode(() -> decoder.decode(SIGNED_JWT)) + .isInstanceOf(JwtValidationException.class) + .hasMessageContaining("mock-description") + .hasFieldOrPropertyWithValue("errors", Arrays.asList(firstFailure, secondFailure)); + } + } } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.java index b70bd2accde..fafb1114225 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.java @@ -61,6 +61,9 @@ public final class JwtAuthenticationProvider implements AuthenticationProvider { private final JwtConverter jwtConverter = new JwtConverter(); + private static final OAuth2Error DEFAULT_INVALID_TOKEN = + invalidToken("An error occurred while attempting to decode the Jwt: Invalid token"); + public JwtAuthenticationProvider(JwtDecoder jwtDecoder) { Assert.notNull(jwtDecoder, "jwtDecoder cannot be null"); @@ -84,15 +87,10 @@ public Authentication authenticate(Authentication authentication) throws Authent try { jwt = this.jwtDecoder.decode(bearer.getToken()); } catch (JwtException failed) { - OAuth2Error invalidToken; - try { - invalidToken = invalidToken(failed.getMessage()); - } catch ( IllegalArgumentException malformed ) { - // some third-party library error messages are not suitable for RFC 6750's error message charset - invalidToken = invalidToken("An error occurred while attempting to decode the Jwt: Invalid token"); - } - throw new OAuth2AuthenticationException(invalidToken, failed); + OAuth2Error invalidToken = invalidToken(failed.getMessage()); + throw new OAuth2AuthenticationException(invalidToken, invalidToken.getDescription(), failed); } + JwtAuthenticationToken token = this.jwtConverter.convert(jwt); token.setDetails(bearer.getDetails()); @@ -108,10 +106,15 @@ public boolean supports(Class authentication) { } private static OAuth2Error invalidToken(String message) { - return new BearerTokenError( - BearerTokenErrorCodes.INVALID_TOKEN, - HttpStatus.UNAUTHORIZED, - message, - "https://tools.ietf.org/html/rfc6750#section-3.1"); + try { + return new BearerTokenError( + BearerTokenErrorCodes.INVALID_TOKEN, + HttpStatus.UNAUTHORIZED, + message, + "https://tools.ietf.org/html/rfc6750#section-3.1"); + } catch (IllegalArgumentException malformed) { + // some third-party library error messages are not suitable for RFC 6750's error message charset + return DEFAULT_INVALID_TOKEN; + } } } From b0e6881db8fbab08a1b96ce831d1b44115f3e673 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 16 Aug 2018 11:55:22 -0500 Subject: [PATCH 226/226] Polish JwtValidators The current name of createDelegatingJwtValidator is not intuitive. The name implies it is just creating a DelegatingOAuth2TokenValidator with no mention that JwtTimestampValidator is being added. To resolve this, the arguments have been removed and only JwtTimestampValidator is added. User's needing additional validators can add the result of this method to DelegatingOAuth2TokenValidator along with the additional validators they wish to use. The method name has been renamed to createDefault which now accurately reflects what is created. There is no need to have JwtValidator at the end of the method since the method is located in JwtValidators. The commit also adds createDefaultWithIssuer for creating with a specific issuer. Issue: gh-5133 --- .../security/oauth2/jwt/JwtValidators.java | 36 ++++++++++++++----- .../jwt/NimbusJwtDecoderJwkSupport.java | 2 +- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidators.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidators.java index 3e8782130a8..95667bbf445 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidators.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidators.java @@ -17,30 +17,50 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; +import java.util.List; import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidator; /** + * Provides factory methods for creating {@code OAuth2TokenValidator} * @author Josh Cummings + * @author Rob Winch * @since 5.1 */ public final class JwtValidators { /** - * Create a {@link Jwt} Validator that contains all standard validators as well as - * any supplied in the parameter list. - * - * @param jwtValidators - additional validators to include in the delegating validator + *

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

      + *

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

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

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

      + *

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

      + * @return - a delegating validator containing all standard validators as well as any supplied + */ + public static OAuth2TokenValidator createDefault() { + return new DelegatingOAuth2TokenValidator<>(Arrays.asList(new JwtTimestampValidator())); + } + private JwtValidators() {} } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java index 1bbc126fa9a..5dedcbeb7c3 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java @@ -78,7 +78,7 @@ public final class NimbusJwtDecoderJwkSupport implements JwtDecoder { private final ConfigurableJWTProcessor jwtProcessor; private final RestOperationsResourceRetriever jwkSetRetriever = new RestOperationsResourceRetriever(); - private OAuth2TokenValidator jwtValidator = JwtValidators.createDelegatingJwtValidator(); + private OAuth2TokenValidator jwtValidator = JwtValidators.createDefault(); /** * Constructs a {@code NimbusJwtDecoderJwkSupport} using the provided parameters.