SIDP-461: Legacy Shib SSO protocol for IdP-initiated SAML 2 SSO
authorcantor <cantor@ab3bd59b-922f-494d-bb5f-6f0a3c29deca>
Wed, 9 Feb 2011 23:03:46 +0000 (23:03 +0000)
committercantor <cantor@ab3bd59b-922f-494d-bb5f-6f0a3c29deca>
Wed, 9 Feb 2011 23:03:46 +0000 (23:03 +0000)
git-svn-id: https://subversion.switch.ch/svn/shibboleth/java-idp/branches/REL_2@2989 ab3bd59b-922f-494d-bb5f-6f0a3c29deca

doc/RELEASE-NOTES.txt
src/installer/resources/conf-tmpl/handler.xml
src/installer/resources/conf-tmpl/internal.xml
src/main/java/edu/internet2/middleware/shibboleth/idp/authn/Saml2LoginContext.java
src/main/java/edu/internet2/middleware/shibboleth/idp/profile/saml2/SSOProfileHandler.java
src/main/java/edu/internet2/middleware/shibboleth/idp/profile/saml2/UnsolicitedSSODecoder.java [new file with mode: 0644]

index bcc584d..ad16247 100644 (file)
@@ -1,8 +1,9 @@
 Changes in Release 2.3.0
 =============================================
-[SIDP-429] - Limit metadata SP credential resolution for encryption to RSA keys only
 [SIDP-272] - Regenerate self-signed certificate with installer task
 [SIDP-404] - Add an install-time setting for the path to web.xml
+[SIDP-429] - Limit metadata SP credential resolution for encryption to RSA keys only
+[SIDP-461] - Add legacy Shib SSO protocol as binding for IdP-initiated SSO for SAML 2.0
 
 Changes in Release 2.2.1
 =============================================
index 7f29da1..df7d7b5 100644 (file)
                                                 urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact">
         <ph:RequestPath>/SAML2/Redirect/SSO</ph:RequestPath>
     </ph:ProfileHandler>
-    
+
+    <ph:ProfileHandler xsi:type="ph:SAML2SSO"
+                    inboundBinding="urn:mace:shibboleth:2.0:profiles:AuthnRequest"
+                    outboundBindingEnumeration="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign
+                                                urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST 
+                                                urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact">
+        <ph:RequestPath>/SAML2/Unsolicited/SSO</ph:RequestPath>
+    </ph:ProfileHandler>
+
     <ph:ProfileHandler xsi:type="ph:SAML2AttributeQuery"
                     inboundBinding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
                     outboundBindingEnumeration="urn:oasis:names:tc:SAML:2.0:bindings:SOAP">
index 4053643..a89ad64 100644 (file)
                 class="edu.internet2.middleware.shibboleth.idp.profile.saml1.ShibbolethSSODecoder">
             </bean>
         </entry>
+        <entry>
+            <key>
+                <value>urn:mace:shibboleth:2.0:profiles:AuthnRequest</value>
+            </key>
+            <bean id="shibboleth.UnsolicitedSSODecoder"
+                class="edu.internet2.middleware.shibboleth.idp.profile.saml2.UnsolicitedSSODecoder">
+                <constructor-arg ref="shibboleth.IdGenerator" />
+            </bean>
+        </entry>
     </util:map>
 
     <util:map id="shibboleth.MessageEncoders">
