diff --git a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java index 875028f447..d42b37ac18 100644 --- a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java @@ -18,6 +18,7 @@ import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.support.ManagedMap; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.security.authentication.AnonymousAuthenticationProvider; @@ -52,8 +53,9 @@ final class AuthenticationConfigBuilder { static final String OPEN_ID_AUTHENTICATION_PROCESSING_FILTER_CLASS = "org.springframework.security.openid.OpenIDAuthenticationFilter"; static final String OPEN_ID_AUTHENTICATION_PROVIDER_CLASS = "org.springframework.security.openid.OpenIDAuthenticationProvider"; - static final String OPEN_ID_CONSUMER_CLASS = "org.springframework.security.openid.OpenID4JavaConsumer"; + private static final String OPEN_ID_CONSUMER_CLASS = "org.springframework.security.openid.OpenID4JavaConsumer"; static final String OPEN_ID_ATTRIBUTE_CLASS = "org.springframework.security.openid.OpenIDAttribute"; + private static final String OPEN_ID_ATTRIBUTE_FACTORY_CLASS = "org.springframework.security.openid.RegexBasedAxFetchListFactory"; static final String AUTHENTICATION_PROCESSING_FILTER_CLASS = "org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter"; private static final String ATT_AUTO_CONFIG = "auto-config"; @@ -192,30 +194,31 @@ final class AuthenticationConfigBuilder { openIDFilter = parser.getFilterBean(); openIDEntryPoint = parser.getEntryPointBean(); - Element attrExElt = DomUtils.getChildElementByTagName(openIDLoginElt, Elements.OPENID_ATTRIBUTE_EXCHANGE); + List attrExElts = DomUtils.getChildElementsByTagName(openIDLoginElt, Elements.OPENID_ATTRIBUTE_EXCHANGE); - if (attrExElt != null) { + if (!attrExElts.isEmpty()) { // Set up the consumer with the required attribute list BeanDefinitionBuilder consumerBldr = BeanDefinitionBuilder.rootBeanDefinition(OPEN_ID_CONSUMER_CLASS); - ManagedList attributes = new ManagedList (); - for (Element attElt : DomUtils.getChildElementsByTagName(attrExElt, Elements.OPENID_ATTRIBUTE)) { - String name = attElt.getAttribute("name"); - String type = attElt.getAttribute("type"); - String required = attElt.getAttribute("required"); - String count = attElt.getAttribute("count"); - BeanDefinitionBuilder attrBldr = BeanDefinitionBuilder.rootBeanDefinition(OPEN_ID_ATTRIBUTE_CLASS); - attrBldr.addConstructorArgValue(name); - attrBldr.addConstructorArgValue(type); - if (StringUtils.hasLength(required)) { - attrBldr.addPropertyValue("required", Boolean.valueOf(required)); + BeanDefinitionBuilder axFactory = BeanDefinitionBuilder.rootBeanDefinition(OPEN_ID_ATTRIBUTE_FACTORY_CLASS); + ManagedMap> axMap = new ManagedMap>(); + + for (Element attrExElt : attrExElts) { + String identifierMatch = attrExElt.getAttribute("identifier-match"); + + if (!StringUtils.hasText(identifierMatch)) { + if (attrExElts.size() > 1) { + pc.getReaderContext().error("You must supply an identifier-match attribute if using more" + + " than one " + Elements.OPENID_ATTRIBUTE_EXCHANGE + " element", attrExElt); + } + // Match anything + identifierMatch = ".*"; } - if (StringUtils.hasLength(count)) { - attrBldr.addPropertyValue("count", Integer.parseInt(count)); - } - attributes.add(attrBldr.getBeanDefinition()); + axMap.put(identifierMatch, parseOpenIDAttributes(attrExElt)); } - consumerBldr.addConstructorArgValue(attributes); + axFactory.addConstructorArgValue(axMap); + + consumerBldr.addConstructorArgValue(axFactory.getBeanDefinition()); openIDFilter.getPropertyValues().addPropertyValue("consumer", consumerBldr.getBeanDefinition()); } } @@ -232,6 +235,29 @@ final class AuthenticationConfigBuilder { } } + private ManagedList parseOpenIDAttributes(Element attrExElt) { + ManagedList attributes = new ManagedList (); + for (Element attElt : DomUtils.getChildElementsByTagName(attrExElt, Elements.OPENID_ATTRIBUTE)) { + String name = attElt.getAttribute("name"); + String type = attElt.getAttribute("type"); + String required = attElt.getAttribute("required"); + String count = attElt.getAttribute("count"); + BeanDefinitionBuilder attrBldr = BeanDefinitionBuilder.rootBeanDefinition(OPEN_ID_ATTRIBUTE_CLASS); + attrBldr.addConstructorArgValue(name); + attrBldr.addConstructorArgValue(type); + if (StringUtils.hasLength(required)) { + attrBldr.addPropertyValue("required", Boolean.valueOf(required)); + } + + if (StringUtils.hasLength(count)) { + attrBldr.addPropertyValue("count", Integer.parseInt(count)); + } + attributes.add(attrBldr.getBeanDefinition()); + } + + return attributes; + } + private void createOpenIDProvider() { Element openIDLoginElt = DomUtils.getChildElementByTagName(httpElt, Elements.OPENID_LOGIN); BeanDefinitionBuilder openIDProviderBuilder = diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-3.1.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-3.1.rnc index 5c62b548e0..c2cfbca62a 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-3.1.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-3.1.rnc @@ -371,10 +371,15 @@ form-login.attlist &= openid-login = ## Sets up form login for authentication with an Open ID identity - element openid-login {form-login.attlist, user-service-ref?, attribute-exchange?} + element openid-login {form-login.attlist, user-service-ref?, attribute-exchange*} attribute-exchange = - element attribute-exchange {openid-attribute+} + ## Sets up an attribute exchange configuration to request specified attributes from the OpenID identity provider. When multiple elements are used, each must have an identifier-attribute attribute. Each configuration will be matched in turn against the supplied login identifier until a match is found. + element attribute-exchange {attribute-exchange.attlist, openid-attribute+} + +attribute-exchange.attlist &= + ## A regular expression which will be compared against the claimed identity, when deciding which attribute-exchange configuration to use during authentication. + attribute identifier-match {xsd:token}? openid-attribute = element openid-attribute {openid-attribute.attlist} diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-3.1.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-3.1.xsd index 7de02c414a..4dcb190721 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-3.1.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-3.1.xsd @@ -627,7 +627,7 @@ Sets up form login for authentication with an Open ID identity - + @@ -902,11 +902,21 @@ - + + Sets up an attribute exchange configuration to request specified attributes from the OpenID identity provider. When multiple elements are used, each must have an identifier-attribute attribute. Each configuration will be matched in turn against the supplied login identifier until a match is found. + + + + + + A regular expression which will be compared against the claimed identity, when deciding which attribute-exchange configuration to use during authentication. + + + diff --git a/config/src/test/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParserTests.java index c0f0e72c27..450e229306 100644 --- a/config/src/test/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParserTests.java @@ -46,6 +46,7 @@ import org.springframework.security.openid.OpenIDAuthenticationProvider; import org.springframework.security.openid.OpenIDAuthenticationToken; import org.springframework.security.openid.OpenIDConsumer; import org.springframework.security.openid.OpenIDConsumerException; +import org.springframework.security.openid.RegexBasedAxFetchListFactory; import org.springframework.security.util.FieldUtils; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.FilterInvocation; @@ -1152,7 +1153,6 @@ public class HttpSecurityBeanDefinitionParserTests { assertEquals("/openid_login", ap.getLoginFormUrl()); } - @SuppressWarnings("unchecked") @Test public void openIDWithAttributeExchangeConfigurationIsParsedCorrectly() throws Exception { setContext( @@ -1168,7 +1168,8 @@ public class HttpSecurityBeanDefinitionParserTests { OpenIDAuthenticationFilter apf = getFilter(OpenIDAuthenticationFilter.class); OpenID4JavaConsumer consumer = (OpenID4JavaConsumer) FieldUtils.getFieldValue(apf, "consumer"); - List attributes = (List) FieldUtils.getFieldValue(consumer, "attributesToFetch"); + RegexBasedAxFetchListFactory axFactory = (RegexBasedAxFetchListFactory) FieldUtils.getFieldValue(consumer, "attributesToFetchFactory"); + List attributes = axFactory.createAttributeList("https://anyopenidprovider.com/"); assertEquals(2, attributes.size()); assertEquals("nickname", attributes.get(0).getName()); assertEquals("http://schema.openid.net/namePerson/friendly", attributes.get(0).getType()); diff --git a/openid/src/main/java/org/springframework/security/openid/AxFetchListFactory.java b/openid/src/main/java/org/springframework/security/openid/AxFetchListFactory.java new file mode 100644 index 0000000000..e2a90afb64 --- /dev/null +++ b/openid/src/main/java/org/springframework/security/openid/AxFetchListFactory.java @@ -0,0 +1,25 @@ +package org.springframework.security.openid; + +import java.util.List; + +/** + * A strategy which can be used by an OpenID consumer implementation, to dynamically determine + * the attribute exchange information based on the OpenID identifier. + *

+ * This allows the list of attributes for a fetch request to be tailored for different OpenID providers, since they + * do not all support the same attributes. + * + * @author Luke Taylor + * @since 3.1 + */ +public interface AxFetchListFactory { + + /** + * Builds the list of attributes which should be added to the fetch request for the + * supplied OpenID identifier. + * + * @param identifier the claimed_identity + * @return the attributes to fetch for this identifier + */ + List createAttributeList(String identifier); +} diff --git a/openid/src/main/java/org/springframework/security/openid/NullAxFetchListFactory.java b/openid/src/main/java/org/springframework/security/openid/NullAxFetchListFactory.java new file mode 100644 index 0000000000..8b1f6dd109 --- /dev/null +++ b/openid/src/main/java/org/springframework/security/openid/NullAxFetchListFactory.java @@ -0,0 +1,14 @@ +package org.springframework.security.openid; + +import java.util.Collections; +import java.util.List; + +/** + * @author Luke Taylor + * @since 3.1 + */ +public class NullAxFetchListFactory implements AxFetchListFactory { + public List createAttributeList(String identifier) { + return Collections.emptyList(); + } +} diff --git a/openid/src/main/java/org/springframework/security/openid/OpenID4JavaConsumer.java b/openid/src/main/java/org/springframework/security/openid/OpenID4JavaConsumer.java index eb747aa4aa..e5c4f3db6a 100644 --- a/openid/src/main/java/org/springframework/security/openid/OpenID4JavaConsumer.java +++ b/openid/src/main/java/org/springframework/security/openid/OpenID4JavaConsumer.java @@ -41,31 +41,50 @@ import org.openid4java.message.ax.FetchResponse; /** * @author Ray Krueger + * @author Luke Taylor */ public class OpenID4JavaConsumer implements OpenIDConsumer { private static final String DISCOVERY_INFO_KEY = DiscoveryInformation.class.getName(); + private static final String ATTRIBUTE_LIST_KEY = "SPRING_SECURITY_OPEN_ID_ATTRIBUTES_FETCH_LIST"; //~ Instance fields ================================================================================================ protected final Log logger = LogFactory.getLog(getClass()); private final ConsumerManager consumerManager; - private List attributesToFetch = Collections.emptyList(); + private final AxFetchListFactory attributesToFetchFactory; //~ Constructors =================================================================================================== public OpenID4JavaConsumer() throws ConsumerException { this.consumerManager = new ConsumerManager(); + this.attributesToFetchFactory = new NullAxFetchListFactory(); } + /** + * @deprecated use the {@link AxFetchListFactory} version instead. + */ + @Deprecated public OpenID4JavaConsumer(List attributes) throws ConsumerException { this(new ConsumerManager(), attributes); } - public OpenID4JavaConsumer(ConsumerManager consumerManager, List attributes) + @Deprecated + public OpenID4JavaConsumer(ConsumerManager consumerManager, final List attributes) throws ConsumerException { this.consumerManager = consumerManager; - this.attributesToFetch = Collections.unmodifiableList(attributes); + this.attributesToFetchFactory = new AxFetchListFactory() { + private List fetchAttrs = Collections.unmodifiableList(attributes); + + public List createAttributeList(String identifier) { + return fetchAttrs; + } + }; + } + + public OpenID4JavaConsumer(AxFetchListFactory attributesToFetchFactory) throws ConsumerException { + this.consumerManager = new ConsumerManager(); + this.attributesToFetchFactory = attributesToFetchFactory; } //~ Methods ======================================================================================================== @@ -88,9 +107,18 @@ public class OpenID4JavaConsumer implements OpenIDConsumer { try { authReq = consumerManager.authenticate(information, returnToUrl, realm); + + logger.debug("Looking up attribute fetch list for identifier: " + identityUrl); + + List attributesToFetch = attributesToFetchFactory.createAttributeList(identityUrl); + if (!attributesToFetch.isEmpty()) { + req.getSession().setAttribute(ATTRIBUTE_LIST_KEY, attributesToFetch); FetchRequest fetchRequest = FetchRequest.createFetchRequest(); for (OpenIDAttribute attr : attributesToFetch) { + if (logger.isDebugEnabled()) { + logger.debug("Adding attribute " + attr.getType() + " to fetch request"); + } fetchRequest.addAttribute(attr.getName(), attr.getType(), attr.isRequired(), attr.getCount()); } authReq.addExtension(fetchRequest); @@ -113,7 +141,10 @@ public class OpenID4JavaConsumer implements OpenIDConsumer { // retrieve the previously stored discovery information DiscoveryInformation discovered = (DiscoveryInformation) request.getSession().getAttribute(DISCOVERY_INFO_KEY); + List attributesToFetch = (List) request.getSession().getAttribute(ATTRIBUTE_LIST_KEY); + request.getSession().removeAttribute(DISCOVERY_INFO_KEY); + request.getSession().removeAttribute(ATTRIBUTE_LIST_KEY); // extract the receiving URL from the HTTP request StringBuffer receivingURL = request.getRequestURL(); @@ -136,9 +167,20 @@ public class OpenID4JavaConsumer implements OpenIDConsumer { throw new OpenIDConsumerException("Error verifying openid response", e); } + List attributes = new ArrayList(); + + // examine the verification result and extract the verified identifier + Identifier verified = verification.getVerifiedId(); + + if (verified == null) { + Identifier id = discovered.getClaimedIdentifier(); + return new OpenIDAuthenticationToken(OpenIDAuthenticationStatus.FAILURE, + id == null ? "Unknown" : id.getIdentifier(), + "Verification status message: [" + verification.getStatusMsg() + "]", attributes); + } + // fetch the attributesToFetch of the response Message authSuccess = verification.getAuthResponse(); - List attributes = new ArrayList(this.attributesToFetch.size()); if (authSuccess.hasExtension(AxMessage.OPENID_NS_AX)) { if (debug) { @@ -166,16 +208,6 @@ public class OpenID4JavaConsumer implements OpenIDConsumer { } } - // examine the verification result and extract the verified identifier - Identifier verified = verification.getVerifiedId(); - - if (verified == null) { - Identifier id = discovered.getClaimedIdentifier(); - return new OpenIDAuthenticationToken(OpenIDAuthenticationStatus.FAILURE, - id == null ? "Unknown" : id.getIdentifier(), - "Verification status message: [" + verification.getStatusMsg() + "]", attributes); - } - return new OpenIDAuthenticationToken(OpenIDAuthenticationStatus.SUCCESS, verified.getIdentifier(), "some message", attributes); } diff --git a/openid/src/main/java/org/springframework/security/openid/RegexBasedAxFetchListFactory.java b/openid/src/main/java/org/springframework/security/openid/RegexBasedAxFetchListFactory.java new file mode 100644 index 0000000000..eb3d50d548 --- /dev/null +++ b/openid/src/main/java/org/springframework/security/openid/RegexBasedAxFetchListFactory.java @@ -0,0 +1,42 @@ +package org.springframework.security.openid; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * + * @author Luke Taylor + * @since 3.1 + */ +public class RegexBasedAxFetchListFactory implements AxFetchListFactory { + private final Map> idToAttributes; + + /** + * @param regexMap map of regular-expressions (matching the identifier) to attributes which should be fetched for + * that pattern. + */ + public RegexBasedAxFetchListFactory(Map> regexMap) { + idToAttributes = new LinkedHashMap>(); + for (Map.Entry> entry : regexMap.entrySet()) { + idToAttributes.put(Pattern.compile(entry.getKey()), entry.getValue()); + } + } + + /** + * Iterates through the patterns stored in the map and returns the list of attributes defined for the + * first match. If no match is found, returns an empty list. + */ + public List createAttributeList(String identifier) { + for (Map.Entry> entry : idToAttributes.entrySet()) { + if (entry.getKey().matcher(identifier).matches()) { + return entry.getValue(); + } + } + + return Collections.emptyList(); + } + +} diff --git a/samples/openid/src/main/webapp/WEB-INF/applicationContext-security.xml b/samples/openid/src/main/webapp/WEB-INF/applicationContext-security.xml index 7741b8358d..ff1b9ccbed 100644 --- a/samples/openid/src/main/webapp/WEB-INF/applicationContext-security.xml +++ b/samples/openid/src/main/webapp/WEB-INF/applicationContext-security.xml @@ -8,7 +8,7 @@ xmlns:b="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd - http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd"> + http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd"> @@ -16,11 +16,14 @@ - + - - - + + + + + + @@ -31,7 +34,16 @@ + + +