org.glite.slcs.shibclient.ShibbolethClient.java Source code

Java tutorial

Introduction

Here is the source code for org.glite.slcs.shibclient.ShibbolethClient.java

Source

/*
 * Copyright (c) 2007-2009. Members of the EGEE Collaboration.
 *
 * 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.
 * 
 * $Id: ShibbolethClient.java,v 1.19 2010/10/25 09:11:56 vtschopp Exp $
 */
package org.glite.slcs.shibclient;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.apache.commons.httpclient.Cookie;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.URI;
import org.apache.commons.httpclient.URIException;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.cookie.CookieSpecBase;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.glite.slcs.AuthException;
import org.glite.slcs.RemoteException;
import org.glite.slcs.SLCSConfigurationException;
import org.glite.slcs.ServiceException;
import org.glite.slcs.UnknownResourceException;
import org.glite.slcs.jericho.html.Element;
import org.glite.slcs.jericho.html.FormControl;
import org.glite.slcs.jericho.html.FormControlType;
import org.glite.slcs.jericho.html.Source;
import org.glite.slcs.jericho.html.Tag;
import org.glite.slcs.shibclient.metadata.IdentityProvider;
import org.glite.slcs.shibclient.metadata.ShibbolethClientMetadata;

/**
 * ShibbolethClient is a SSO login parser and a Shibboleth Browser/POST
 * (SAML/POST) or SAML/Artifact profile handler.
 * <p>
 * <b>This code is just a hack. Don't rely on it to develop something else, you
 * have been warned.</b>
 * 
 * @author Valery Tschopp <tschopp@switch.ch>
 * @version $Revision: 1.19 $
 */
public class ShibbolethClient {

    /** Logging */
    private static final Log LOG = LogFactory.getLog(ShibbolethClient.class);

    /** The HttpClient delegate */
    private HttpClient httpClient_;

    /** Shibboleth client metadata */
    private ShibbolethClientMetadata metadata_;

    /** Shibboleth client credentials */
    private ShibbolethCredentials credentials_;

    /** authenticated or not */
    private boolean isAuthenticated_ = false;

    /**
     * Creates a Shibboleth client wrapping a {@link HttpClient}. Requires the
     * Shibboleth client metadata and the Shibboleth credentials.
     * 
     * @param client
     *            The HttpClient engine.
     * @param metadata
     *            The Shibboleth client metadata
     * @param credentials
     *            The Shibboleth credentials (username and password)
     * @throws SLCSConfigurationException
     */
    public ShibbolethClient(HttpClient client, ShibbolethClientMetadata metadata, ShibbolethCredentials credentials)
            throws SLCSConfigurationException {
        // check consistancy of creds and metadata
        String id = credentials.getIdentityProviderID();
        IdentityProvider idp = metadata.getIdentityProvider(id);
        if (idp == null) {
            LOG.error("Unknown IdP " + id);
            throw new SLCSConfigurationException("IdP providerId: " + id
                    + " is not defined in metadata file.\nPlease run the 'slcs-info' command.");
        }

        this.httpClient_ = client;
        this.metadata_ = metadata;
        this.credentials_ = credentials;

    }

    /**
     * Authenticates the user with his IdP for the default SLCS SP.
     * 
     * @return <code>true</code> iff the user have been authenticated by his
     *         Identity Provider.
     * @throws ServiceException
     * @throws RemoteException
     * @throws AuthException
     * @throws UnknownResourceException
     */
    public boolean authenticate()
            throws AuthException, RemoteException, ServiceException, UnknownResourceException {
        String slcsEntryURL = metadata_.getSLCS().getUrl();
        return authenticate(slcsEntryURL);
    }

    /**
     * Authenticates the user on his IdP for the given SP.
     * 
     * @param spEntryURL
     *            The Service Provider entry point URL
     * @return <code>true</code> iff the user have been authenticated by his
     *         Identity Provider.
     * @throws AuthException
     * @throws RemoteException
     * @throws ServiceException
     * @throws UnknownResourceException
     */
    public boolean authenticate(String spEntryURL)
            throws AuthException, RemoteException, ServiceException, UnknownResourceException {

        String idpProviderID = credentials_.getIdentityProviderID();
        IdentityProvider idp = metadata_.getIdentityProvider(idpProviderID);
        if (idp == null) {
            throw new UnknownResourceException("IdP " + idpProviderID + " not found in Metadata");
        }
        LOG.info(spEntryURL + " IdP=" + idp.getUrl() + " AuthType=" + idp.getAuthTypeName());
        try {
            URI idpSSOResponseURI = null;

            // 1. get the first redirection, or the SAML2 DS response, or the
            // same
            // (already authN)
            URI spLoginResponseURI = processSPEntry(spEntryURL, idp);
            // either wayf or idp or same (already authenticated)
            String spLoginResponseURL = spLoginResponseURI.getEscapedURI();
            LOG.debug("spLoginResponseURL=" + spLoginResponseURL);
            // check if already authenticated (multiple call to authenticate)
            if (spLoginResponseURL.startsWith(spEntryURL)) {
                LOG.info("Already authenticated? " + isAuthenticated_ + ": " + spLoginResponseURL);
                return this.isAuthenticated_;
            }

            // 2. process the IdP SSO login
            idpSSOResponseURI = processIdPSSO(idp, spLoginResponseURI);

            // 3. process the IdP SSO response -> Artifact or Browser/POST
            // profile
            URI idpResponseURI = processIdPSSOResponse(idp, idpSSOResponseURI);
            String url = idpResponseURI.getURI();
            if (url.equals(spEntryURL)) {
                this.isAuthenticated_ = true;
                LOG.info("Sucessful authentication");
            }

        } catch (URIException e) {
            LOG.error("URIException: " + e);
            e.printStackTrace();
        } catch (HttpException e) {
            LOG.error("HttpException: " + e);
            e.printStackTrace();
        } catch (IOException e) {
            LOG.error("IOException: " + e);
            e.printStackTrace();
            throw new RemoteException(e);
        }
        return this.isAuthenticated_;
    }