index f8d3811..41c73b9 100644 (file)
@@ -52,6 +52,9 @@ public class Saml2LoginContext extends LoginContext implements Serializable {
 
     /** Serialized authentication request. */
     private String serialAuthnRequest;
+    
+    /** Unsolicited SSO indicator.  */
+    private boolean unsolicited;
 
     /**
      * Creates a new instance of Saml2LoginContext.
@@ -98,6 +101,24 @@ public class Saml2LoginContext extends LoginContext implements Serializable {
     }
 
     /**
+     * Returns the unsolicited SSO indicator.
+     * 
+     * @return the unsolicited SSO indicator
+     */
+    public boolean isUnsolicited() {
+        return unsolicited;
+    }
+
+    /**
+     * Sets the unsolicited SSO indicator.
+     * 
+     * @param unsolicited unsolicited SSO indicator to set
+     */
+    public void setUnsolicited(boolean unsolicited) {
+        this.unsolicited = unsolicited;
+    }        
+    
+    /**
      * Serializes an authentication request into a string.
      * 
      * @param request the request to serialize
index d656df6..963b7e6 100644 (file)
@@ -31,6 +31,7 @@ import org.opensaml.common.SAMLObjectBuilder;
 import org.opensaml.common.binding.decoding.SAMLMessageDecoder;
 import org.opensaml.common.xml.SAMLConstants;
 import org.opensaml.saml2.binding.AuthnResponseEndpointSelector;
+import org.opensaml.saml2.core.Assertion;
 import org.opensaml.saml2.core.AttributeStatement;
 import org.opensaml.saml2.core.AuthnContext;
 import org.opensaml.saml2.core.AuthnContextClassRef;
@@ -44,6 +45,8 @@ import org.opensaml.saml2.core.Response;
 import org.opensaml.saml2.core.Statement;
 import org.opensaml.saml2.core.StatusCode;
 import org.opensaml.saml2.core.Subject;
+import org.opensaml.saml2.core.SubjectConfirmation;
+import org.opensaml.saml2.core.SubjectConfirmationData;
 import org.opensaml.saml2.core.SubjectLocality;
 import org.opensaml.saml2.metadata.AffiliateMember;
 import org.opensaml.saml2.metadata.AffiliationDescriptor;
@@ -198,6 +201,7 @@ public class SSOProfileHandler extends AbstractSAML2ProfileHandler {
             log.debug("Creating login context and transferring control to authentication engine");
             Saml2LoginContext loginContext = new Saml2LoginContext(relyingPartyId, requestContext.getRelayState(),
                     requestContext.getInboundSAMLMessage());
+            loginContext.setUnsolicited(requestContext.isUnsolicited());
             loginContext.setAuthenticationEngineURL(authenticationManagerPath);
             loginContext.setProfileHandlerURL(HttpHelper.getRequestUriWithoutContext(httpRequest));
             loginContext.setDefaultAuthenticationMethod(rpConfig.getDefaultAuthenticationMethod());
@@ -276,6 +280,11 @@ public class SSOProfileHandler extends AbstractSAML2ProfileHandler {
 
             samlResponse = buildResponse(requestContext, "urn:oasis:names:tc:SAML:2.0:cm:bearer", statements);
         } catch (ProfileException e) {
+            if (requestContext.isUnsolicited()) {
+                // Just delegate to the IdP's global error handler
+                log.warn("Unsolicited response generation failed: {}", e.getMessage());
+                throw e;
+            }
             samlResponse = buildErrorResponse(requestContext);
         }
 
@@ -285,7 +294,7 @@ public class SSOProfileHandler extends AbstractSAML2ProfileHandler {
         encodeResponse(requestContext);
         writeAuditLogEntry(requestContext);
     }
-
+    
     /**
      * Decodes an incoming request and stores the information in a created request context.
      * 
@@ -406,6 +415,7 @@ public class SSOProfileHandler extends AbstractSAML2ProfileHandler {
         requestContext.setMessageDecoder(getInboundMessageDecoder(requestContext));
 
         requestContext.setLoginContext(loginContext);
+        requestContext.setUnsolicited(loginContext.isUnsolicited());
 
         requestContext.setInboundMessageTransport(in);
         requestContext.setInboundSAMLProtocol(SAMLConstants.SAML20P_NS);
@@ -418,7 +428,7 @@ public class SSOProfileHandler extends AbstractSAML2ProfileHandler {
         String relyingPartyId = loginContext.getRelyingPartyId();
         requestContext.setPeerEntityId(relyingPartyId);
         requestContext.setInboundMessageIssuer(relyingPartyId);
-
+        
         populateRequestContext(requestContext);
 
         return requestContext;
@@ -672,13 +682,53 @@ public class SSOProfileHandler extends AbstractSAML2ProfileHandler {
         }
     }
 
+    /** {@inheritDoc} */
+    protected void postProcessAssertion(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext, Assertion assertion)
+            throws ProfileException {
+        SSORequestContext ctx = (SSORequestContext) requestContext;
+        if (ctx.isUnsolicited()) {
+            Subject subject = assertion.getSubject();
+            if (subject != null) {
+                for (SubjectConfirmation sc : subject.getSubjectConfirmations()) {
+                    if (sc != null) {
+                        SubjectConfirmationData scd = sc.getSubjectConfirmationData();
+                        if (scd != null) {
+                            scd.setInResponseTo(null);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    protected void postProcessResponse(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext, Response samlResponse)
+            throws ProfileException {
+        SSORequestContext ctx = (SSORequestContext) requestContext;
+        if (ctx.isUnsolicited()) {
+            samlResponse.setInResponseTo(null);
+        }
+    }    
+
     /** Represents the internal state of a SAML 2.0 SSO Request while it's being processed by the IdP. */
     protected class SSORequestContext extends BaseSAML2ProfileRequestContext<AuthnRequest, Response, SSOConfiguration> {
 
+        /** Unsolicited SSO indicator. */
+        private boolean unsolicited;
+
         /** Current login context. */
         private Saml2LoginContext loginContext;
 
         /**
+         * Returns the unsolicited SSO indicator.
+         * 
+         * @return the unsolicited SSO indicator
+         */
+        public boolean isUnsolicited() {
+            return unsolicited;
+        }
+        
+        /**
          * Gets the current login context.
          * 
          * @return current login context
@@ -686,6 +736,15 @@ public class SSOProfileHandler extends AbstractSAML2ProfileHandler {
         public Saml2LoginContext getLoginContext() {
             return loginContext;
         }
+        
+        /**
+         * Sets the unsolicited SSO indicator.
+         * 
+         * @param unsolicited unsolicited SSO indicator to set
+         */
+        public void setUnsolicited(boolean unsolicited) {
+            this.unsolicited = unsolicited;
+        }        
 
         /**
          * Sets the current login context.
@@ -695,5 +754,6 @@ public class SSOProfileHandler extends AbstractSAML2ProfileHandler {
         public void setLoginContext(Saml2LoginContext context) {
             loginContext = context;
         }
+
     }
 }
\ No newline at end of file
diff --git a/src/main/java/edu/internet2/middleware/shibboleth/idp/profile/saml2/UnsolicitedSSODecoder.java b/src/main/java/edu/internet2/middleware/shibboleth/idp/profile/saml2/UnsolicitedSSODecoder.java
new file mode 100644 (file)
index 0000000..7e702ba
--- /dev/null
@@ -0,0 +1,274 @@
+/*
+ * Copyright 2011 University Corporation for Advanced Internet Development, Inc.
+ *
+ * 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 edu.internet2.middleware.shibboleth.idp.profile.saml2;
+
+import org.joda.time.DateTime;
+import org.joda.time.chrono.ISOChronology;
+import org.opensaml.Configuration;
+import org.opensaml.common.IdentifierGenerator;
+import org.opensaml.common.SAMLObjectBuilder;
+import org.opensaml.common.binding.BasicEndpointSelector;
+import org.opensaml.common.binding.SAMLMessageContext;
+import org.opensaml.common.binding.decoding.SAMLMessageDecoder;
+import org.opensaml.common.xml.SAMLConstants;
+import org.opensaml.saml2.binding.decoding.BaseSAML2MessageDecoder;
+import org.opensaml.saml2.core.AuthnRequest;
+import org.opensaml.saml2.core.Issuer;
+import org.opensaml.saml2.core.NameIDPolicy;
+import org.opensaml.saml2.metadata.AssertionConsumerService;
+import org.opensaml.saml2.metadata.Endpoint;
+import org.opensaml.saml2.metadata.SPSSODescriptor;
+import org.opensaml.saml2.metadata.provider.MetadataProvider;
+import org.opensaml.saml2.metadata.provider.MetadataProviderException;
+import org.opensaml.ws.message.MessageContext;
+import org.opensaml.ws.message.decoder.MessageDecodingException;
+import org.opensaml.ws.transport.http.HTTPInTransport;
+import org.opensaml.ws.transport.http.HttpServletRequestAdapter;
+import org.opensaml.xml.XMLObjectBuilderFactory;
+import org.opensaml.xml.util.DatatypeHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import edu.internet2.middleware.shibboleth.idp.profile.saml2.SSOProfileHandler.SSORequestContext;
+
+/**
+ * Shibboleth 2.x HTTP request parameter-based SSO authentication request message decoder.
+ * 
+ * <p>
+ * This decoder understands and processes a set of defined HTTP request parameters representing a logical
+ * SAML 2 SSO authentication request, and builds a corresponding {@link AuthnRequest} message.
+ * This message is then stored in the {@link SAMLMessageContext} so that it may be processed 
+ * by other components (e.g. profile handler) that process standard AuthnRequest messages.
+ * </p>
+ * .
+ */
+public class UnsolicitedSSODecoder extends BaseSAML2MessageDecoder implements SAMLMessageDecoder {
+
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(UnsolicitedSSODecoder.class);
+
+    /** The binding URI default value. */
+    public String defaultBinding;
+    
+    /** AuthnRequest builder. */
+    private SAMLObjectBuilder<AuthnRequest> authnRequestBuilder;
+
+    /** Issuer builder. */
+    private SAMLObjectBuilder<Issuer> issuerBuilder;
+
+    /** NameIDPolicy builder. */
+    private SAMLObjectBuilder<NameIDPolicy> nipBuilder;
+    
+    /** Identifier generator. */
+    private IdentifierGenerator idGenerator;
+
+    /**
+     * Constructor.
+     * 
+     * @param identifierGenerator the IdentifierGenerator instance to use.
+     */
+    @SuppressWarnings("unchecked")
+    public UnsolicitedSSODecoder(IdentifierGenerator identifierGenerator) {
+        super();
+
+        XMLObjectBuilderFactory builderFactory = Configuration.getBuilderFactory();
+
+        authnRequestBuilder = 
+            (SAMLObjectBuilder<AuthnRequest>) builderFactory.getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME);
+        issuerBuilder = 
+            (SAMLObjectBuilder<Issuer>) builderFactory.getBuilder(Issuer.DEFAULT_ELEMENT_NAME);
+        nipBuilder = 
+            (SAMLObjectBuilder<NameIDPolicy>) builderFactory.getBuilder(NameIDPolicy.DEFAULT_ELEMENT_NAME);
+
+        idGenerator = identifierGenerator;
+        defaultBinding = SAMLConstants.SAML2_POST_BINDING_URI;
+    }
+
+    /** {@inheritDoc} */
+    public String getBindingURI() {
+        return "urn:mace:shibboleth:2.0:profiles:AuthnRequest";
+    }
+
+    /** {@inheritDoc} */
+    @SuppressWarnings("unchecked")
+    protected boolean isIntendedDestinationEndpointURIRequired(SAMLMessageContext samlMsgCtx) {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @SuppressWarnings("unchecked")
+    protected String getIntendedDestinationEndpointURI(SAMLMessageContext samlMsgCtx) throws MessageDecodingException {
+        // Not relevant in this binding/profile, there is neither SAML message
+        // nor binding parameter with this information
+        return null;
+    }
+    
+    /**
+     * Returns the default ACS binding.
+     * @return  default binding URI
+     */
+    public String getDefaultBinding() {
+        return defaultBinding;
+    }
+    
+    /**
+     * Sets the default ACS binding.
+     * @param binding default binding URI
+     */
+    public void setDefaultBinding(String binding) {
+        defaultBinding = binding;
+    }
+    
+    /** {@inheritDoc} */
+    @SuppressWarnings("unchecked")
+    protected void doDecode(MessageContext messageContext) throws MessageDecodingException {
+        if (!(messageContext instanceof SSORequestContext)) {
+            log.warn("Invalid message context type, this decoder only supports SSORequestContext");
+            throw new MessageDecodingException(
+                    "Invalid message context type, this decoder only supports SSORequestContext");
+        }
+
+        if (!(messageContext.getInboundMessageTransport() instanceof HTTPInTransport)) {
+            log.warn("Invalid inbound message transport type, this decoder only support HTTPInTransport");
+            throw new MessageDecodingException(
+                    "Invalid inbound message transport type, this decoder only support HTTPInTransport");
+        }
+
+        SSORequestContext requestContext = (SSORequestContext) messageContext;
+        HTTPInTransport transport = (HTTPInTransport) messageContext.getInboundMessageTransport();
+        
+        String providerId = DatatypeHelper.safeTrimOrNullString(transport.getParameterValue("providerId"));
+        if (providerId == null) {
+            log.warn("No providerId parameter given in unsolicited SSO authentication request.");
+            throw new MessageDecodingException(
+                    "No providerId parameter given in unsolicited SSO authentication request.");
+        }
+
+        requestContext.setRelayState(DatatypeHelper.safeTrimOrNullString(transport.getParameterValue("target")));
+
+        String timeStr = DatatypeHelper.safeTrimOrNullString(transport.getParameterValue("time"));
+        String sessionID = ((HttpServletRequestAdapter) transport).getWrappedRequest().getRequestedSessionId();
+
+        String binding = null;
+        String acsURL = DatatypeHelper.safeTrimOrNullString(transport.getParameterValue("shire"));
+        if (acsURL == null) {
+            acsURL = lookupACSURL(requestContext.getMetadataProvider(), providerId);
+            if (acsURL == null) {
+                log.warn("Unable to resolve SP ACS URL for AuthnRequest construction for entityID: {}",
+                        providerId);
+                throw new MessageDecodingException("Unable to resolve SP ACS URL for AuthnRequest construction");
+            }
+            binding = defaultBinding;
+        }
+        
+        AuthnRequest authnRequest = buildAuthnRequest(providerId, acsURL, binding, timeStr, sessionID);
+        requestContext.setInboundMessage(authnRequest);
+        requestContext.setInboundSAMLMessage(authnRequest);
+        log.debug("Mocked up SAML message");
+
+        populateMessageContext(requestContext);
+        
+        requestContext.setUnsolicited(true);
+    }
+
+    /**
+     * Build a SAML 2 AuthnRequest from the parameters specified in the inbound transport.
+     * 
+     * @param entityID the requester identity
+     * @param acsURL the ACS URL
+     * @param acsBinding the ACS binding URI
+     * @param timeStr the request timestamp
+     * @param sessionID the container session, if any
+     * @return a newly constructed AuthnRequest instance
+     */
+    @SuppressWarnings("unchecked")
+    private AuthnRequest buildAuthnRequest(String entityID, String acsURL, String acsBinding, String timeStr, String sessionID) {
+        
+        AuthnRequest authnRequest = authnRequestBuilder.buildObject();
+        authnRequest.setAssertionConsumerServiceURL(acsURL);
+        if (acsBinding != null) {
+            authnRequest.setProtocolBinding(acsBinding);
+        }
+
+        Issuer issuer = issuerBuilder.buildObject();
+        issuer.setValue(entityID);
+        authnRequest.setIssuer(issuer);
+
+        // Matches the default semantic a typical SP would have.
+        NameIDPolicy nip = nipBuilder.buildObject();
+        nip.setAllowCreate(true);
+        authnRequest.setNameIDPolicy(nip);
+        
+        if (timeStr != null) {
+            authnRequest.setIssueInstant(
+                    new DateTime(Long.parseLong(timeStr) * 1000, ISOChronology.getInstanceUTC()));
+            if (sessionID != null) {
+                // Construct a pseudo message ID by combining the timestamp
+                // and a client-specific ID (the Java session ID).
+                // This allows for replay detection if the 
+                authnRequest.setID('_' + sessionID + '!' + timeStr);
+            } else {
+                authnRequest.setID(idGenerator.generateIdentifier());
+            }
+        } else {
+            authnRequest.setID(idGenerator.generateIdentifier());
+            authnRequest.setIssueInstant(new DateTime());
+        }
+        
+        return authnRequest;
+    }
+
+    /**
+     * Lookup the ACS URL for the specified SP entityID and binding URI.
+     * 
+     * @param mdProvider the SAML message context's metadata source
+     * @param entityId the SP entityID
+     * @return the resolved ACS URL endpoint
+     * @throws MessageDecodingException if there is an error resolving the ACS URL
+     */
+    @SuppressWarnings("unchecked")
+    private String lookupACSURL(MetadataProvider mdProvider, String entityId)
+            throws MessageDecodingException {
+        SPSSODescriptor spssoDesc = null;
+        try {
+            spssoDesc = (SPSSODescriptor) mdProvider.getRole(entityId, SPSSODescriptor.DEFAULT_ELEMENT_NAME,
+                    SAMLConstants.SAML20P_NS);
+        } catch (MetadataProviderException e) {
+            throw new MessageDecodingException("Error resolving metadata role for SP entityId: " + entityId, e);
+        }
+
+        if (spssoDesc == null) {
+            throw new MessageDecodingException(
+                    "SAML 2 SPSSODescriptor could not be resolved from metadata for SP entityID: " + entityId);
+        }
+
+        BasicEndpointSelector selector = new BasicEndpointSelector();
+        selector.setEntityRoleMetadata(spssoDesc);
+        selector.setEndpointType(AssertionConsumerService.DEFAULT_ELEMENT_NAME);
+        selector.getSupportedIssuerBindings().add(defaultBinding);
+
+        Endpoint endpoint = selector.selectEndpoint();
+        if (endpoint == null || endpoint.getLocation() == null) {
+            throw new MessageDecodingException(
+                    "SAML 2 ACS endpoint could not be resolved from metadata for SP entityID and binding: " + entityId
+                            + " -- " + defaultBinding);
+        }
+
+        return endpoint.getLocation();
+    }
+
+}
\ No newline at end of file