Create a login handler provides known authentication to an external authentication...
authorlajoie <lajoie@ab3bd59b-922f-494d-bb5f-6f0a3c29deca>
Sun, 20 Mar 2011 21:42:48 +0000 (21:42 +0000)
committerlajoie <lajoie@ab3bd59b-922f-494d-bb5f-6f0a3c29deca>
Sun, 20 Mar 2011 21:42:48 +0000 (21:42 +0000)
git-svn-id: https://subversion.switch.ch/svn/shibboleth/java-idp/branches/REL_2@3002 ab3bd59b-922f-494d-bb5f-6f0a3c29deca

doc/RELEASE-NOTES.txt
src/main/java/edu/internet2/middleware/shibboleth/idp/authn/provider/ExternalAuthnSystemLoginHandler.java [new file with mode: 0644]
src/main/java/edu/internet2/middleware/shibboleth/idp/authn/provider/ExternalAuthnSystemServlet.java [new file with mode: 0644]
src/main/java/edu/internet2/middleware/shibboleth/idp/util/HttpServletHelper.java
src/main/webapp/WEB-INF/web.xml

index 0de8554..3f5812d 100644 (file)
@@ -4,6 +4,8 @@ Changes in Release 2.3.0
 [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-442] - Add JSESSIONID and ClientIP to MDC
+[SIDP-448] - Create a login handler that provides authn "state" data to an external authentication 
+             system and has that system authenticate the user.
 [SIDP-461] - Add legacy Shib SSO protocol as binding for IdP-initiated SSO for SAML 2.0
 [SIDP-464] - An SPNameQualifier in NameIDPolicy always treated as an affiliation
 [SIDP-468] - Add taglib to simplify rendering login pages.