    /**
     * Processes the response of the IdP SSO and dispatches to the Browser/POST
     * or the Artificate processor.
     * 
     * @param idp
     *            The {@link IdentityProvider}.
     * @param idpSSOResponseURI
     *            The IdP SSO response {@link URI}.
     * @return
     * @throws RemoteException
     */
    private URI processIdPSSOResponse(IdentityProvider idp, URI idpSSOResponseURI)
            throws HttpException, IOException, RemoteException {
        URI idpResponseURI = null;

        String idpSSOResponseURL = idpSSOResponseURI.getURI();
        GetMethod getIdPSSOResponseMethod = new GetMethod(idpSSOResponseURL);

        // BUG FIX: set the JSESSIONID and IdP 2.X _idp_session
        // cookies by hand.
        // Because default CookiePolicy is RFC2109 and doesn't handle
        // correctly FQDN hostname as cookie domain.
        /*
         * String host = getIdPSSOResponseMethod.getURI().getHost(); String path
         * = getIdPSSOResponseMethod.getURI().getPath(); Cookie cookies[] =
         * getMatchingCookies(host, path); for (int j = 0; j < cookies.length;
         * j++) { if (LOG.isDebugEnabled()) { LOG.debug("setting Cookie: " +
         * cookies[j]); } getIdPSSOResponseMethod.addRequestHeader("Cookie",
         * cookies[j].toString()); }
         */

        // BUG FIX: check if url have a SAML/Artifact endpoint
        boolean isPossiblyUsingArtifact = false;
        if (matchesSAMLArtifact(idpSSOResponseURL)) {
            isPossiblyUsingArtifact = true;
            LOG.info("The SP try to use a SAML/Artifact profile");
        }

        LOG.info("GET IdPSSOResponse: " + idpSSOResponseURL);
        int idpSSOResponseStatus = executeMethod(getIdPSSOResponseMethod);
        LOG.debug(getIdPSSOResponseMethod.getStatusLine());

        // SAML/Artifact are already processed.
        if (isPossiblyUsingArtifact) {
            // check if there is a loop. if so the SSO failed for some reason...
            if (idpSSOResponseURI.equals(getIdPSSOResponseMethod.getURI())) {
                String htmlBody = inputStreamToString(getIdPSSOResponseMethod.getResponseBodyAsStream());
                LOG.error("Something went wrong with the IdP SAML/Artifact response: " + htmlBody);
                throw new RemoteException("The Identity Provider SAML/Artifact response failed: " + idp.getUrl()
                        + ". Please see the log file.");
            }
            idpResponseURI = getIdPSSOResponseMethod.getURI();
        } else {
            // try to parse the Browser/POST profile in the HTML source
            InputStream htmlStream = getIdPSSOResponseMethod.getResponseBodyAsStream();
            idpResponseURI = processIdPBrowserPOST(idp, idpSSOResponseURI, htmlStream);
        }

        LOG.debug("getIdPSSOResponseMethod.releaseConnection()");
        getIdPSSOResponseMethod.releaseConnection();

        return idpResponseURI;
    }

    /**
     * Fully reads an {@link InputStream} into a {@link String}.
     * 
     * @param is
     *            The InputStream to read.
     * @return The context of the input stream.
     */
    private String inputStreamToString(InputStream is) {
        StringBuffer sb = new StringBuffer(1024);
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        try {
            char[] chars = new char[1024];
            while (reader.read(chars) > -1) {
                sb.append(chars);
                chars = new char[1024];
            }
            reader.close();
        } catch (IOException e) {
            LOG.error(e);
        }
        return sb.toString();
    }

