From 6c7d49759efcbe63403c2de8480041a3ad866abd Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Sat, 21 Jul 2018 20:48:18 -0400 Subject: [PATCH] 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 32f861983d..a647792354 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 abstract class AbstractAuthenticationFilterConfigurer 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 @@ public abstract class AbstractAuthenticationFilterConfigurer> exten 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 @@ public final class OAuth2LoginConfigurer> exten 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 @@ public final class OAuth2LoginConfigurer> exten 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())); - clientRegistrations.forEach(registration -> authenticationUrlToClientName.put( - authorizationRequestBaseUri + "/" + registration.getRegistrationId(), - registration.getClientName())); - loginPageGeneratingFilter.setOauth2LoginEnabled(true); - loginPageGeneratingFilter.setOauth2AuthenticationUrlToClientName(authenticationUrlToClientName); - loginPageGeneratingFilter.setLoginPageUrl(this.getLoginPage()); - loginPageGeneratingFilter.setFailureUrl(this.getFailureUrl()); + return loginUrlToClientName; + } + + 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 11c41eb90b..01a7111cfc 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.PropertyAccessorFactory; 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 @@ import static org.assertj.core.api.Assertions.assertThat; * 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 class OAuth2LoginConfigurerTests { 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 @@ public class OAuth2LoginConfigurerTests { } 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 @@ public class OAuth2LoginConfigurerTests { http .oauth2Login() .clientRegistrationRepository( - new InMemoryClientRegistrationRepository(CLIENT_REGISTRATION)); + new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION)); super.configure(http); } } @@ -380,7 +447,7 @@ public class OAuth2LoginConfigurerTests { http .oauth2Login() .clientRegistrationRepository( - new InMemoryClientRegistrationRepository(CLIENT_REGISTRATION)) + new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION)) .userInfoEndpoint() .userAuthoritiesMapper(createGrantedAuthoritiesMapper()); super.configure(http); @@ -398,7 +465,7 @@ public class OAuth2LoginConfigurerTests { @Bean ClientRegistrationRepository clientRegistrationRepository() { - return new InMemoryClientRegistrationRepository(CLIENT_REGISTRATION); + return new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION); } @Bean @@ -414,7 +481,7 @@ public class OAuth2LoginConfigurerTests { http .oauth2Login() .clientRegistrationRepository( - new InMemoryClientRegistrationRepository(CLIENT_REGISTRATION)) + new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION)) .loginProcessingUrl("/login/oauth2/*"); super.configure(http); } @@ -423,7 +490,7 @@ public class OAuth2LoginConfigurerTests { @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 @@ public class OAuth2LoginConfigurerTests { } } + @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()