SEC-1460: Added AxFetchListFactory which matches OpenID identifiers to lists of attributes to use in a fetch-request.

This allows different configurations to be used based on the identity-provider (google, yahoo etc). The default implementation iterates through a map of regex patterns to attribute lists. The namespace has also been extended to support this facility, with the "identifier-match" attribute being added to the attribute-exchange element. Multiple attribute-exchange elements can now be defined, each matching a different identifier.
This commit is contained in:
Luke Taylor 2010-04-20 23:44:58 +01:00
parent 3af75afec1
commit 2f025fba6c
9 changed files with 211 additions and 44 deletions

View File

@ -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<Element> 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<BeanDefinition> attributes = new ManagedList<BeanDefinition> ();
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<String, ManagedList<BeanDefinition>> axMap = new ManagedMap<String, ManagedList<BeanDefinition>>();
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<BeanDefinition> parseOpenIDAttributes(Element attrExElt) {
ManagedList<BeanDefinition> attributes = new ManagedList<BeanDefinition> ();
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 =

View File

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

View File

@ -627,7 +627,7 @@
<xs:documentation>Sets up form login for authentication with an Open ID identity</xs:documentation>
</xs:annotation><xs:complexType>
<xs:sequence>
<xs:element minOccurs="0" ref="security:attribute-exchange"/>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="security:attribute-exchange"/>
</xs:sequence>
<xs:attributeGroup ref="security:form-login.attlist"/>
<xs:attribute name="user-service-ref" type="xs:token">
@ -902,11 +902,21 @@
</xs:attribute>
</xs:attributeGroup>
<xs:element name="attribute-exchange"><xs:complexType>
<xs:element name="attribute-exchange"><xs:annotation>
<xs:documentation>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. </xs:documentation>
</xs:annotation><xs:complexType>
<xs:sequence>
<xs:element maxOccurs="unbounded" ref="security:openid-attribute"/>
</xs:sequence>
<xs:attributeGroup ref="security:attribute-exchange.attlist"/>
</xs:complexType></xs:element>
<xs:attributeGroup name="attribute-exchange.attlist">
<xs:attribute name="identifier-match" type="xs:token">
<xs:annotation>
<xs:documentation>A regular expression which will be compared against the claimed identity, when deciding which attribute-exchange configuration to use during authentication.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:element name="openid-attribute"><xs:complexType>
<xs:attributeGroup ref="security:openid-attribute.attlist"/>
</xs:complexType></xs:element>

View File

@ -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<OpenIDAttribute> attributes = (List<OpenIDAttribute>) FieldUtils.getFieldValue(consumer, "attributesToFetch");
RegexBasedAxFetchListFactory axFactory = (RegexBasedAxFetchListFactory) FieldUtils.getFieldValue(consumer, "attributesToFetchFactory");
List<OpenIDAttribute> 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());

View File

@ -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.
* <p>
* 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<OpenIDAttribute> createAttributeList(String identifier);
}

View File

@ -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<OpenIDAttribute> createAttributeList(String identifier) {
return Collections.emptyList();
}
}

View File

@ -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<OpenIDAttribute> 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<OpenIDAttribute> attributes) throws ConsumerException {
this(new ConsumerManager(), attributes);
}
public OpenID4JavaConsumer(ConsumerManager consumerManager, List<OpenIDAttribute> attributes)
@Deprecated
public OpenID4JavaConsumer(ConsumerManager consumerManager, final List<OpenIDAttribute> attributes)
throws ConsumerException {
this.consumerManager = consumerManager;
this.attributesToFetch = Collections.unmodifiableList(attributes);
this.attributesToFetchFactory = new AxFetchListFactory() {
private List<OpenIDAttribute> fetchAttrs = Collections.unmodifiableList(attributes);
public List<OpenIDAttribute> 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<OpenIDAttribute> 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<OpenIDAttribute> attributesToFetch = (List<OpenIDAttribute>) 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<OpenIDAttribute> attributes = new ArrayList<OpenIDAttribute>();
// 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<OpenIDAttribute> attributes = new ArrayList<OpenIDAttribute>(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);
}

View File

@ -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<Pattern, List<OpenIDAttribute>> idToAttributes;
/**
* @param regexMap map of regular-expressions (matching the identifier) to attributes which should be fetched for
* that pattern.
*/
public RegexBasedAxFetchListFactory(Map<String, List<OpenIDAttribute>> regexMap) {
idToAttributes = new LinkedHashMap<Pattern, List<OpenIDAttribute>>();
for (Map.Entry<String, List<OpenIDAttribute>> 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<OpenIDAttribute> createAttributeList(String identifier) {
for (Map.Entry<Pattern, List<OpenIDAttribute>> entry : idToAttributes.entrySet()) {
if (entry.getKey().matcher(identifier).matches()) {
return entry.getValue();
}
}
return Collections.emptyList();
}
}

View File

@ -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">
<http>
<intercept-url pattern="/**" access="ROLE_USER"/>
@ -16,11 +16,14 @@
<logout/>
<openid-login login-page="/openidlogin.jsp" user-service-ref="registeringUserService"
authentication-failure-url="/openidlogin.jsp?login_error=true">
<attribute-exchange>
<attribute-exchange identifier-match="https://www.google.com/.*">
<openid-attribute name="email" type="http://axschema.org/contact/email" required="true" count="1"/>
<openid-attribute name="firstname" type="http://axschema.org/namePerson/first" />
<openid-attribute name="lastname" type="http://axschema.org/namePerson/last" />
<openid-attribute name="fullname" type="http://axschema.org/namePerson" />
<openid-attribute name="firstname" type="http://axschema.org/namePerson/first" required="true" />
<openid-attribute name="lastname" type="http://axschema.org/namePerson/last" required="true" />
</attribute-exchange>
<attribute-exchange identifier-match=".*yahoo.com.*">
<openid-attribute name="email" type="http://axschema.org/contact/email" required="true"/>
<openid-attribute name="fullname" type="http://axschema.org/namePerson" required="true" />
</attribute-exchange>
</openid-login>
<remember-me token-repository-ref="tokenRepo"/>
@ -31,7 +34,16 @@
<authentication-manager alias="authenticationManager"/>
<!--
A custom UserDetailsService which will allow any user to authenticate and "register" their IDs in an internal map
for use if they return to the site. This is the most common usage pattern for sites which use OpenID.
-->
<b:bean id="registeringUserService" class="org.springframework.security.samples.openid.CustomUserDetailsService" />
<!--
A namespace-based UserDetailsService which will reject users who are not already defined.
This can be used as an alternative.
-->
<!--
<user-service id="userService">
<user name="http://luke.taylor.myopenid.com/" authorities="ROLE_SUPERVISOR,ROLE_USER" />