    /**
     * Processes the IdP response as a Browser/POST
     * 
     * @param idp
     *            The {@link IdentityProvider}.
     * @param idpSSOResponseURI
     *            The IdP SSO reponse {@link URI}.
     * @return the SP URI to go to
     * @throws RemoteException
     */
    private URI processIdPBrowserPOST(IdentityProvider idp, URI idpSSOResponseURI, InputStream htmlStream)
            throws RemoteException {
        // return value
        URI browserPostResponseURI = null;
        RemoteException remoteException = null;

        try {
            Source source = new Source(htmlStream);
            List<Element> forms = source.findAllElements(Tag.FORM);
            if (!forms.isEmpty()) {
                // check if form contains a valid SAML Browser/POST
                for (Element form : forms) {
                    String spSAMLURL = form.getAttributeValue("ACTION");
                    LOG.debug("SAML Browser/POST URL=" + spSAMLURL);
                    if (spSAMLURL == null) {
                        // no SAML post URL found
                        String htmlBody = inputStreamToString(htmlStream);
                        LOG.error("No SAML Browser/POST FORM ACTION found: " + idpSSOResponseURI + ": " + htmlBody);
                        remoteException = new RemoteException("No SAML Browser/POST FORM ACTION found: "
                                + idpSSOResponseURI + ". Please see the log file.");

                        break; // exit loop
                    }

                    // create POST method
                    PostMethod postSPSAMLMethod = new PostMethod(spSAMLURL);
                    // add all HIDDEN fields to POST
                    List<FormControl> formControls = form.findFormControls();
                    for (FormControl control : formControls) {
                        FormControlType type = control.getFormControlType();
                        if (type.equals(FormControlType.HIDDEN)) {
                            String name = control.getName();
                            Collection<CharSequence> values = control.getValues();
                            for (CharSequence value : values) {
                                LOG.debug("HIDDEN " + name + "=" + value);
                                // add all hidden fields
                                postSPSAMLMethod.addParameter(name, (String) value);
                            }
                        }
                    }

                    // execute the SAML post
                    LOG.info("POST SPSAMLMethod: " + postSPSAMLMethod.getURI());
                    int spSAMLResponseStatus = executeMethod(postSPSAMLMethod);
                    LOG.debug(postSPSAMLMethod.getStatusLine());

                    // status must be 302 and redirect Location
                    Header location = postSPSAMLMethod.getResponseHeader("Location");
                    if (spSAMLResponseStatus == 302 && location != null) {
                        String url = location.getValue();
                        browserPostResponseURI = new URI(url, false);
                        LOG.debug("Redirect: " + browserPostResponseURI);

                    } else {
                        LOG.error(
                                "Unexpected SP response: Status=" + spSAMLResponseStatus + " Location=" + location);
                        remoteException = new RemoteException(
                                "Unexpected SP response: Status=" + spSAMLResponseStatus + " Location=" + location);
                    }

                    LOG.trace("postSPSAMLMethod.releaseConnection()");
                    postSPSAMLMethod.releaseConnection();

                } // forms loop
            } else {
                // no SAML post found
                String htmlBody = inputStreamToString(htmlStream);
                LOG.error("No SAML Browser/POST profile found: " + idpSSOResponseURI + ": " + htmlBody);
                remoteException = new RemoteException(
                        "No SAML Browser/POST profile found: " + idpSSOResponseURI + ". Please see the log file.");
            }

        } catch (URIException e) {
            e.printStackTrace();
            remoteException = new RemoteException(e.getMessage(), e);
        } catch (IOException e) {
            e.printStackTrace();
            remoteException = new RemoteException(e.getMessage(), e);
        }

        if (browserPostResponseURI == null) {
            if (remoteException != null) {
                throw remoteException;
            }
        }

        return browserPostResponseURI;
    }

    /**
     * Gets the Shibboleth session needed afterward.
     * <p>
     * Supports Shib1 (SAML1) WAFY and direct redirection to IdP, SAML2 direct
     * redirection to IdP, and SAML2 DS
     * 
     * @param entryURL
     * @param idp
     * @return
     * @throws URIException
     * @throws HttpException
     * @throws IOException
     * @throws RemoteException
     */
    private URI processSPEntry(String entryURL, IdentityProvider idp)
            throws URIException, HttpException, IOException, RemoteException, ServiceException {
        GetMethod getSPEntryMethod = new GetMethod(entryURL);
        LOG.info("GET SPEntryMethod: " + getSPEntryMethod.getURI());
        // get only the first redirect, if any
        getSPEntryMethod.setFollowRedirects(false);

        int spEntryStatus = executeMethod(getSPEntryMethod);
        String spEntryStatusLine = getSPEntryMethod.getStatusLine().toString();
        LOG.debug("spEntryStatusLine=" + spEntryStatusLine);

        URI loginResponseURI = getSPEntryMethod.getURI();
        LOG.debug("loginResponseURI=" + loginResponseURI);
        if (spEntryStatus == 302) {
            Header locationHeader = getSPEntryMethod.getResponseHeader("Location");
            String location = locationHeader.getValue();
            LOG.debug("Redirect location=" + location);
            loginResponseURI = new URI(location, true);
        }

        LOG.trace("getSPEntryMethod.releaseConnection()");
        getSPEntryMethod.releaseConnection();

        String loginResponseURL = loginResponseURI.getEscapedURI();
        LOG.debug("loginResponseURL=" + loginResponseURL);

        if (spEntryStatus == 200 && loginResponseURL.startsWith(entryURL)) {
            LOG.debug("SP entry response is the same, already authenticated?");
            return loginResponseURI;
        }
        if (spEntryStatus != 302) {
            throw new RemoteException(
                    "Unexpected SP response: " + spEntryStatusLine + " " + loginResponseURI + " for " + entryURL);
        }
        // checks if URL is Shib1 (SAML1) 'shire=' and 'target=' params (also
        // WAYF)
        if (loginResponseURL.indexOf("shire=") != -1 && loginResponseURL.indexOf("target=") != -1) {
            LOG.debug("loginResponseURL is Shib1 (SAML1, WAYF)");
            return loginResponseURI;

        }
        // checks if URL is SAML2 'SAMLRequest=' (direct redirection to IdP)
        // params
        else if (loginResponseURL.indexOf("SAMLRequest=") != -1) {
            LOG.debug("loginResponseURL is SAML2 redirection to IdP");
            return loginResponseURI;
        }
        // checks if URL is SAML2 'entityID=' and 'return=' (SAMLDS)
        else if (loginResponseURL.indexOf("entityID=") != -1 && loginResponseURL.indexOf("return=") != -1) {
            LOG.debug("loginResponseURL is SAML2 DiscoveryService");
            // redirect to return url with entityID of the IdP
            loginResponseURI = processSPSAML2DS(loginResponseURI, idp);
            return loginResponseURI;
        } else {
            String error = "SP response URL is not Shib1, nor SAML2 protocol: " + loginResponseURL;
            LOG.error(error);
            throw new RemoteException(error);
        }

    }

