Allow configuration of openid login through nested builder

Issue: gh-5557
This commit is contained in:
Eleftheria Stein 2019-07-09 10:03:29 -04:00
parent c3dad06ea6
commit bf1bbd14e9
3 changed files with 370 additions and 2 deletions

View File

@ -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

View File

@ -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

View File

@ -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
}
}
}