diff --git a/src/main/java/edu/internet2/middleware/shibboleth/idp/authn/provider/ExternalAuthnSystemLoginHandler.java b/src/main/java/edu/internet2/middleware/shibboleth/idp/authn/provider/ExternalAuthnSystemLoginHandler.java
new file mode 100644 (file)
index 0000000..75fd0ae
--- /dev/null
@@ -0,0 +1,226 @@
+/*
+ * 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.authn.provider;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.opensaml.util.URLBuilder;
+import org.opensaml.xml.util.DatatypeHelper;
+import org.opensaml.xml.util.Pair;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import edu.internet2.middleware.shibboleth.idp.authn.LoginContext;
+import edu.internet2.middleware.shibboleth.idp.util.HttpServletHelper;
+
+/**
+ * A login handler meant to bridge between the IdP and an external, web-based, authentication service.
+ * 
+ * This login handler will redirect the user-agent to a context-relative path and include the following query
+ * parameters: {@link #FORCE_AUTHN_PARAM}, {@link #PASSIVE_AUTHN_PARAM}, {@link #AUTHN_METHOD_PARAM},
+ * {@link #RETURN_URL_PARAM}, {@link #RELYING_PARTY_PARAM}.
+ * 
+ * The external authentication service must be configured to protect the page to which the user-agent is authenticated.
+ * This external service must populate the REMOTE_USER header with the principal name of the authenticated user. The
+ * external authentication service may also indicate which authentication method it actually performed by populating the
+ * HTTP header name {@value #AUTHN_METHOD_PARAM}.
+ */
+public class ExternalAuthnSystemLoginHandler extends AbstractLoginHandler {
+
+    /**
+     * Query parameter that indicates whether the authentication request requires forced authentication. Parameter Name:
+     * * {@value}
+     */
+    public static final String FORCE_AUTHN_PARAM = "forceAuthn";
+
+    /**
+     * Query parameter that indicates whether the authentication requires passive authentication. Parameter Name: * * *
+     * * {@value}
+     */
+    public static final String PASSIVE_AUTHN_PARAM = "isPassive";
+
+    /** Query parameter that provides which authentication method should be attempted. Parameter Name: {@value} */
+    public static final String AUTHN_METHOD_PARAM = "authnMethod";
+
+    /**
+     * Query parameter that provides the entity ID of the relying party that is requesting authentication. Parameter
+     * Name: {@value}
+     */
+    public static final String RELYING_PARTY_PARAM = "relyingParty";
+
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(RemoteUserLoginHandler.class);
+
+    /** The context-relative path to the SSO-protected Servlet. Default value: {@value} */
+    private String protectedPath = "/Authn/External";
+
+    /** Static query parameters sent to the SSO-protected Servlet. */
+    private Map<String, String> queryParameters;
+
+    /** Constructor. */
+    public ExternalAuthnSystemLoginHandler() {
+        super();
+        queryParameters = new HashMap<String, String>();
+    }
+
+    /**
+     * Get context-relative path to the SSO-protected Servlet.
+     * 
+     * @return context-relative path to the SSO-protected Servlet
+     */
+    public String getProtectedPath() {
+        return protectedPath;
+    }
+
+    /**
+     * Set context-relative path to the SSO-protected Servlet. The given path may not contain fragments and/or query
+     * params.
+     * 
+     * @param path context-relative path to the SSO-protected Servlet, may not be null or empty
+     */
+    public void setProtectedPath(String path) {
+        String trimmedPath = DatatypeHelper.safeTrimOrNullString(path);
+        if (trimmedPath == null) {
+            throw new IllegalArgumentException("Protected path may not be null or empty");
+        }
+
+        if (trimmedPath.contains("?")) {
+            throw new IllegalArgumentException("Protected path may not include query parameters");
+        }
+
+        if (trimmedPath.contains("#")) {
+            throw new IllegalArgumentException("Protected path may not include document fragements");
+        }
+
+        protectedPath = trimmedPath;
+    }
+
+    /**
+     * Gets the immutable set of query parameters sent to the SSO-protected Servlet.
+     * 
+     * @return immutable set of query parameters sent to the SSO-protected Servlet, never null
+     */
+    public Map<String, String> getQueryParameters() {
+        return queryParameters;
+    }
+
+    /**
+     * Sets the query parameters that will be sent to the SSO-protected Servlet.
+     * 
+     * @param params query parameters that will be sent to the SSO-protected Servlet, maybe null
+     */
+    public void setQueryParameters(Map<String, String> params) {
+        HashMap<String, String> newParams = new HashMap<String, String>();
+
+        String trimmedKeyName;
+        for (Entry<String, String> param : params.entrySet()) {
+            trimmedKeyName = DatatypeHelper.safeTrimOrNullString(param.getKey());
+            if (trimmedKeyName != null) {
+                newParams.put(trimmedKeyName, DatatypeHelper.safeTrimOrNullString(param.getValue()));
+            }
+        }
+
+        queryParameters = Collections.unmodifiableMap(newParams);
+    }
+
+    /** {@inheritDoc} */
+    public void login(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
+
+        // forward control to the servlet.
+        try {
+            String profileUrl = HttpServletHelper.getContextRelativeUrl(httpRequest, protectedPath).buildURL();
+
+            log.debug("Redirecting to {}", profileUrl);
+            httpResponse.sendRedirect(profileUrl);
+            return;
+        } catch (IOException ex) {
+            log.error("Unable to redirect to remote user authentication servlet.", ex);
+        }
+    }
+
+    /**
+     * Builds the URL that redirects to the external authentication service.
+     * 
+     * @param httpRequest current HTTP request
+     * 
+     * @return URL to which to redirect the user-agent
+     */
+    protected String buildRedirectUrl(HttpServletRequest httpRequest) {
+        URLBuilder urlBuilder = new URLBuilder();
+        urlBuilder.setScheme(httpRequest.getScheme());
+        urlBuilder.setHost(httpRequest.getServerName());
+        urlBuilder.setPort(httpRequest.getServerPort());
+
+        StringBuilder pathBuilder = new StringBuilder();
+        if (!"".equals(httpRequest.getContextPath())) {
+            pathBuilder.append(httpRequest.getContextPath());
+        }
+        if (!protectedPath.startsWith("/")) {
+            pathBuilder.append("/");
+        }
+        pathBuilder.append(protectedPath);
+        urlBuilder.setPath(pathBuilder.toString());
+
+        urlBuilder.getQueryParams().addAll(buildQueryParameters(httpRequest));
+
+        return urlBuilder.buildURL();
+    }
+
+    /**
+     * Builds the query parameters that will be sent to the external authentication service.
+     * 
+     * @param httpRequest current HTTP request
+     * 
+     * @return query parameters to be sent to the external authentication service
+     */
+    protected List<Pair<String, String>> buildQueryParameters(HttpServletRequest httpRequest) {
+        LoginContext loginContext = HttpServletHelper.getLoginContext(httpRequest);
+
+        ArrayList<Pair<String, String>> params = new ArrayList<Pair<String, String>>();
+
+        for (Entry<String, String> staticParam : queryParameters.entrySet()) {
+            params.add(new Pair<String, String>(staticParam.getKey(), staticParam.getValue()));
+        }
+
+        if (loginContext.isForceAuthRequired()) {
+            params.add(new Pair<String, String>(FORCE_AUTHN_PARAM, Boolean.TRUE.toString()));
+        } else {
+            params.add(new Pair<String, String>(FORCE_AUTHN_PARAM, Boolean.FALSE.toString()));
+        }
+
+        if (loginContext.isPassiveAuthRequired()) {
+            params.add(new Pair<String, String>(PASSIVE_AUTHN_PARAM, Boolean.TRUE.toString()));
+        } else {
+            params.add(new Pair<String, String>(PASSIVE_AUTHN_PARAM, Boolean.FALSE.toString()));
+        }
+
+        params.add(new Pair<String, String>(AUTHN_METHOD_PARAM, loginContext.getAttemptedAuthnMethod()));
+
+        params.add(new Pair<String, String>(RELYING_PARTY_PARAM, loginContext.getRelyingPartyId()));
+
+        return params;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/edu/internet2/middleware/shibboleth/idp/authn/provider/ExternalAuthnSystemServlet.java b/src/main/java/edu/internet2/middleware/shibboleth/idp/authn/provider/ExternalAuthnSystemServlet.java
new file mode 100644 (file)
index 0000000..4d22dd8
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * 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.authn.provider;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.opensaml.xml.util.DatatypeHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import edu.internet2.middleware.shibboleth.idp.authn.AuthenticationEngine;
+import edu.internet2.middleware.shibboleth.idp.authn.LoginHandler;
+import edu.internet2.middleware.shibboleth.idp.authn.UsernamePrincipal;
+
+/**
+ * Extracts the REMOTE_USER and, optionally, the method used to authentication the user and places the information in
+ * request attributes used by the authentication engine.
+ */
+public class ExternalAuthnSystemServlet extends HttpServlet {
+
+    /** Serial version UID. */
+    private static final long serialVersionUID = -6153665874235557534L;
+
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(ExternalAuthnSystemServlet.class);
+
+    /** {@inheritDoc} */
+    protected void service(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException,
+            IOException {
+        String principalName = httpRequest.getRemoteUser();
+
+        log.debug("User identified as {} returning control back to authentication engine", principalName);
+        httpRequest.setAttribute(LoginHandler.PRINCIPAL_KEY, new UsernamePrincipal(principalName));
+
+        String authnMethod = DatatypeHelper.safeTrimOrNullString(httpRequest
+                .getHeader(ExternalAuthnSystemLoginHandler.AUTHN_METHOD_PARAM));
+        if (authnMethod != null) {
+            log.debug("User {} authenticated by the method {}", principalName, authnMethod);
+            httpRequest.setAttribute(LoginHandler.AUTHENTICATION_METHOD_KEY, authnMethod);
+        }
+
+        AuthenticationEngine.returnToAuthenticationEngine(httpRequest, httpResponse);
+    }
+}
\ No newline at end of file
index 78737d8..1aae630 100644 (file)
@@ -31,6 +31,8 @@ import org.opensaml.xml.util.DatatypeHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import sun.security.action.GetLongAction;
+
 import edu.internet2.middleware.shibboleth.common.attribute.filtering.AttributeFilteringEngine;
 import edu.internet2.middleware.shibboleth.common.attribute.provider.SAML1AttributeAuthority;
 import edu.internet2.middleware.shibboleth.common.attribute.provider.SAML2AttributeAuthority;
@@ -176,6 +178,8 @@ public class HttpServletHelper {
         contextKeyCookie.setPath("".equals(httpRequest.getContextPath()) ? "/" : httpRequest.getContextPath());
         contextKeyCookie.setSecure(httpRequest.isSecure());
         httpResponse.addCookie(contextKeyCookie);
+        
+        httpRequest.setAttribute(LOGIN_CTX_KEY_NAME, loginContext);
     }
 
     /**
@@ -197,8 +201,8 @@ public class HttpServletHelper {
      * @return the service or null if there is no such service bound to the context
      */
     public static AttributeFilteringEngine<?> getAttributeFilterEnginer(ServletContext context) {
-        return getAttributeFilterEnginer(context, getContextParam(context, ATTRIBUTE_FILTER_ENGINE_SID_CTX_PARAM,
-                DEFAULT_ATTRIBUTE_FILTER_ENGINE_SID));
+        return getAttributeFilterEnginer(context,
+                getContextParam(context, ATTRIBUTE_FILTER_ENGINE_SID_CTX_PARAM, DEFAULT_ATTRIBUTE_FILTER_ENGINE_SID));
     }
 
     /**
@@ -221,8 +225,8 @@ public class HttpServletHelper {
      * @return the service or null if there is no such service bound to the context
      */
     public static AttributeResolver<?> getAttributeResolver(ServletContext context) {
-        return getAttributeResolver(context, getContextParam(context, ATTRIBUTE_RESOLVER_SID_CTX_PARAM,
-                DEFAULT_ATTRIBUTE_RESOLVER_SID));
+        return getAttributeResolver(context,
+                getContextParam(context, ATTRIBUTE_RESOLVER_SID_CTX_PARAM, DEFAULT_ATTRIBUTE_RESOLVER_SID));
     }
 
     /**
@@ -280,10 +284,12 @@ public class HttpServletHelper {
      * Gets the login context from the current request. The login context is only in this location while the request is
      * being transferred from the authentication engine back to the profile handler.
      * 
+     * This method only works during the first hand-off from the authentication engine to the login handler. Afterwords
+     * you must use {@link #getLoginContext(StorageService, ServletContext, HttpServletRequest)}.
+     * 
      * @param httpRequest current HTTP request
      * 
      * @return the login context or null if no login context is bound to the request
-     * @deprecated
      */
     public static LoginContext getLoginContext(HttpServletRequest httpRequest) {
         return (LoginContext) httpRequest.getAttribute(LOGIN_CTX_KEY_NAME);
@@ -348,8 +354,8 @@ public class HttpServletHelper {
      * @return the service or null if there is no such service bound to the context
      */
     public static IdPProfileHandlerManager getProfileHandlerManager(ServletContext context) {
-        return getProfileHandlerManager(context, getContextParam(context, PROFILE_HANDLER_MNGR_SID_CTX_PARAM,
-                DEFAULT_PROFILE_HANDLER_MNGR_SID));
+        return getProfileHandlerManager(context,
+                getContextParam(context, PROFILE_HANDLER_MNGR_SID_CTX_PARAM, DEFAULT_PROFILE_HANDLER_MNGR_SID));
     }
 
     /**
@@ -372,8 +378,8 @@ public class HttpServletHelper {
      * @return the service or null if there is no such service bound to the context
      */
     public static RelyingPartyConfigurationManager getRelyingPartyConfigurationManager(ServletContext context) {
-        return getRelyingPartyConfigurationManager(context, getContextParam(context, RP_CONFIG_MNGR_SID_CTX_PARAM,
-                DEFAULT_RP_CONFIG_MNGR_SID));
+        return getRelyingPartyConfigurationManager(context,
+                getContextParam(context, RP_CONFIG_MNGR_SID_CTX_PARAM, DEFAULT_RP_CONFIG_MNGR_SID));
     }
 
     /**
@@ -399,8 +405,8 @@ public class HttpServletHelper {
      * @deprecated use {@link #getRelyingPartyConfigurationManager(ServletContext)}
      */
     public static RelyingPartyConfigurationManager getRelyingPartyConfirmationManager(ServletContext context) {
-        return getRelyingPartyConfirmationManager(context, getContextParam(context, RP_CONFIG_MNGR_SID_CTX_PARAM,
-                DEFAULT_RP_CONFIG_MNGR_SID));
+        return getRelyingPartyConfirmationManager(context,
+                getContextParam(context, RP_CONFIG_MNGR_SID_CTX_PARAM, DEFAULT_RP_CONFIG_MNGR_SID));
     }
 
     /**
@@ -449,8 +455,8 @@ public class HttpServletHelper {
      * @return the service or null if there is no such service bound to the context
      */
     public static SAML1AttributeAuthority getSAML1AttributeAuthority(ServletContext context) {
-        return getSAML1AttributeAuthority(context, getContextParam(context, SAML1_AA_SID_CTX_PARAM,
-                DEFAULT_SAML1_AA_SID));
+        return getSAML1AttributeAuthority(context,
+                getContextParam(context, SAML1_AA_SID_CTX_PARAM, DEFAULT_SAML1_AA_SID));
     }
 
     /**
@@ -473,8 +479,8 @@ public class HttpServletHelper {
      * @return the service or null if there is no such service bound to the context
      */
     public static SAML2AttributeAuthority getSAML2AttributeAuthority(ServletContext context) {
-        return getSAML2AttributeAuthority(context, getContextParam(context, SAML2_AA_SID_CTX_PARAM,
-                DEFAULT_SAML2_AA_SID));
+        return getSAML2AttributeAuthority(context,
+                getContextParam(context, SAML2_AA_SID_CTX_PARAM, DEFAULT_SAML2_AA_SID));
     }
 
     /**
@@ -521,8 +527,8 @@ public class HttpServletHelper {
      * @return the service or null if there is no such service bound to the context
      */
     public static StorageService<?, ?> getStorageService(ServletContext context) {
-        return getStorageService(context, getContextParam(context, STORAGE_SERVICE_SID_CTX_PARAM,
-                DEFAULT_STORAGE_SERVICE_SID));
+        return getStorageService(context,
+                getContextParam(context, STORAGE_SERVICE_SID_CTX_PARAM, DEFAULT_STORAGE_SERVICE_SID));
     }
 
     /**
@@ -596,7 +602,7 @@ public class HttpServletHelper {
 
         String storageServicePartition = getContextParam(context, LOGIN_CTX_PARTITION_CTX_PARAM,
                 DEFAULT_LOGIN_CTX_PARITION);
-        
+
         log.debug("Removing LoginContext, with key {}, from StorageService partition {}", loginContextKey,
                 storageServicePartition);
         LoginContextEntry entry = (LoginContextEntry) storageService.remove(storageServicePartition, loginContextKey);
@@ -622,7 +628,7 @@ public class HttpServletHelper {
         urlBuilder.setPath(httpRequest.getContextPath());
         return urlBuilder;
     }
-    
+
     /**
      * Builds a URL to a path that is meant to be relative to the Servlet context.
      * 
@@ -631,22 +637,22 @@ public class HttpServletHelper {
      * 
      * @return URL builder containing the scheme, server name, server port, and full path
      */
-    public static URLBuilder getContextRelativeUrl(HttpServletRequest httpRequest, String path){
+    public static URLBuilder getContextRelativeUrl(HttpServletRequest httpRequest, String path) {
         URLBuilder urlBuilder = new URLBuilder();
         urlBuilder.setScheme(httpRequest.getScheme());
         urlBuilder.setHost(httpRequest.getServerName());
         urlBuilder.setPort(httpRequest.getServerPort());
-        
+
         StringBuilder pathBuilder = new StringBuilder();
-        if(!"".equals(httpRequest.getContextPath())){
+        if (!"".equals(httpRequest.getContextPath())) {
             pathBuilder.append(httpRequest.getContextPath());
         }
-        if(!path.startsWith("/")){
+        if (!path.startsWith("/")) {
             pathBuilder.append("/");
         }
         pathBuilder.append(DatatypeHelper.safeTrim(path));
         urlBuilder.setPath(pathBuilder.toString());
-        
+
         return urlBuilder;
     }
 }
\ No newline at end of file
index 89e0ed4..57278b9 100644 (file)
         <servlet-name>AuthenticationEngine</servlet-name>
         <url-pattern>/AuthnEngine</url-pattern>
     </servlet-mapping>
+    
+    <!-- Servlet protected by an external authentication service -->
+    <servlet>
+        <servlet-name>ExternalAuthHandler</servlet-name>
+        <servlet-class>edu.internet2.middleware.shibboleth.idp.authn.provider.ExternalAuthnSystemServlet</servlet-class>
+        <load-on-startup>3</load-on-startup>
+    </servlet>
 
-    <!-- Servlet protected by container user for RemoteUser authentication -->
+    <servlet-mapping>
+        <servlet-name>ExternalAuthHandler</servlet-name>
+        <url-pattern>/Authn/External</url-pattern>
+    </servlet-mapping>
+
+    <!-- Servlet protected by container used for RemoteUser authentication -->
     <servlet>
         <servlet-name>RemoteUserAuthHandler</servlet-name>
         <servlet-class>edu.internet2.middleware.shibboleth.idp.authn.provider.RemoteUserAuthServlet</servlet-class>
     <servlet>
         <servlet-name>UsernamePasswordAuthHandler</servlet-name>
         <servlet-class>edu.internet2.middleware.shibboleth.idp.authn.provider.UsernamePasswordLoginServlet</servlet-class>
-        <load-on-startup>4</load-on-startup>
+        <load-on-startup>3</load-on-startup>
     </servlet>
 
     <servlet-mapping>