    /**
     * Redirect to SP SAMLDS return url with entityID of the IdP
     * 
     * @param spResponseURI
     * @param idp
     * @return
     * @throws NullPointerException
     * @throws ServiceException
     * @throws IOException
     * @throws HttpException
     * @throws RemoteException
     */
    private URI processSPSAML2DS(URI spResponseURI, IdentityProvider idp)
            throws NullPointerException, ServiceException, HttpException, IOException, RemoteException {
        String entityID = idp.getEntityID();
        if (entityID == null) {
            String error = "IdentityProvider " + idp.getId() + " doesn't have a SAML2 entityID";
            LOG.error(error);
            throw new ServiceException(error);
        }
        String returnURL = null;
        String query = spResponseURI.getEscapedQuery();
        LOG.debug("SAMLDS query: " + query);
        for (String param : query.split("&")) {
            if (param.startsWith("return=")) {
                returnURL = param.substring("return=".length());
                try {
                    returnURL = URLDecoder.decode(returnURL, "UTF-8");
                } catch (UnsupportedEncodingException e) {
                    // ignored, not possible
                }
            }
        }
        if (returnURL == null) {
            throw new ServiceException("SAML2 DS return URL not found in " + spResponseURI);
        }

        // add the IdP SAML2 entityID param
        returnURL += "&entityID=" + entityID;
        LOG.debug("return URL to SAMLDS: " + returnURL);

        GetMethod getSPSAML2DSMethod = new GetMethod(returnURL);
        getSPSAML2DSMethod.setFollowRedirects(false);
        int spSAML2DSStatus = executeMethod(getSPSAML2DSMethod);
        String spSAML2DSStatusLine = getSPSAML2DSMethod.getStatusLine().toString();
        LOG.debug("spSAML2DSStatusLine=" + spSAML2DSStatusLine);
        URI saml2DSResponseURI = getSPSAML2DSMethod.getURI();
        if (spSAML2DSStatus == 302) {
            Header locationHeader = getSPSAML2DSMethod.getResponseHeader("Location");
            String location = locationHeader.getValue();
            LOG.debug("Redirect location=" + location);
            saml2DSResponseURI = new URI(location, true);
        }

        LOG.trace("getSPSAML2DSMethod.releaseConnection()");
        getSPSAML2DSMethod.releaseConnection();

        if (spSAML2DSStatus != 302) {
            throw new RemoteException("Unexpected SAML2 DS response: " + spSAML2DSStatusLine + " ("
                    + saml2DSResponseURI + ") for " + returnURL);
        }

        return saml2DSResponseURI;

    }

