/*
* 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.annotation.web.configurers.openid;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.openid4java.consumer.ConsumerException;
import org.openid4java.consumer.ConsumerManager;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
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;
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
import org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer;
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.openid.AxFetchListFactory;
import org.springframework.security.openid.OpenID4JavaConsumer;
import org.springframework.security.openid.OpenIDAttribute;
import org.springframework.security.openid.OpenIDAuthenticationFilter;
import org.springframework.security.openid.OpenIDAuthenticationProvider;
import org.springframework.security.openid.OpenIDAuthenticationToken;
import org.springframework.security.openid.OpenIDConsumer;
import org.springframework.security.openid.RegexBasedAxFetchListFactory;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* Adds support for OpenID based authentication.
*
* <h2>Example Configuration</h2>
*
* <pre>
*
* @Configuration
* @EnableWebSecurity
* public class OpenIDLoginConfig extends WebSecurityConfigurerAdapter {
*
* @Override
* protected void configure(HttpSecurity http) {
* http
* .authorizeRequests()
* .antMatchers("/**").hasRole("USER")
* .and()
* .openidLogin()
* .permitAll();
* }
*
* @Override
* protected void configure(AuthenticationManagerBuilder auth)(
* AuthenticationManagerBuilder auth) throws Exception {
* auth
* .inMemoryAuthentication()
* .withUser("https://www.google.com/accounts/o8/id?id=lmkCn9xzPdsxVwG7pjYMuDgNNdASFmobNkcRPaWU")
* .password("password")
* .roles("USER");
* }
* }
* </pre>
*
* <h2>Security Filters</h2>
*
* The following Filters are populated
*
* <ul>
* <li>{@link OpenIDAuthenticationFilter}</li>
* </ul>
*
* <h2>Shared Objects Created</h2>
*
* <ul>
* <li>{@link AuthenticationEntryPoint} is populated with a
* {@link LoginUrlAuthenticationEntryPoint}</li>
* <li>A {@link OpenIDAuthenticationProvider} is populated into
* {@link HttpSecurity#authenticationProvider(org.springframework.security.authentication.AuthenticationProvider)}
* </li>
* </ul>
*
* <h2>Shared Objects Used</h2>
*
* The following shared objects are used:
*
* <ul>
* <li>{@link AuthenticationManager}</li>
* <li>{@link RememberMeServices} - is optionally used. See {@link RememberMeConfigurer}
* </li>
* <li>{@link SessionAuthenticationStrategy} - is optionally used. See
* {@link SessionManagementConfigurer}</li>
* </ul>
*
* @author Rob Winch
* @since 3.2
*/
public final class OpenIDLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
AbstractAuthenticationFilterConfigurer<H, OpenIDLoginConfigurer<H>, OpenIDAuthenticationFilter> {
private OpenIDConsumer openIDConsumer;
private ConsumerManager consumerManager;
private AuthenticationUserDetailsService<OpenIDAuthenticationToken> authenticationUserDetailsService;
private List<AttributeExchangeConfigurer> attributeExchangeConfigurers = new ArrayList<AttributeExchangeConfigurer>();
/**
* Creates a new instance
*/
public OpenIDLoginConfigurer() {
super(new OpenIDAuthenticationFilter(), "/login/openid");
}
/**
* Sets up OpenID attribute exchange for OpenID's matching the specified pattern.
*
* @param identifierPattern the regular expression for matching on OpenID's (i.e.
* "https://www.google.com/.*", ".*yahoo.com.*", etc)
* @return a {@link AttributeExchangeConfigurer} for further customizations of the
* attribute exchange
*/
public AttributeExchangeConfigurer attributeExchange(String identifierPattern) {
AttributeExchangeConfigurer attributeExchangeConfigurer = new AttributeExchangeConfigurer(
identifierPattern);
this.attributeExchangeConfigurers.add(attributeExchangeConfigurer);
return attributeExchangeConfigurer;
}
/**
* Allows specifying the {@link OpenIDConsumer} to be used. The default is using an
* {@link OpenID4JavaConsumer}.
*
* @param consumer the {@link OpenIDConsumer} to be used
* @return the {@link OpenIDLoginConfigurer} for further customizations
*/
public OpenIDLoginConfigurer<H> consumer(OpenIDConsumer consumer) {
this.openIDConsumer = consumer;
return this;
}
/**
* Allows specifying the {@link ConsumerManager} to be used. If specified, will be
* populated into an {@link OpenID4JavaConsumer}.
*
* <p>
* This is a shortcut for specifying the {@link OpenID4JavaConsumer} with a specific
* {@link ConsumerManager} on {@link #consumer(OpenIDConsumer)}.
* </p>
*
* @param consumerManager the {@link ConsumerManager} to use. Cannot be null.
* @return the {@link OpenIDLoginConfigurer} for further customizations
*/
public OpenIDLoginConfigurer<H> consumerManager(ConsumerManager consumerManager) {
this.consumerManager = consumerManager;
return this;
}
/**
* The {@link AuthenticationUserDetailsService} to use. By default a
* {@link UserDetailsByNameServiceWrapper} is used with the {@link UserDetailsService}
* shared object found with {@link HttpSecurity#getSharedObject(Class)}.
*
* @param authenticationUserDetailsService the {@link AuthenticationDetailsSource} to
* use
* @return the {@link OpenIDLoginConfigurer} for further customizations
*/
public OpenIDLoginConfigurer<H> authenticationUserDetailsService(
AuthenticationUserDetailsService<OpenIDAuthenticationToken> authenticationUserDetailsService) {
this.authenticationUserDetailsService = authenticationUserDetailsService;
return this;
}
/**
* Specifies the URL used to authenticate OpenID requests. If the
* {@link HttpServletRequest} matches this URL the {@link OpenIDAuthenticationFilter}
* will attempt to authenticate the request. The default is "/login/openid".
*
* @param loginProcessingUrl the URL used to perform authentication
* @return the {@link OpenIDLoginConfigurer} for additional customization
*/
@Override
public OpenIDLoginConfigurer<H> loginProcessingUrl(String loginProcessingUrl) {
return super.loginProcessingUrl(loginProcessingUrl);
}
/**
* <p>
* Specifies the URL to send users to if login is required. If used with
* {@link WebSecurityConfigurerAdapter} a default login page will be generated when
* this attribute is not specified.
* </p>
*
* <p>
* If a URL is specified or this is not being used in conjuction with
* {@link WebSecurityConfigurerAdapter}, users are required to process the specified
* URL to generate a login page.
* </p>
*
* <ul>
* <li>It must be an HTTP POST</li>
* <li>It must be submitted to {@link #loginProcessingUrl(String)}</li>
* <li>It should include the OpenID as an HTTP parameter by the name of
* {@link OpenIDAuthenticationFilter#DEFAULT_CLAIMED_IDENTITY_FIELD}</li>
* </ul>
*
*
* <h2>Impact on other defaults</h2>
*
* Updating this value, also impacts a number of other default values. For example,
* the following are the default values when only formLogin() was specified.
*
* <ul>
* <li>/login GET - the login form</li>
* <li>/login POST - process the credentials and if valid authenticate the user</li>
* <li>/login?error GET - redirect here for failed authentication attempts</li>
* <li>/login?logout GET - redirect here after successfully logging out</li>
* </ul>
*
* If "/authenticate" was passed to this method it update the defaults as shown below:
*
* <ul>
* <li>/authenticate GET - the login form</li>
* <li>/authenticate POST - process the credentials and if valid authenticate the user
* </li>
* <li>/authenticate?error GET - redirect here for failed authentication attempts</li>
* <li>/authenticate?logout GET - redirect here after successfully logging out</li>
* </ul>
*
* @param loginPage the login page to redirect to if authentication is required (i.e.
* "/login")
* @return the {@link FormLoginConfigurer} for additional customization
*/
@Override
public OpenIDLoginConfigurer<H> loginPage(String loginPage) {
return super.loginPage(loginPage);
}
@Override
public void init(H http) throws Exception {
super.init(http);
OpenIDAuthenticationProvider authenticationProvider = new OpenIDAuthenticationProvider();
authenticationProvider.setAuthenticationUserDetailsService(
getAuthenticationUserDetailsService(http));
authenticationProvider = postProcess(authenticationProvider);
http.authenticationProvider(authenticationProvider);
initDefaultLoginFilter(http);
}
@Override
public void configure(H http) throws Exception {
getAuthenticationFilter().setConsumer(getConsumer());
super.configure(http);
}
/*
* (non-Javadoc)
*
* @see org.springframework.security.config.annotation.web.configurers.
* AbstractAuthenticationFilterConfigurer
* #createLoginProcessingUrlMatcher(java.lang.String)
*/
@Override
protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
return new AntPathRequestMatcher(loginProcessingUrl);
}
/**
* Gets the {@link OpenIDConsumer} that was configured or defaults to an
* {@link OpenID4JavaConsumer}.
* @return the {@link OpenIDConsumer} to use
* @throws ConsumerException
*/
private OpenIDConsumer getConsumer() throws ConsumerException {
if (this.openIDConsumer == null) {
this.openIDConsumer = new OpenID4JavaConsumer(getConsumerManager(),
attributesToFetchFactory());
}
return this.openIDConsumer;
}
/**
* Gets the {@link ConsumerManager} that was configured or defaults to using a
* {@link ConsumerManager} with the default constructor.
* @return the {@link ConsumerManager} to use
*/
private ConsumerManager getConsumerManager() {
if (this.consumerManager != null) {
return this.consumerManager;
}
return new ConsumerManager();
}
/**
* Creates an {@link RegexBasedAxFetchListFactory} using the attributes populated by
* {@link AttributeExchangeConfigurer}
*
* @return the {@link AxFetchListFactory} to use
*/
private AxFetchListFactory attributesToFetchFactory() {
Map<String, List<OpenIDAttribute>> identityToAttrs = new HashMap<String, List<OpenIDAttribute>>();
for (AttributeExchangeConfigurer conf : this.attributeExchangeConfigurers) {
identityToAttrs.put(conf.identifier, conf.getAttributes());
}
return new RegexBasedAxFetchListFactory(identityToAttrs);
}
/**
* Gets the {@link AuthenticationUserDetailsService} that was configured or defaults
* to {@link UserDetailsByNameServiceWrapper} that uses a {@link UserDetailsService}
* looked up using {@link HttpSecurity#getSharedObject(Class)}
*
* @param http the current {@link HttpSecurity}
* @return the {@link AuthenticationUserDetailsService}.
*/
private AuthenticationUserDetailsService<OpenIDAuthenticationToken> getAuthenticationUserDetailsService(
H http) {
if (this.authenticationUserDetailsService != null) {
return this.authenticationUserDetailsService;
}
return new UserDetailsByNameServiceWrapper<OpenIDAuthenticationToken>(
http.getSharedObject(UserDetailsService.class));
}
/**
* If available, initializes the {@link DefaultLoginPageGeneratingFilter} shared
* object.
*
* @param http the {@link HttpSecurityBuilder} to use
*/
private void initDefaultLoginFilter(H http) {
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
.getSharedObject(DefaultLoginPageGeneratingFilter.class);
if (loginPageGeneratingFilter != null && !isCustomLoginPage()) {
loginPageGeneratingFilter.setOpenIdEnabled(true);
loginPageGeneratingFilter.setOpenIDauthenticationUrl(getLoginProcessingUrl());
String loginPageUrl = loginPageGeneratingFilter.getLoginPageUrl();
if (loginPageUrl == null) {
loginPageGeneratingFilter.setLoginPageUrl(getLoginPage());
loginPageGeneratingFilter.setFailureUrl(getFailureUrl());
}
loginPageGeneratingFilter.setOpenIDusernameParameter(
OpenIDAuthenticationFilter.DEFAULT_CLAIMED_IDENTITY_FIELD);
}
}
/**
* A class used to add OpenID attributes to look up
*
* @author Rob Winch
*/
public final class AttributeExchangeConfigurer {
private final String identifier;
private List<OpenIDAttribute> attributes = new ArrayList<OpenIDAttribute>();
private List<AttributeConfigurer> attributeConfigurers = new ArrayList<AttributeConfigurer>();
/**
* Creates a new instance
* @param identifierPattern the pattern that attempts to match on the OpenID
* @see OpenIDLoginConfigurer#attributeExchange(String)
*/
private AttributeExchangeConfigurer(String identifierPattern) {
this.identifier = identifierPattern;
}
/**
* Get the {@link OpenIDLoginConfigurer} to customize the OpenID configuration
* further
* @return the {@link OpenIDLoginConfigurer}
*/
public OpenIDLoginConfigurer<H> and() {
return OpenIDLoginConfigurer.this;
}
/**
* Adds an {@link OpenIDAttribute} to be obtained for the configured OpenID
* pattern.
* @param attribute the {@link OpenIDAttribute} to obtain
* @return the {@link AttributeExchangeConfigurer} for further customization of
* attribute exchange
*/
public AttributeExchangeConfigurer attribute(OpenIDAttribute attribute) {
this.attributes.add(attribute);
return this;
}
/**
* Adds an {@link OpenIDAttribute} with the given name
* @param name the name of the {@link OpenIDAttribute} to create
* @return an {@link AttributeConfigurer} to further configure the
* {@link OpenIDAttribute} that should be obtained.
*/
public AttributeConfigurer attribute(String name) {
AttributeConfigurer attributeConfigurer = new AttributeConfigurer(name);
this.attributeConfigurers.add(attributeConfigurer);
return attributeConfigurer;
}
/**
* Gets the {@link OpenIDAttribute}'s for the configured OpenID pattern
* @return
*/
private List<OpenIDAttribute> getAttributes() {
for (AttributeConfigurer config : this.attributeConfigurers) {
this.attributes.add(config.build());
}
this.attributeConfigurers.clear();
return this.attributes;
}
/**
* Configures an {@link OpenIDAttribute}
*
* @author Rob Winch
* @since 3.2
*/
public final class AttributeConfigurer {
private String name;
private int count = 1;
private boolean required = false;
private String type;
/**
* Creates a new instance
* @param name the name of the attribute
* @see AttributeExchangeConfigurer#attribute(String)
*/
private AttributeConfigurer(String name) {
this.name = name;
}
/**
* Specifies the number of attribute values to request. Default is 1.
* @param count the number of attributes to request.
* @return the {@link AttributeConfigurer} for further customization
*/
public AttributeConfigurer count(int count) {
this.count = count;
return this;
}
/**
* Specifies that this attribute is required. The default is
* <code>false</code>. Note that as outlined in the OpenID specification,
* required attributes are not validated by the OpenID Provider. Developers
* should perform any validation in custom code.
*
* @param required specifies the attribute is required
* @return the {@link AttributeConfigurer} for further customization
*/
public AttributeConfigurer required(boolean required) {
this.required = required;
return this;
}
/**
* The OpenID attribute type.
* @param type
* @return
*/
public AttributeConfigurer type(String type) {
this.type = type;
return this;
}
/**
* Gets the {@link AttributeExchangeConfigurer} for further customization of
* the attributes
*
* @return the {@link AttributeConfigurer}
*/
public AttributeExchangeConfigurer and() {
return AttributeExchangeConfigurer.this;
}
/**
* Builds the {@link OpenIDAttribute}.
* @return
*/
private OpenIDAttribute build() {
OpenIDAttribute attribute = new OpenIDAttribute(this.name, this.type);
attribute.setCount(this.count);
attribute.setRequired(this.required);
return attribute;
}
}
}
}