Przeglądaj źródła

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.
Luke Taylor 15 lat temu
rodzic
commit
2f025fba6c

+ 45 - 19
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<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 =

+ 7 - 2
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}

+ 12 - 2
config/src/main/resources/org/springframework/security/config/spring-security-3.1.xsd

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

+ 3 - 2
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<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());

+ 25 - 0
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.
+ * <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);
+}

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

+ 46 - 14
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<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);
     }

+ 42 - 0
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<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();
+    }
+
+}

+ 17 - 5
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">
 
     <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" />