    /**
     * @param idp
     * @param query
     * @throws URIException
     * @throws HttpException
     * @throws IOException
     * @throws AuthException
     * @throws RemoteException
     * @throws ServiceException
     */
    private URI processIdPSSO(IdentityProvider idp, URI spResponseURI)
            throws URIException, HttpException, IOException, AuthException, RemoteException, ServiceException {
        String idpSSOURL = idp.getUrl();
        LOG.debug("IdP SSO URL: " + idpSSOURL);
        String spResponseURL = spResponseURI.getEscapedURI();
        LOG.debug("spResponseURL=" + spResponseURL);
        // check if the spResponseURL is not already the IdP SSO url
        if (spResponseURL.startsWith(idpSSOURL)) {
            idpSSOURL = spResponseURL;
        } else {
            // if not, build it
            // BUG FIX: Shib SP 2.0 send the ?service=... in the query
            String query = spResponseURI.getEscapedQuery();
            LOG.debug("IdP SSO Query: " + query);
            if (query != null) {
                if (query.startsWith("service=")) {
                    int i = query.indexOf("shire");
                    if (i > 0) {
                        query = query.substring(i);
                        LOG.debug(
                                "IdP SSO Query contains the 'service=...' part, cutting up to 'shire=': " + query);
                    } else {
                        LOG.error(
                                "IdP SSO Query contains the 'service=' parameter, but not the 'shire=' parameter: "
                                        + query);
                        throw new RemoteException(
                                "IdP SSO Query contains the 'service=' parameter, but not the 'shire=' parameter: "
                                        + query);
                    }
                }
                // getIdpSSOMethod.setQueryString(query);
                idpSSOURL += "?" + query;
            }
            LOG.debug("IdP SSO URL (with query): " + idpSSOURL);
        }
        // create HttpMethod
        GetMethod getIdpSSOMethod = new GetMethod(idpSSOURL);

        URI idpSSOURI = getIdpSSOMethod.getURI();
        // set credential for basic or ntlm
        int authType = idp.getAuthType();
        LOG.debug("IdP authType: " + idp.getAuthTypeName());
        if (authType == IdentityProvider.SSO_AUTHTYPE_BASIC || authType == IdentityProvider.SSO_AUTHTYPE_NTLM) {
            // enable BASIC or NTLM authN
            AuthScope scope = new AuthScope(idpSSOURI.getHost(), idpSSOURI.getPort(), idp.getAuthRealm());
            LOG.info("Enable " + idp.getAuthTypeName() + " authentication: " + credentials_ + ", scope: " + scope);
            httpClient_.getState().setCredentials(scope, credentials_);
            getIdpSSOMethod.setDoAuthentication(true);
        }

        // execute the method
        LOG.info("GET IdpSSOMethod: " + idpSSOURI);
        int idpSSOResponseStatus = executeMethod(getIdpSSOMethod);
        String idpSSOResponseStatusLine = getIdpSSOMethod.getStatusLine().toString();
        LOG.debug(idpSSOResponseStatusLine);

        URI idpSSOResponseURI = getIdpSSOMethod.getURI();
        LOG.debug("idpSSOResponseURI=" + idpSSOResponseURI);
        String idpSSOResponseQuery = idpSSOResponseURI.getEscapedQuery();
        LOG.debug("idpSSOResponseQuery=" + idpSSOResponseQuery);

        // XXX
        dumpHttpClientCookies();

        LOG.debug("idpSSOResponseStatus=" + idpSSOResponseStatus);
        // BASIC or NTLM response handling
        if (idp.getAuthType() == IdentityProvider.SSO_AUTHTYPE_BASIC
                || idp.getAuthType() == IdentityProvider.SSO_AUTHTYPE_NTLM) {
            // !200 and same URL as before: if BASIC or NTLM failed on IdP

            LOG.debug("idpSSOURI=" + idpSSOURI);
            LOG.debug("idpSSOResponseURI=" + idpSSOResponseURI);

            if (idpSSOResponseStatus != 200 && idpSSOResponseURI.equals(idpSSOURI)) {

                LOG.trace("getIdpSSOMethod.releaseConnection()");
                getIdpSSOMethod.releaseConnection();

                LOG.error("BASIC or NTLM authentication was required for " + idpSSOURL);

                throw new AuthException(
                        idp.getAuthTypeName() + " authentication failed: " + idpSSOResponseStatusLine + " URL: "
                                + idpSSOResponseURI + " Credentials: " + this.credentials_);
            } else {
                // SAML/Artifact: idpSSOResponseURI is already SP login and the
                // XML <SLCSLoginResponse> is already there
                // XXX: resend to same IdP SSO page once again
                LOG.info("IdP BASIC or NTLM authN: resend again to the same IdP SSO URI: " + idpSSOURL);
                idpSSOResponseURI = new URI(idpSSOURL, false);
            }

        }
        // CAS sends 200 + Cookies and the login form directly
        else if (idpSSOResponseStatus == 200 && (idp.getAuthType() == IdentityProvider.SSO_AUTHTYPE_CAS
                || idp.getAuthType() == IdentityProvider.SSO_AUTHTYPE_FORM)) {
            LOG.debug("Process " + idp.getAuthTypeName() + " login form...");
            // process CAS login form
            InputStream idpLoginForm = getIdpSSOMethod.getResponseBodyAsStream();
            LOG.debug("idpSSOURI Query=" + idpSSOURI.getQuery());
            idpSSOResponseURI = processIdPLoginForm(idp, idpSSOResponseURI, idpSSOURI.getQuery(), idpLoginForm);
            LOG.debug(idp.getAuthTypeName() + " idpSSOResponseURI=" + idpSSOResponseURI);
        }
        // FIXME: ETHZ use a new PubCookie
        // PUBCOOKIE sends 200 + onload form with hidden fields to post
        else if (idpSSOResponseStatus == 200 && idp.getAuthType() == IdentityProvider.SSO_AUTHTYPE_PUBCOOKIE) {

            // parse <form> and extract hidden fields, then post
            PostMethod postPubcookieFormMethod = null;
            InputStream pubcookieFormStream = getIdpSSOMethod.getResponseBodyAsStream();
            Source source = new Source(pubcookieFormStream);
            List<Element> forms = source.findAllElements(Tag.FORM);
            for (Element form : forms) {
                String formAction = form.getAttributeValue("ACTION");
                LOG.debug("PubCookie form action=" + formAction);
                if (!idp.getAuthUrl().equalsIgnoreCase(formAction)) {
                    // TODO: ERROR
                    throw new RemoteException(
                            "form action: " + formAction + " doesn't match IdP authN url: " + idp.getAuthUrl());
                }
                // create PubCookie POST
                postPubcookieFormMethod = new PostMethod(formAction);

                // add all HIDDEN fields to POST
                List<FormControl> formControls = form.findFormControls();
                for (FormControl control : formControls) {
                    FormControlType type = control.getFormControlType();
                    if (type.equals(FormControlType.HIDDEN)) {
                        String name = control.getName();
                        Collection<String> values = control.getValues();
                        for (String value : values) {
                            LOG.debug("add hidden: " + name + "=" + value);
                            // add all hidden fields
                            postPubcookieFormMethod.addParameter(name, value);
                        }
                    }
                }

            } // for all forms

            LOG.trace("getIdpSSOMethod.releaseConnection()");
            getIdpSSOMethod.releaseConnection();

            if (postPubcookieFormMethod == null) {
                // ERROR
                LOG.error("PubCookie form not found");
                throw new RemoteException("PubCookie form not found");
            }

            //
            LOG.debug("POST " + postPubcookieFormMethod.getURI());
            int postPubcookieFormStatus = executeMethod(postPubcookieFormMethod);
            LOG.debug(postPubcookieFormMethod.getStatusLine());

            // XXX
            dumpHttpClientCookies();

            // process pubcookie login form
            InputStream loginFormStream = postPubcookieFormMethod.getResponseBodyAsStream();
            idpSSOResponseURI = processIdPLoginForm(idp, idpSSOResponseURI, idpSSOURI.getQuery(), loginFormStream);
            LOG.debug("Pubcookie idpSSOResponseURI=" + idpSSOResponseURI);

            LOG.trace("postPubcookieFormMethod.releaseConnection()");
            postPubcookieFormMethod.releaseConnection();

        } else {
            // error handling
            InputStream htmlStream = getIdpSSOMethod.getResponseBodyAsStream();
            String htmlBody = inputStreamToString(htmlStream);
            LOG.error("Unexpected IdP SSO reponse: URL: " + idpSSOResponseURI + " Status: "
                    + idpSSOResponseStatusLine + " AuthType: " + idp.getAuthTypeName() + " HTML:\n" + htmlBody);
            LOG.debug("getIdpSSOMethod.releaseConnection()");
            getIdpSSOMethod.releaseConnection();
            throw new RemoteException("Unexprected IdP SSO reponse: URL: " + idpSSOResponseURI + " Status: "
                    + idpSSOResponseStatusLine + ". Please see the log file.");
        }

        LOG.debug("getIdpSSOMethod.releaseConnection()");
        getIdpSSOMethod.releaseConnection();

        return idpSSOResponseURI;

    }

