/*
 * 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>
 *
 * &#064;Configuration
 * &#064;EnableWebSecurity
 * public class OpenIDLoginConfig extends WebSecurityConfigurerAdapter {
 *
 * 	&#064;Override
 * 	protected void configure(HttpSecurity http) {
 * 		http
 * 			.authorizeRequests()
 * 				.antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;)
 * 				.and()
 * 			.openidLogin()
 * 				.permitAll();
 * 	}
 *
 * 	&#064;Override
 * 	protected void configure(AuthenticationManagerBuilder auth)(
 * 			AuthenticationManagerBuilder auth) throws Exception {
 * 		auth
 * 			.inMemoryAuthentication()
 * 				.withUser(&quot;https://www.google.com/accounts/o8/id?id=lmkCn9xzPdsxVwG7pjYMuDgNNdASFmobNkcRPaWU&quot;)
 * 					.password(&quot;password&quot;)
 * 					.roles(&quot;USER&quot;);
 * 	}
 * }
 * </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>An {@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<>();

	/**
	 * 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<>(
				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<>();
		private List<AttributeConfigurer> attributeConfigurers = new ArrayList<>();

		/**
		 * 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;
			}
		}
	}
}
