浏览代码

Allow configuration of openid login through nested builder

Issue: gh-5557
Eleftheria Stein 6 年之前
父节点
当前提交
bf1bbd14e9

+ 122 - 0
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.
+	 *
+	 * <h2>Example Configurations</h2>
+	 *
+	 * A basic example accepting the defaults and not using attribute exchange:
+	 *
+	 * <pre>
+	 * &#064;Configuration
+	 * &#064;EnableWebSecurity
+	 * public class OpenIDLoginConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	&#064;Override
+	 * 	protected void configure(HttpSecurity http) {
+	 * 		http
+	 * 			.authorizeRequests(authorizeRequests ->
+	 * 				authorizeRequests
+	 * 					.antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;)
+	 * 			)
+	 * 			.openidLogin(openidLogin ->
+	 * 				openidLogin
+	 * 					.permitAll()
+	 * 			);
+	 * 	}
+	 *
+	 * 	&#064;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(
+	 * 						&quot;https://www.google.com/accounts/o8/id?id=lmkCn9xzPdsxVwG7pjYMuDgNNdASFmobNkcRPaWU&quot;)
+	 * 				.password(&quot;password&quot;).roles(&quot;USER&quot;);
+	 * 	}
+	 * }
+	 * </pre>
+	 *
+	 * A more advanced example demonstrating using attribute exchange and providing a
+	 * custom AuthenticationUserDetailsService that will make any user that authenticates
+	 * a valid user.
+	 *
+	 * <pre>
+	 * &#064;Configuration
+	 * &#064;EnableWebSecurity
+	 * public class OpenIDLoginConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	&#064;Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http.authorizeRequests(authorizeRequests ->
+	 * 				authorizeRequests
+	 * 					.antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;)
+	 * 			)
+	 * 			.openidLogin(openidLogin ->
+	 * 				openidLogin
+	 * 					.loginPage(&quot;/login&quot;)
+	 * 					.permitAll()
+	 * 					.authenticationUserDetailsService(
+	 * 						new AutoProvisioningUserDetailsService())
+	 * 					.attributeExchange(googleExchange ->
+	 * 						googleExchange
+	 * 							.identifierPattern(&quot;https://www.google.com/.*&quot;)
+	 * 							.attribute(emailAttribute ->
+	 * 								emailAttribute
+	 * 									.name(&quot;email&quot;)
+	 * 									.type(&quot;https://axschema.org/contact/email&quot;)
+	 * 									.required(true)
+	 * 							)
+	 * 							.attribute(firstnameAttribute ->
+	 * 								firstnameAttribute
+	 * 									.name(&quot;firstname&quot;)
+	 * 									.type(&quot;https://axschema.org/namePerson/first&quot;)
+	 * 									.required(true)
+	 * 							)
+	 * 							.attribute(lastnameAttribute ->
+	 * 								lastnameAttribute
+	 * 									.name(&quot;lastname&quot;)
+	 * 									.type(&quot;https://axschema.org/namePerson/last&quot;)
+	 * 									.required(true)
+	 * 							)
+	 * 					)
+	 * 					.attributeExchange(yahooExchange ->
+	 * 						yahooExchange
+	 * 							.identifierPattern(&quot;.*yahoo.com.*&quot;)
+	 * 							.attribute(emailAttribute ->
+	 * 								emailAttribute
+	 * 									.name(&quot;email&quot;)
+	 * 									.type(&quot;https://schema.openid.net/contact/email&quot;)
+	 * 									.required(true)
+	 * 							)
+	 * 							.attribute(fullnameAttribute ->
+	 * 								fullnameAttribute
+	 * 									.name(&quot;fullname&quot;)
+	 * 									.type(&quot;https://axschema.org/namePerson&quot;)
+	 * 									.required(true)
+	 * 							)
+	 * 					)
+	 * 			);
+	 * 	}
+	 * }
+	 *
+	 * public class AutoProvisioningUserDetailsService implements
+	 * 		AuthenticationUserDetailsService&lt;OpenIDAuthenticationToken&gt; {
+	 * 	public UserDetails loadUserDetails(OpenIDAuthenticationToken token)
+	 * 			throws UsernameNotFoundException {
+	 * 		return new User(token.getName(), &quot;NOTUSED&quot;,
+	 * 				AuthorityUtils.createAuthorityList(&quot;ROLE_USER&quot;));
+	 * 	}
+	 * }
+	 * </pre>
+	 *
+	 * @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<OpenIDLoginConfigurer<HttpSecurity>> 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

+ 70 - 2
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<H extends HttpSecurityBuilder<H>> exten
 		return attributeExchangeConfigurer;
 	}
 
+	/**
+	 * Sets up OpenID attribute exchange for OpenIDs matching the specified pattern.
+	 * The default pattern is &quot;.*&quot;, 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<H> attributeExchange(Customizer<AttributeExchangeConfigurer> 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<H extends HttpSecurityBuilder<H>> exten
 	 * @author Rob Winch
 	 */
 	public final class AttributeExchangeConfigurer {
-		private final String identifier;
+		private String identifier;
 		private List<OpenIDAttribute> attributes = new ArrayList<>();
 		private List<AttributeConfigurer> attributeConfigurers = new ArrayList<>();
 
@@ -395,6 +414,19 @@ public final class OpenIDLoginConfigurer<H extends HttpSecurityBuilder<H>> 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<H extends HttpSecurityBuilder<H>> exten
 			return attributeConfigurer;
 		}
 
+		/**
+		 * Adds an {@link OpenIDAttribute} named &quot;default-attribute&quot;.
+		 * 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<AttributeConfigurer> 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<H extends HttpSecurityBuilder<H>> 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<H extends HttpSecurityBuilder<H>> 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

+ 178 - 0
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("<XRDS><XRD><Service><URI>%s</URI></Service></XRD></XRDS>", 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<OpenIDAttribute> attributeList = (List<OpenIDAttribute>) 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("<XRDS><XRD><Service><URI>%s</URI></Service></XRD></XRDS>", 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<OpenIDAttribute> attributeList = (List<OpenIDAttribute>) 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
+		}
+	}
 }