    /**
     * Parses and processes Pubcookie or CAS login form.
     * 
     * @param idp
     * @param htmlForm
     * @throws IOException
     * @throws RemoteException
     * @throws ServiceException
     * @throws AuthException
     */
    private URI processIdPLoginForm(IdentityProvider idp, URI ssoLoginURI, String ssoQuery, InputStream htmlForm)
            throws IOException, RemoteException, ServiceException, AuthException {
        LOG.info("Parse and process " + idp.getAuthTypeName() + " login form: " + ssoLoginURI);

        boolean formFound = false;
        URI idpLoginFormResponseURI = null;

        // Parse the FORM with Jericho HTML Parser
        Source source = new Source(htmlForm);
        List<Element> forms = source.findAllElements(Tag.FORM);
        for (Element form : forms) {
            String formName = form.getAttributeValue("NAME");
            // BUG FIX: UniL use a CAS login form with NO NAME defined.
            // first try with the form ID as NAME, otherwise use an empty name.
            // the metadata should also define an empty name for this particular
            // form.
            LOG.debug("form name= " + formName);
            if (formName == null) {
                LOG.warn("form have no NAME, try form ID...");
                String formId = form.getAttributeValue("ID");
                if (formId == null) {
                    LOG.warn("form have no NAME and no ID, using empty name...");
                    formName = "";
                } else {
                    formName = formId;
                }
            }

            if (formName.equals(idp.getAuthFormName())) {
                formFound = true;
                String formAction = form.getAttributeValue("ACTION");
                LOG.debug("form action=" + formAction);
                if (formAction == null || formAction.equals("")) {
                    // no form action to POST, use default from metadata
                    formAction = ssoLoginURI.getEscapedURI();
                    LOG.info("default form action=" + formAction);
                } else {
                    URI formActionURI = new URI(formAction, false);
                    if (formActionURI.isRelativeURI()) {
                        // action URL is not absolute like:
                        // http://localhost/cas/login?...
                        formActionURI = new URI(ssoLoginURI, formActionURI.getPathQuery(), true);
                    }
                    formAction = formActionURI.getEscapedURI();
                    LOG.info("corrected form action=" + formAction);
                }

                String formMethod = form.getAttributeValue("METHOD");
                LOG.debug("form name=" + formName + " action=" + formAction + " method=" + formMethod);

                if (!formAction.equals("") && formMethod.equalsIgnoreCase("POST")) {

                    PostMethod postLoginFormMethod = new PostMethod(formAction);

                    // add all HIDDEN fields to POST
                    List<FormControl> formControls = form.findFormControls();
                    for (FormControl control : formControls) {
                        FormControlType type = control.getFormControlType();
                        if (type.equals(FormControlType.HIDDEN)) {
                            String name = control.getName();
                            Collection<String> values = control.getValues();
                            for (String value : values) {
                                LOG.debug("add hidden: " + name + "=" + value);
                                // add all hidden fields
                                postLoginFormMethod.addParameter(name, value);
                            }
                        }
                    }
                    // add username field
                    postLoginFormMethod.addParameter(idp.getAuthFormUsername(), this.credentials_.getUserName());
                    // add the PASSWORD field
                    postLoginFormMethod.addParameter(idp.getAuthFormPassword(), this.credentials_.getPassword());

                    // execute the login POST
                    LOG.info("POST LoginFormMethod: " + postLoginFormMethod.getURI());

                    int formLoginResponseStatus = executeMethod(postLoginFormMethod);
                    LOG.debug(postLoginFormMethod.getStatusLine());

                    // XXX
                    dumpHttpClientCookies();

                    // CAS, or FORM can, send a 302 + Location header back
                    if (formLoginResponseStatus == 302 && (idp.getAuthType() == IdentityProvider.SSO_AUTHTYPE_CAS
                            || idp.getAuthType() == IdentityProvider.SSO_AUTHTYPE_FORM)) {
                        LOG.debug("Process " + idp.getAuthTypeName()
                                + " redirect response (302 + Location header)...");
                        Header location = postLoginFormMethod.getResponseHeader("Location");
                        if (location != null) {
                            String locationURL = location.getValue();
                            LOG.debug("302 Location: " + locationURL);
                            // CAS: if location path (/cas/login) is not the IdP
                            // 1.3
                            // SSO path (/shibboleth-idp/SSO) or the IdP 2.X
                            // /Authn/RemoteUser
                            // handler, then it's a wrong login
                            URI locationURI = new URI(locationURL, false);
                            String locationPath = locationURI.getPath();
                            String idpSSOURL = idp.getUrl();
                            URI idpSSOURI = new URI(idpSSOURL, false);
                            String idpSSOPath = idpSSOURI.getPath();
                            if (LOG.isDebugEnabled()) {
                                LOG.debug("location path: " + locationPath);
                                LOG.debug("location is the /Authn/RemoteUser hanlder? "
                                        + locationPath.endsWith("/Authn/RemoteUser"));
                                LOG.debug("IdP SSO path: " + idpSSOPath);
                            }
                            if (!locationPath.equals(idpSSOPath) && !locationPath.endsWith("/Authn/RemoteUser")) {
                                LOG.error("Redirect response is not the SSO (" + idpSSOURL
                                        + ") or the /Authn/RemoteUser handler: " + locationURL);
                                throw new AuthException(
                                        idp.getAuthTypeName() + " Authentication failed: " + this.credentials_);
                            }
                            idpLoginFormResponseURI = new URI(locationURL, false);
                            LOG.debug("(" + idp.getAuthTypeName() + ": 302 + Location) idpLoginFormReponseURI= "
                                    + idpLoginFormResponseURI);
                        } else {
                            LOG.error(idp.getAuthTypeName() + ": Status 302 but no redirect Location header");
                            throw new AuthException(
                                    idp.getAuthTypeName() + " Authentication failed: " + this.credentials_);
                        }
                    }
                    // IdP 2.1 FORM authN send 200 and directly the SAMLResponse
                    // form
                    else if (formLoginResponseStatus == 200
                            && idp.getAuthType() == IdentityProvider.SSO_AUTHTYPE_FORM) {
                        // BUG FIX: check for Browser/POST hidden form element
                        // SAMLResponse for valid authentication
                        LOG.debug("check for SAMLResponse hidden element");
                        boolean samlResponseFound = false;
                        InputStream authnLoginResponse = postLoginFormMethod.getResponseBodyAsStream();
                        Source authnSource = new Source(authnLoginResponse);
                        List<Element> browserPOSTForms = authnSource.findAllElements(Tag.FORM);
                        for (Element browserPOSTForm : browserPOSTForms) {
                            List<FormControl> browserPOSTFormControls = browserPOSTForm.findFormControls();
                            for (FormControl control : browserPOSTFormControls) {
                                FormControlType type = control.getFormControlType();
                                if (type.equals(FormControlType.HIDDEN)) {
                                    String name = control.getName();
                                    if (name.equals("SAMLResponse")) {
                                        LOG.debug("Hidden element found: " + control.getName());
                                        samlResponseFound = true;
                                    }
                                }
                            }
                        }
                        if (!samlResponseFound) {
                            LOG.error(
                                    idp.getAuthTypeName() + ": no Browser/POST SAMLResponse hidden element found");
                            throw new AuthException(
                                    idp.getAuthTypeName() + " Authentication failed: " + this.credentials_);

                        }

                        LOG.debug("Process FORM (200 + full Browser/POST profile) response...");
                        idpLoginFormResponseURI = new URI(idp.getUrl(), false);
                        // re-set the original SSO query params
                        idpLoginFormResponseURI.setQuery(ssoQuery);
                        LOG.debug("(FORM: 200 + Browser/POST) idpLoginFormReponseURI= " + idpLoginFormResponseURI);
                    }
                    // Pubcookie send 200 + fucking HTML form relay with hidden
                    // fields!!!
                    // <form method=post
                    // action="https://aai-login.ethz.ch/PubCookie.reply"
                    // name=relay>
                    // then reply a redirect 302 + Location header
                    else if (formLoginResponseStatus == 200
                            && idp.getAuthType() == IdentityProvider.SSO_AUTHTYPE_PUBCOOKIE) {
                        LOG.debug("Process Pubcookie (200 + relay FORM) response...");
                        InputStream pubcookieLoginResponse = postLoginFormMethod.getResponseBodyAsStream();
                        Source pubcookieSource = new Source(pubcookieLoginResponse);
                        PostMethod postPubcookieRelayMethod = null;
                        List<Element> relayForms = pubcookieSource.findAllElements(Tag.FORM);
                        for (Element relayForm : relayForms) {
                            String relayFormAction = relayForm.getAttributeValue("ACTION");
                            LOG.debug("Pubcookie relay form action= " + relayFormAction);
                            if (relayFormAction == null) {
                                LOG.error("Pubcookie relay form action not found.");
                                throw new RemoteException("Pubcookie relay form action not found");
                            }
                            // create PubCookie relay POST
                            postPubcookieRelayMethod = new PostMethod(relayFormAction);

                            // add all HIDDEN fields to POST
                            List<FormControl> relayFormControls = relayForm.findFormControls();
                            for (FormControl control : relayFormControls) {
                                FormControlType type = control.getFormControlType();
                                if (type.equals(FormControlType.HIDDEN)) {
                                    String name = control.getName();
                                    Collection<String> values = control.getValues();
                                    for (String value : values) {
                                        LOG.debug("add hidden: " + name + "=" + value);
                                        // add all hidden fields
                                        postPubcookieRelayMethod.addParameter(name, value);
                                    }
                                }
                            } // add hidden fields
                        } // for all relay forms

                        if (postPubcookieRelayMethod != null) {
                            LOG.debug("POST postPubcookieRelayMethod: " + postPubcookieRelayMethod.getURI());
                            int pubcookieRelayStatus = executeMethod(postPubcookieRelayMethod);
                            LOG.debug(postPubcookieRelayMethod.getStatusLine());
                            Header location = postPubcookieRelayMethod.getResponseHeader("Location");
                            LOG.debug("postPubcookieRelayMethod.releaseConnection()");
                            postPubcookieRelayMethod.releaseConnection();
                            if (location != null) {
                                String locationURL = location.getValue();
                                LOG.debug("302 Location: " + locationURL);
                                // parse Location
                                idpLoginFormResponseURI = new URI(locationURL, false);
                                LOG.debug("(PubCookie: 302 + Location header) idpLoginFormReponseURI= "
                                        + idpLoginFormResponseURI);
                            } else {
                                LOG.error("Pubcookie relay response 302 + Location header not found");
                                throw new AuthException(
                                        idp.getAuthTypeName() + " Authentication failed: " + this.credentials_);
                            }
                        } else {
                            LOG.error("Pubcookie relay form not found");
                            throw new AuthException(
                                    idp.getAuthTypeName() + " Authentication failed: " + this.credentials_);
                        }

                        // XXX
                        dumpHttpClientCookies();
                    } else {
                        LOG.error("Unexpected response status: " + formLoginResponseStatus + " AuthType:"
                                + idp.getAuthTypeName());
                        throw new AuthException(
                                idp.getAuthTypeName() + " Authentication failed: " + this.credentials_);
                    }

                    LOG.debug("POSTLoginFormMethod.releaseConnection()");
                    postLoginFormMethod.releaseConnection();

                } // end if form action is set and method is POST
            } // end if form name match metadata
        } // end for all forms

        if (!formFound) {
            LOG.error("FORM name=" + idp.getAuthFormName() + " not found");
            throw new ServiceException("FORM name=" + idp.getAuthFormName() + " not found");
        }

        return idpLoginFormResponseURI;

    }

