From bf1bbd14e989282d477286156ec54126e07a18bb Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Tue, 9 Jul 2019 10:03:29 -0400 Subject: [PATCH] Allow configuration of openid login through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 122 ++++++++++++ .../openid/OpenIDLoginConfigurer.java | 72 ++++++- .../openid/OpenIDLoginConfigurerTests.java | 178 ++++++++++++++++++ 3 files changed, 370 insertions(+), 2 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 175cf5b11f..d1ab52e23c 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 @@ -239,6 +239,128 @@ public final class HttpSecurity extends return getOrApply(new OpenIDLoginConfigurer<>()); } + /** + * Allows configuring OpenID based authentication. + * + *

Example Configurations

+ * + * A basic example accepting the defaults and not using attribute exchange: + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class OpenIDLoginConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) {
+	 * 		http
+	 * 			.authorizeRequests(authorizeRequests ->
+	 * 				authorizeRequests
+	 * 					.antMatchers("/**").hasRole("USER")
+	 * 			)
+	 * 			.openidLogin(openidLogin ->
+	 * 				openidLogin
+	 * 					.permitAll()
+	 * 			);
+	 * 	}
+	 *
+	 * 	@Override
+	 * 	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
+	 * 		auth.inMemoryAuthentication()
+	 * 				// the username must match the OpenID of the user you are
+	 * 				// logging in with
+	 * 				.withUser(
+	 * 						"https://www.google.com/accounts/o8/id?id=lmkCn9xzPdsxVwG7pjYMuDgNNdASFmobNkcRPaWU")
+	 * 				.password("password").roles("USER");
+	 * 	}
+	 * }
+	 * 
+ * + * A more advanced example demonstrating using attribute exchange and providing a + * custom AuthenticationUserDetailsService that will make any user that authenticates + * a valid user. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class OpenIDLoginConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http.authorizeRequests(authorizeRequests ->
+	 * 				authorizeRequests
+	 * 					.antMatchers("/**").hasRole("USER")
+	 * 			)
+	 * 			.openidLogin(openidLogin ->
+	 * 				openidLogin
+	 * 					.loginPage("/login")
+	 * 					.permitAll()
+	 * 					.authenticationUserDetailsService(
+	 * 						new AutoProvisioningUserDetailsService())
+	 * 					.attributeExchange(googleExchange ->
+	 * 						googleExchange
+	 * 							.identifierPattern("https://www.google.com/.*")
+	 * 							.attribute(emailAttribute ->
+	 * 								emailAttribute
+	 * 									.name("email")
+	 * 									.type("https://axschema.org/contact/email")
+	 * 									.required(true)
+	 * 							)
+	 * 							.attribute(firstnameAttribute ->
+	 * 								firstnameAttribute
+	 * 									.name("firstname")
+	 * 									.type("https://axschema.org/namePerson/first")
+	 * 									.required(true)
+	 * 							)
+	 * 							.attribute(lastnameAttribute ->
+	 * 								lastnameAttribute
+	 * 									.name("lastname")
+	 * 									.type("https://axschema.org/namePerson/last")
+	 * 									.required(true)
+	 * 							)
+	 * 					)
+	 * 					.attributeExchange(yahooExchange ->
+	 * 						yahooExchange
+	 * 							.identifierPattern(".*yahoo.com.*")
+	 * 							.attribute(emailAttribute ->
+	 * 								emailAttribute
+	 * 									.name("email")
+	 * 									.type("https://schema.openid.net/contact/email")
+	 * 									.required(true)
+	 * 							)
+	 * 							.attribute(fullnameAttribute ->
+	 * 								fullnameAttribute
+	 * 									.name("fullname")
+	 * 									.type("https://axschema.org/namePerson")
+	 * 									.required(true)
+	 * 							)
+	 * 					)
+	 * 			);
+	 * 	}
+	 * }
+	 *
+	 * public class AutoProvisioningUserDetailsService implements
+	 * 		AuthenticationUserDetailsService<OpenIDAuthenticationToken> {
+	 * 	public UserDetails loadUserDetails(OpenIDAuthenticationToken token)
+	 * 			throws UsernameNotFoundException {
+	 * 		return new User(token.getName(), "NOTUSED",
+	 * 				AuthorityUtils.createAuthorityList("ROLE_USER"));
+	 * 	}
+	 * }
+	 * 
+ * + * @see OpenIDLoginConfigurer + * + * @param openidLoginCustomizer the {@link Customizer} to provide more options for + * the {@link OpenIDLoginConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity openidLogin(Customizer> openidLoginCustomizer) throws Exception { + openidLoginCustomizer.customize(getOrApply(new OpenIDLoginConfigurer<>())); + return HttpSecurity.this; + } + /** * Adds the Security headers to the response. This is activated by default when using * {@link WebSecurityConfigurerAdapter}'s default constructor. Accepting the 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 71d7c2e215..01e1b3198a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.openid4java.consumer.ConsumerManager; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @@ -148,6 +149,24 @@ public final class OpenIDLoginConfigurer> exten return attributeExchangeConfigurer; } + /** + * Sets up OpenID attribute exchange for OpenIDs matching the specified pattern. + * The default pattern is ".*", it can be specified using + * {@link AttributeExchangeConfigurer#identifierPattern(String)} + * + * @param attributeExchangeCustomizer the {@link Customizer} to provide more options for + * the {@link AttributeExchangeConfigurer} + * @return a {@link OpenIDLoginConfigurer} for further customizations + * @throws Exception + */ + public OpenIDLoginConfigurer attributeExchange(Customizer attributeExchangeCustomizer) + throws Exception { + AttributeExchangeConfigurer attributeExchangeConfigurer = new AttributeExchangeConfigurer(".*"); + attributeExchangeCustomizer.customize(attributeExchangeConfigurer); + this.attributeExchangeConfigurers.add(attributeExchangeConfigurer); + return this; + } + /** * Allows specifying the {@link OpenIDConsumer} to be used. The default is using an * {@link OpenID4JavaConsumer}. @@ -373,7 +392,7 @@ public final class OpenIDLoginConfigurer> exten * @author Rob Winch */ public final class AttributeExchangeConfigurer { - private final String identifier; + private String identifier; private List attributes = new ArrayList<>(); private List attributeConfigurers = new ArrayList<>(); @@ -395,6 +414,19 @@ public final class OpenIDLoginConfigurer> exten return OpenIDLoginConfigurer.this; } + /** + * Sets the regular expression for matching on OpenID's (i.e. + * "https://www.google.com/.*", ".*yahoo.com.*", etc) + * + * @param identifierPattern the regular expression for matching on OpenID's + * @return the {@link AttributeExchangeConfigurer} for further customization of + * attribute exchange + */ + public AttributeExchangeConfigurer identifierPattern(String identifierPattern) { + this.identifier = identifierPattern; + return this; + } + /** * Adds an {@link OpenIDAttribute} to be obtained for the configured OpenID * pattern. @@ -419,6 +451,22 @@ public final class OpenIDLoginConfigurer> exten return attributeConfigurer; } + /** + * Adds an {@link OpenIDAttribute} named "default-attribute". + * The name can by updated using {@link AttributeConfigurer#name(String)}. + * + * @param attributeCustomizer the {@link Customizer} to provide more options for + * the {@link AttributeConfigurer} + * @return a {@link AttributeExchangeConfigurer} for further customizations + * @throws Exception + */ + public AttributeExchangeConfigurer attribute(Customizer attributeCustomizer) throws Exception { + AttributeConfigurer attributeConfigurer = new AttributeConfigurer(); + attributeCustomizer.customize(attributeConfigurer); + this.attributeConfigurers.add(attributeConfigurer); + return this; + } + /** * Gets the {@link OpenIDAttribute}'s for the configured OpenID pattern * @return @@ -443,6 +491,16 @@ public final class OpenIDLoginConfigurer> exten private boolean required = false; private String type; + /** + * Creates a new instance named "default-attribute". + * The name can by updated using {@link #name(String)}. + * + * @see AttributeExchangeConfigurer#attribute(String) + */ + private AttributeConfigurer() { + this.name = "default-attribute"; + } + /** * Creates a new instance * @param name the name of the attribute @@ -486,6 +544,16 @@ public final class OpenIDLoginConfigurer> exten return this; } + /** + * The OpenID attribute name. + * @param name + * @return the {@link AttributeConfigurer} for further customizations + */ + public AttributeConfigurer name(String name) { + this.name = name; + return this; + } + /** * Gets the {@link AttributeExchangeConfigurer} for further customization of * the attributes diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurerTests.java index d18efe0b37..159c54967e 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurerTests.java @@ -16,8 +16,13 @@ package org.springframework.security.config.annotation.web.configurers.openid; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; import org.junit.Rule; import org.junit.Test; +import org.openid4java.consumer.ConsumerManager; +import org.openid4java.discovery.DiscoveryInformation; +import org.openid4java.message.AuthRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.ObjectPostProcessor; @@ -26,13 +31,23 @@ 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.openid.OpenIDAttribute; import org.springframework.security.openid.OpenIDAuthenticationFilter; import org.springframework.security.openid.OpenIDAuthenticationProvider; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.openid4java.discovery.yadis.YadisResolver.YADIS_XRDS_LOCATION; +import static org.springframework.security.config.Customizer.withDefaults; 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; @@ -128,4 +143,167 @@ public class OpenIDLoginConfigurerTests { // @formatter:on } } + + @Test + public void requestWhenOpenIdLoginPageInLambdaThenRedirectsToLoginPAge() throws Exception { + this.spring.register(OpenIdLoginPageInLambdaConfig.class).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login/custom")); + } + + @EnableWebSecurity + static class OpenIdLoginPageInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().authenticated() + ) + .openidLogin(openIdLogin -> + openIdLogin + .loginPage("/login/custom") + ); + // @formatter:on + } + } + + @Test + public void requestWhenAttributeExchangeConfiguredThenFetchAttributesMatchAttributeList() throws Exception { + OpenIdAttributesInLambdaConfig.CONSUMER_MANAGER = mock(ConsumerManager.class); + AuthRequest mockAuthRequest = mock(AuthRequest.class); + DiscoveryInformation mockDiscoveryInformation = mock(DiscoveryInformation.class); + when(mockAuthRequest.getDestinationUrl(anyBoolean())).thenReturn("mockUrl"); + when(OpenIdAttributesInLambdaConfig.CONSUMER_MANAGER.associate(any())) + .thenReturn(mockDiscoveryInformation); + when(OpenIdAttributesInLambdaConfig.CONSUMER_MANAGER.authenticate(any(DiscoveryInformation.class), any(), any())) + .thenReturn(mockAuthRequest); + this.spring.register(OpenIdAttributesInLambdaConfig.class).autowire(); + + 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))); + + MvcResult mvcResult = this.mvc.perform(get("/login/openid") + .param(OpenIDAuthenticationFilter.DEFAULT_CLAIMED_IDENTITY_FIELD, endpoint)) + .andExpect(status().isFound()) + .andReturn(); + + Object attributeObject = mvcResult.getRequest().getSession().getAttribute("SPRING_SECURITY_OPEN_ID_ATTRIBUTES_FETCH_LIST"); + assertThat(attributeObject).isInstanceOf(List.class); + List attributeList = (List) attributeObject; + assertThat(attributeList.stream().anyMatch(attribute -> + "nickname".equals(attribute.getName()) + && "https://schema.openid.net/namePerson/friendly".equals(attribute.getType()))) + .isTrue(); + assertThat(attributeList.stream().anyMatch(attribute -> + "email".equals(attribute.getName()) + && "https://schema.openid.net/contact/email".equals(attribute.getType()) + && attribute.isRequired() + && attribute.getCount() == 2)) + .isTrue(); + } + } + + @EnableWebSecurity + static class OpenIdAttributesInLambdaConfig extends WebSecurityConfigurerAdapter { + static ConsumerManager CONSUMER_MANAGER; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().permitAll() + ) + .openidLogin(openIdLogin -> + openIdLogin + .consumerManager(CONSUMER_MANAGER) + .attributeExchange(attributeExchange -> + attributeExchange + .identifierPattern(".*") + .attribute(nicknameAttribute -> + nicknameAttribute + .name("nickname") + .type("https://schema.openid.net/namePerson/friendly") + ) + .attribute(emailAttribute -> + emailAttribute + .name("email") + .type("https://schema.openid.net/contact/email") + .required(true) + .count(2) + ) + ) + ); + // @formatter:on + } + } + + @Test + public void requestWhenAttributeNameNotSpecifiedThenAttributeNameDefaulted() + throws Exception { + OpenIdAttributesNullNameConfig.CONSUMER_MANAGER = mock(ConsumerManager.class); + AuthRequest mockAuthRequest = mock(AuthRequest.class); + DiscoveryInformation mockDiscoveryInformation = mock(DiscoveryInformation.class); + when(mockAuthRequest.getDestinationUrl(anyBoolean())).thenReturn("mockUrl"); + when(OpenIdAttributesNullNameConfig.CONSUMER_MANAGER.associate(any())) + .thenReturn(mockDiscoveryInformation); + when(OpenIdAttributesNullNameConfig.CONSUMER_MANAGER.authenticate(any(DiscoveryInformation.class), any(), any())) + .thenReturn(mockAuthRequest); + this.spring.register(OpenIdAttributesNullNameConfig.class).autowire(); + + 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))); + + MvcResult mvcResult = this.mvc.perform(get("/login/openid") + .param(OpenIDAuthenticationFilter.DEFAULT_CLAIMED_IDENTITY_FIELD, endpoint)) + .andExpect(status().isFound()) + .andReturn(); + + Object attributeObject = mvcResult.getRequest().getSession().getAttribute("SPRING_SECURITY_OPEN_ID_ATTRIBUTES_FETCH_LIST"); + assertThat(attributeObject).isInstanceOf(List.class); + List attributeList = (List) attributeObject; + assertThat(attributeList).hasSize(1); + assertThat(attributeList.get(0).getName()).isEqualTo("default-attribute"); + } + } + + @EnableWebSecurity + static class OpenIdAttributesNullNameConfig extends WebSecurityConfigurerAdapter { + static ConsumerManager CONSUMER_MANAGER; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().permitAll() + ) + .openidLogin(openIdLogin -> + openIdLogin + .consumerManager(CONSUMER_MANAGER) + .attributeExchange(attributeExchange -> + attributeExchange + .identifierPattern(".*") + .attribute(withDefaults()) + ) + ); + // @formatter:on + } + } }