    /**
     * @return isAuthenticated
     */
    public boolean isAuthenticated() {
        return this.isAuthenticated_;
    }

    /**
     * Returns <code>true</code> if the url contains a SAML/Artifact service
     * endpoint.
     * 
     * @param url
     *            The url (with query parameter) to check.
     * @return <code>true</code> if the url contains a SAML/Artifact service
     *         url.
     */
    private boolean matchesSAMLArtifact(String url) {
        if (url.indexOf("/SAML/Artifact") != -1) {
            return true;
        } else if (url.indexOf("/SAML2/Artifact") != -1) {
            return true;
        }
        return false;
    }

    /**
     * Delegates execution of the HttpMethod to the underlying HttpClient.
     * 
     * @param method
     *            The HttpMethod to execute.
     * @return The method's response code.
     * @throws HttpException
     *             If an I/O (transport) error occurs.
     * @throws IOException
     *             If a protocol exception occurs.
     */
    public int executeMethod(HttpMethod method) throws HttpException, IOException {
        if (LOG.isTraceEnabled())
            LOG.trace("exec: " + method.getName() + " " + method.getURI());
        // use delegate
        return this.httpClient_.executeMethod(method);
    }

    private void dumpHttpClientCookies() {
        if (LOG.isDebugEnabled()) {
            Cookie[] cookies = this.httpClient_.getState().getCookies();
            StringBuffer sb = new StringBuffer();
            sb.append("\n");
            sb.append("---[CookiePolicy=").append(httpClient_.getParams().getCookiePolicy()).append("]---\n");
            for (int i = 0; i < cookies.length; i++) {
                Cookie cookie = cookies[i];
                String path = cookie.getPath();
                String domain = cookie.getDomain();
                boolean secure = cookie.getSecure();
                int version = cookie.getVersion();
                // sb.append(name).append('=').append(value).append("\n");
                sb.append(i).append(": ").append(cookie);
                sb.append(" domain:").append(domain);
                sb.append(" path:").append(path);
                sb.append(" secure:").append(secure);
                sb.append(" version:").append(version);
                sb.append("\n");
            }
            sb.append("---[End]---");
            LOG.debug(sb.toString());
        }
    }

    /**
     * Returns the <b>first</b> Cookie or <code>null</code> if not exists.
     * 
     * @param name
     *            The Cookie name
     * @return The cookie or <code>null</code>
     */
    private Cookie getCookie(String name) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Cookie name=" + name);
        }
        Cookie[] cookies = this.httpClient_.getState().getCookies();
        for (int i = 0; i < cookies.length; i++) {
            String cookieName = cookies[i].getName();
            if (name.equals(cookieName)) {
                return cookies[i];
            }
        }
        return null;
    }

    private Cookie[] getMatchingCookies(String host, String path) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("search all Cookies matching for host:" + host + " path:" + path);
        }
        List<Cookie> matchingCookies = new ArrayList<Cookie>();
        Cookie[] cookies = this.httpClient_.getState().getCookies();
        CookieSpecBase cookieSpecBase = new CookieSpecBase();
        for (int i = 0; i < cookies.length; i++) {
            Cookie cookie = cookies[i];
            if (cookieSpecBase.match(host, 443, path, true, cookie)) {
                LOG.debug("Cookie " + cookie + " matched");
                matchingCookies.add(cookie);
            }
        }
        return (Cookie[]) matchingCookies.toArray(new Cookie[matchingCookies.size()]);
    }

    private Cookie[] getMatchingCookies(String name, String host, String path) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("search Cookie matching name:" + name + " host:" + host + " path:" + path);
        }
        List<Cookie> matchingCookies = new ArrayList<Cookie>();
        Cookie[] cookies = this.httpClient_.getState().getCookies();
        CookieSpecBase cookieSpecBase = new CookieSpecBase();
        for (int i = 0; i < cookies.length; i++) {
            Cookie cookie = cookies[i];
            if (cookieSpecBase.match(host, 443, path, true, cookie)) {
                if (cookie.getName().equals(name)) {
                    LOG.debug("Cookie " + cookie + " matched");
                    matchingCookies.add(cookie);
                }
            }
        }
        return (Cookie[]) matchingCookies.toArray(new Cookie[matchingCookies.size()]);
    }

}