com.microsoft.tfs.client.common.ui.dialogs.connect.ACSCredentialsDialogD11.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.tfs.client.common.ui.dialogs.connect.ACSCredentialsDialogD11.java

Source

// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See License.txt in the repository root.

package com.microsoft.tfs.client.common.ui.dialogs.connect;

import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.SAXParser;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.core.runtime.Preferences;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.LocationEvent;
import org.eclipse.swt.browser.LocationListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import com.microsoft.tfs.client.common.ui.Messages;
import com.microsoft.tfs.client.common.ui.TFSCommonUIClientPlugin;
import com.microsoft.tfs.client.common.ui.config.UIClientConnectionAdvisor;
import com.microsoft.tfs.client.common.ui.controls.generic.FullFeaturedBrowser;
import com.microsoft.tfs.client.common.ui.framework.WindowSystem;
import com.microsoft.tfs.client.common.ui.framework.helper.UIHelpers;
import com.microsoft.tfs.client.common.ui.framework.layout.GridDataBuilder;
import com.microsoft.tfs.client.common.ui.helpers.HTMLIncludeHelper;
import com.microsoft.tfs.client.common.ui.helpers.HTMLIncludeHelper.HTMLIncludeResourceProvider;
import com.microsoft.tfs.client.common.ui.prefs.UIPreferenceConstants;
import com.microsoft.tfs.core.config.ConnectionInstanceData;
import com.microsoft.tfs.core.httpclient.Cookie;
import com.microsoft.tfs.core.httpclient.CookieCredentials;
import com.microsoft.tfs.core.httpclient.Header;
import com.microsoft.tfs.core.httpclient.HttpClient;
import com.microsoft.tfs.core.httpclient.HttpStatus;
import com.microsoft.tfs.core.httpclient.NameValuePair;
import com.microsoft.tfs.core.httpclient.cookie.CookiePolicy;
import com.microsoft.tfs.core.httpclient.cookie.CookieSpec;
import com.microsoft.tfs.core.httpclient.methods.PostMethod;
import com.microsoft.tfs.core.util.URIUtils;
import com.microsoft.tfs.core.ws.runtime.exceptions.FederatedAuthException;
import com.microsoft.tfs.util.Check;
import com.microsoft.tfs.util.GUID;
import com.microsoft.tfs.util.Platform;
import com.microsoft.tfs.util.TypesafeEnum;
import com.microsoft.tfs.util.listeners.SingleListenerFacade;
import com.microsoft.tfs.util.xml.SAXUtils;

/**
 * Provide a dialog to handle ACS authentication.
 *
 * @threadsafety unknown
 */
public class ACSCredentialsDialogD11 extends CredentialsCompleteDialog {
    private static final Log log = LogFactory.getLog(ACSCredentialsDialogD11.class);

    /*
     * Minimum version of SWT required to use the HTML evaluate method. 3.5 and
     * later are supported.
     */
    private static final int MINIMUM_SWT_VERSION = 3500;

    /*
     * A property to define whether the browser-based login should be allowed.
     * Default is true.
     */
    private static final String ENABLE_BROWSER_AUTH_PROPERTY_NAME = "com.microsoft.tfs.client.common.ui.dialogs.connect.enablebrowserauth"; //$NON-NLS-1$

    /* Availability checks */
    private static Boolean browserAvailable = null;
    private static int browserAvailableForStyle;

    /* HTML to decorate our internal loading transient page */
    private static final String HTML_RESOURCE_PATH = "resources"; //$NON-NLS-1$
    private static final String LOADING_HTML_RESOURCE_NAME = "acscredentialsdialog.html"; //$NON-NLS-1$

    private final URI serverURI;
    private final URI serverSigninURL;
    private final FederatedAuthException exception;

    private static final String ACS_SCHEME = "https"; //$NON-NLS-1$
    private static final String ACS_DOMAIN = ".accesscontrol.windows.net"; //$NON-NLS-1$
    private static final String ACS_PATH = "/v2/wsfederation"; //$NON-NLS-1$
    private static final String ACS_QUERY = "wa=wsignin1.0"; //$NON-NLS-1$

    private final List<Cookie> cookies = new ArrayList<Cookie>();

    /*
     * Whether we should start processing on the authentication provider page
     * (to work around a webkit bug) instead of the normal processing beginning
     * at the ACS page.
     */
    private Boolean useAuthenticationProviderPage = null;

    private Text locationText;
    private FullFeaturedBrowser browser;

    private HttpClient httpClient;

    private final SingleListenerFacade credentialsCompleteListeners = new SingleListenerFacade(
            CredentialsCompleteListener.class);

    public ACSCredentialsDialogD11(final Shell parentShell, final URI serverURI, final URI serverSigninURL,
            final FederatedAuthException exception) {
        super(parentShell);

        Check.notNull(serverURI, "serverURI"); //$NON-NLS-1$
        Check.notNull(serverSigninURL, "serverSigninURL"); //$NON-NLS-1$
        Check.notNull(exception, "exception"); //$NON-NLS-1$

        this.serverURI = serverURI;
        this.serverSigninURL = serverSigninURL;
        this.exception = exception;

        setOptionIncludeDefaultButtons(false);
        setOptionEnforceMinimumSize(false);
    }

    /**
     * Gets whether {@link ACSCredentialsDialog} is available for the running
     * platform (browser requirements are met). This status is advisory - you
     * may still open a {@link ACSCredentialsDialog}, however it will likely not
     * provide authentication services to the client (likely due to lack of
     * Javascript.)
     *
     * @return <code>true</code> if {@link ACSCredentialsDialog} is supported on
     *         the running platform, <code>false</code> if minimum browser
     *         requirements are not met (see the log for details)
     */
    public static boolean isAvailable() {
        /*
         * No synchronization here because this method is always called on the
         * UI thread.
         */

        // If never checked or was checked for a different style, re-check
        final int browserStyle = getBrowserStyle();
        if (browserAvailable == null || browserStyle != browserAvailableForStyle) {
            browserAvailable = Boolean.valueOf(isAvailableInternal());
            browserAvailableForStyle = browserStyle;
        }

        return browserAvailable.booleanValue();
    }

    private static boolean isAvailableInternal() {
        final String enabledPropValue = System.getProperty(ENABLE_BROWSER_AUTH_PROPERTY_NAME);

        if (enabledPropValue != null && enabledPropValue.equalsIgnoreCase("false")) //$NON-NLS-1$
        {
            log.info("ACSCredentialsDialog asked not to start (via system property)"); //$NON-NLS-1$
            return false;
        }

        /*
         * OS X's SWT native browser is bad on Carbon, avoid it.
         * https://bugs.eclipse.org/bugs/show_bug.cgi?id=230035
         */
        if (WindowSystem.isCurrentWindowSystem(WindowSystem.CARBON)) {
            log.warn("ACSCredentialsDialog does not support SWT Browser on Mac OS Carbon"); //$NON-NLS-1$
            return false;
        }

        /*
         * FreeBSD's xulrunner version does not appear to handle the Javascript
         * used by the Live ID login page.
         */
        if (Platform.isCurrentPlatform(Platform.FREEBSD)) {
            log.warn("ACSCredentialsDialog does not support SWT Browser on FreeBSD"); //$NON-NLS-1$
            return false;
        }

        if (SWT.getVersion() < MINIMUM_SWT_VERSION) {
            log.warn(MessageFormat.format(
                    "SWT version {0} not new enough ({1} or newer required) to use ACSCredentialsDialog", //$NON-NLS-1$
                    Integer.toString(SWT.getVersion()), Integer.toString(MINIMUM_SWT_VERSION)));
            return false;
        }

        Shell shell = null;
        FullFeaturedBrowser browser = null;

        try {
            shell = new Shell();
            browser = new FullFeaturedBrowser(shell, SWT.NONE, getBrowserStyle());

            browser.setJavascriptEnabled(true);
            if (browser.getJavascriptEnabled() == false) {
                log.warn("Could not enable Javascript in SWT Browser for ACSCredentialsDialog"); //$NON-NLS-1$
                return false;
            }

            /*
             * On Windows, only IE 7 and newer will run our Javascript
             * correctly. We don't use XMLHttpRequest, but it's a good test for
             * IE 6, which doesn't have it (IE 7+ does).
             */
            if (Platform.isCurrentPlatform(Platform.WINDOWS) && browser.getBrowserType().equalsIgnoreCase("ie")) //$NON-NLS-1$
            {
                // Our Javascript needs a document to run with.
                browser.setText("<html></html>"); //$NON-NLS-1$

                final Object hasXMLHttpRequest = browser.evaluate("return ('XMLHttpRequest' in window);"); //$NON-NLS-1$

                if (hasXMLHttpRequest != null && hasXMLHttpRequest instanceof Boolean
                        && ((Boolean) hasXMLHttpRequest).booleanValue() == false) {
                    log.warn("IE major version 6 detected; this version not supported by ACSCredentialsDialog"); //$NON-NLS-1$
                    return false;
                }
            }

            // Success!
            log.info("SWT Browser successfully loaded for ACSCredentialsDialog"); //$NON-NLS-1$
            return true;
        } catch (final Throwable t) {
            log.warn("SWT Browser failed to load for ACSCredentialsDialog", t); //$NON-NLS-1$
            return false;
        } finally {
            if (browser != null) {
                browser.dispose();
            }

            if (shell != null) {
                shell.dispose();
            }
        }
    }

    private static int getBrowserStyle() {
        final Preferences preferences = TFSCommonUIClientPlugin.getDefault().getPluginPreferences();
        return preferences.getInt(UIPreferenceConstants.EMBEDDED_WEB_BROWSER_TYPE);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected String provideDialogTitle() {
        return MessageFormat.format(Messages.getString("ACSCredentialsDialog.DialogTitleFormat"), //$NON-NLS-1$
                this.serverURI.getHost());
    }

    @Override
    protected Point defaultComputeInitialSize() {
        // Set the default size of the ACS Dialog to be one that the standard
        // log-in page is set up for
        // But if running on a small screen, set to 75% of screen real estate.
        final Rectangle parentBounds = getParentShell().getMonitor().getClientArea();

        final int width = Math.min((int) (parentBounds.width * 0.75), 976);
        final int height = Math.min((int) (parentBounds.height * 0.75), 616);
        return new Point(width, height);
    }

    @Override
    protected void hookDialogIsOpen() {
        /*
         * Take the user to the ACS authentication URL once the dialog is open.
         * This allows our setText call, below, to decorate the loading page.
         * Without this, the user will simply see a blank white page while
         * loading the first page.
         */
        new Thread(new Runnable() {
            @Override
            public void run() {
                UIHelpers.runOnUIThread(true, new Runnable() {
                    @Override
                    public void run() {
                        if (!browser.isDisposed()) {
                            browser.setUrl(serverSigninURL.toString());
                        }
                    }
                });
            }
        }).start();
    }

    @Override
    protected void hookAddToDialogArea(final Composite dialogArea) {
        final GridLayout layout = new GridLayout(1, false);
        layout.marginWidth = 0;
        layout.marginHeight = 0;
        layout.horizontalSpacing = getHorizontalSpacing();
        layout.verticalSpacing = getVerticalSpacing();
        dialogArea.setLayout(layout);

        browser = new FullFeaturedBrowser(dialogArea, SWT.NONE, getBrowserStyle());

        GridDataBuilder.newInstance().grab().fill().wHint((int) (getMinimumMessageAreaWidth() * 1.5))
                .hHint(getMinimumMessageAreaWidth()).applyTo(browser);

        browser.setText(loadInterstitial());

        browser.addLocationListener(new LocationListener() {
            @Override
            public void changing(final LocationEvent event) {
                locationText.setText(event.location);

                /*
                 * The browser is trying to navigate to the signin completion
                 * page, that means the current page is the page that posts back
                 * to TFS (and thus gets cookies set.) Intercept here so that we
                 * can get the cookies and configure the profile appropriately.
                 *
                 * On error, show a dialog but still close the page.
                 */

                URI locationURI;

                try {
                    locationURI = URIUtils.newURI(event.location);
                } catch (final IllegalArgumentException e) {
                    log.warn(MessageFormat.format("Server redirected to an unparseable URL: {0}", event.location), //$NON-NLS-1$
                            e);
                    return;
                }

                ACSConfigurationResult configurationResult = ACSConfigurationResult.CONTINUE;

                try {
                    configurationResult = configureAuthenticationFromCurrentPage(locationURI);
                } catch (final Exception e) {
                    log.error("Could not process ACS authentication", e); //$NON-NLS-1$
                    configurationResult = ACSConfigurationResult.FAILURE;
                }

                /* Return to continue processing. */
                if (ACSConfigurationResult.CONTINUE.equals(configurationResult)) {
                    return;
                }

                /* Stop the browser, close this dialog and stop processing. */
                setReturnCode(ACSConfigurationResult.SUCCESS.equals(configurationResult) ? IDialogConstants.OK_ID
                        : IDialogConstants.CANCEL_ID);
                browser.stop();
                close();

                if (ACSConfigurationResult.FAILURE.equals(configurationResult)) {
                    MessageDialog.openError(getShell(),
                            Messages.getString("ACSCredentialsDialog.ACSFailedErrorTitle"), //$NON-NLS-1$
                            Messages.getString("ACSCredentialsDialog.ACSFailedErrorMessage")); //$NON-NLS-1$
                }

                event.doit = false;
                return;
            }

            @Override
            public void changed(final LocationEvent event) {
                final String message;

                log.debug("The location changed to the URL: " + event.location); //$NON-NLS-1$

                if (event.location.equalsIgnoreCase(serverSigninURL.toString())) {
                    if (exception.getCredentials() != null
                            && (exception.getCredentials() instanceof CookieCredentials)) {
                        message = MessageFormat.format(
                                Messages.getString("ACSCredentialsDialog.UnauthorizedErrorMessageFormat"), //$NON-NLS-1$
                                serverURI.toString());
                    } else {
                        message = Messages.getString("ACSCredentialsDialog.SignInMessage"); //$NON-NLS-1$
                    }

                    locationText.setText(message);
                } else {
                    message = Messages.getString("ACSCredentialsDialog.DoneMessageText"); //$NON-NLS-1$
                }

                locationText.setText(message);
            }
        });

        final Composite spacerComposite = new Composite(dialogArea, SWT.NONE);
        GridDataBuilder.newInstance().hGrab().hFill().applyTo(spacerComposite);

        final GridLayout spacerLayout = new GridLayout(1, false);
        spacerLayout.marginHeight = 0;
        spacerLayout.marginTop = 0;
        spacerLayout.marginBottom = getVerticalMargin() / 2;
        spacerLayout.marginWidth = getHorizontalMargin() / 2;
        spacerComposite.setLayout(spacerLayout);

        locationText = new Text(spacerComposite, SWT.READ_ONLY);
        locationText.setText(serverSigninURL.toString());
        locationText.setBackground(getShell().getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
        GridDataBuilder.newInstance().hGrab().hFill().applyTo(locationText);
    }

    @Override
    protected Control createButtonBar(final Composite parent) {
        return null;
    }

    @Override
    protected void hookDialogAboutToClose() {
        ((CredentialsCompleteListener) credentialsCompleteListeners.getListener()).credentialsComplete();
    }

    @Override
    public void addCredentialsCompleteListener(final CredentialsCompleteListener listener) {
        credentialsCompleteListeners.addListener(listener);
    }

    /*
     * On Mac OS we need to start our hijacking at the authentication provider's
     * page, when running under WebKit 534.56.5 (included with Mac OS 10.7.4.)
     */
    private boolean useAuthenticationProviderPage() {
        if (useAuthenticationProviderPage == null) {
            useAuthenticationProviderPage = Boolean.FALSE;

            if (Platform.isCurrentPlatform(Platform.MAC_OS_X)) {
                try {
                    final String userAgent = (String) browser.evaluate("return navigator.userAgent;"); //$NON-NLS-1$

                    if (userAgent.contains("AppleWebKit/534.56.5 ")) //$NON-NLS-1$
                    {
                        useAuthenticationProviderPage = Boolean.TRUE;
                    }
                } catch (final Exception e) {
                    log.warn("Could not determine Webkit browser version", e); //$NON-NLS-1$
                }
            }
        }

        return useAuthenticationProviderPage.booleanValue();
    }

    private ACSConfigurationResult configureAuthenticationFromCurrentPage(final URI locationURI) throws Exception {
        /*
         * Mac OS 10.7.4's Webkit has a bug where it caches post data
         * incorrectly, causing it to resubmit an old form back to ACS. Thus, we
         * need to hijack the form bound for ACS and submit it ourselves,
         * wherein we can process the result and send that to TFS service.
         */
        if (useAuthenticationProviderPage() && ACS_SCHEME.equalsIgnoreCase(locationURI.getScheme())
                && locationURI.getHost() != null && locationURI.getHost().toLowerCase().endsWith(ACS_DOMAIN)
                && ACS_PATH.equalsIgnoreCase(locationURI.getPath())
                && ACS_QUERY.equalsIgnoreCase(locationURI.getQuery())) {
            return configureAuthenticationFromAuthProviderPage();
        }

        /*
         * General case: hijack the post results going to TFS service and we'll
         * submit them ourselves to capture the cookie.
         */
        if (locationURI.getHost().equalsIgnoreCase(serverSigninURL.getHost())
                && locationURI.getPath().equalsIgnoreCase(serverSigninURL.getPath())) {
            final String[] queryPairs = locationURI.getQuery().split("&"); //$NON-NLS-1$

            for (final String pair : queryPairs) {
                final String[] keyValue = pair.split("=", 2); //$NON-NLS-1$
                final String key;
                final String value;

                try {
                    key = URLDecoder.decode(keyValue[0], "UTF-8"); //$NON-NLS-1$
                    value = URLDecoder.decode(keyValue[1], "UTF-8"); //$NON-NLS-1$
                } catch (final UnsupportedEncodingException e) {
                    log.warn("Could not decode location URI query parameters as UTF-8", e); //$NON-NLS-1$
                    break;
                }

                /*
                 * The "complete=1" param means that auth has completed and we
                 * can harvest the cookies from the ACS page.
                 */
                if ("complete".equalsIgnoreCase(key) && "1".equals(value)) //$NON-NLS-1$ //$NON-NLS-2$
                {
                    return configureAuthenticationFromACSPage();
                }
            }
        }

        /* This page is not helpful, continue processing. */
        return ACSConfigurationResult.CONTINUE;
    }

    /*
     * Configure authentication by submitting some authentication provider's
     * data to ACS, then taking the ACS result and submitting that back to TFS.
     * This is not the preferred way to handle this, but may be necessary if
     * there's some sort of browser bug in authentication handling w/ ACS.
     */
    private ACSConfigurationResult configureAuthenticationFromAuthProviderPage() throws Exception {
        final Object hasSwtForm = browser.evaluate(
                "if(document != null && document.forms != null && document.forms.length == 1) { return true; } return false;"); //$NON-NLS-1$

        if (!Boolean.TRUE.equals(hasSwtForm)) {
            throw new Exception("Document does not contain ACS form data"); //$NON-NLS-1$
        }

        /* Determine the location of the hidden ACS form */
        final String postLocation = (String) browser.evaluate("return document.forms[0].action;"); //$NON-NLS-1$

        /* Sanity check that we're posting back to our original server. */
        if (postLocation == null) {
            throw new Exception("ACS form does not have an action"); //$NON-NLS-1$
        }

        /* Determine the number of elements in the ACS form */
        int elementCount = 0;
        final Object elementCountObj = browser.evaluate("return document.forms[0].elements.length;"); //$NON-NLS-1$

        if (elementCountObj instanceof Integer) {
            elementCount = ((Integer) elementCountObj).intValue();
        } else if (elementCountObj instanceof Double) {
            elementCount = (int) (((Double) elementCountObj).doubleValue());
        } else {
            throw new Exception(
                    MessageFormat.format("Could not deserialize ACS form element length ({0})", elementCountObj)); //$NON-NLS-1$
        }

        if (elementCount == 0) {
            throw new Exception("No ACS form elements"); //$NON-NLS-1$
        }

        final NameValuePair[] postParameters = new NameValuePair[elementCount];
        for (int i = 0; i < elementCount; i++) {
            postParameters[i] = new NameValuePair(
                    (String) browser.evaluate(MessageFormat.format("return document.forms[0].elements[{0}].name;", //$NON-NLS-1$
                            Integer.toString(i))), (String) browser.evaluate(MessageFormat.format("return document.forms[0].elements[{0}].value;", //$NON-NLS-1$
                            Integer.toString(i))));
        }

        return configureAuthenticationFromAuthProvider(postLocation, postParameters);
    }

    private ACSConfigurationResult configureAuthenticationFromAuthProvider(final String target,
            final NameValuePair[] formInput) throws Exception {
        final URI targetURI;

        try {
            targetURI = new URI(target);
        } catch (final Exception e) {
            log.warn(MessageFormat.format("ACS form action is not parseable: {0}", target), e); //$NON-NLS-1$
            throw new Exception("ACS form action is not parseable"); //$NON-NLS-1$
        }

        /* Make sure we're posting back to ACS */
        if (!ACS_SCHEME.equalsIgnoreCase(targetURI.getScheme()) || targetURI.getHost() == null
                || !targetURI.getHost().toLowerCase().endsWith(ACS_DOMAIN)
                || !ACS_PATH.equalsIgnoreCase(targetURI.getPath())
                || !ACS_QUERY.equalsIgnoreCase(targetURI.getQuery())) {
            throw new Exception(MessageFormat.format("ACS form location is not in the domain: {0}", ACS_DOMAIN)); //$NON-NLS-1$
        }

        /*
         * Build a connection to the server to submit the form.
         */
        final HttpClient httpClient = getHttpClient();
        final PostMethod postMethod = new PostMethod(target);

        /*
         * Ignore cookies, do not follow redirects, do not do authentication. We
         * expect the resultant page (and only the resultant page) to populate
         * our cookies.
         */
        postMethod.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES);
        postMethod.setFollowRedirects(false);
        postMethod.setDoAuthentication(false);
        postMethod.getParams().setContentCharset("UTF-8"); //$NON-NLS-1$

        /* Add the ACS form elements */
        for (int i = 0; i < formInput.length; i++) {
            postMethod.addParameter(formInput[i]);
        }

        final int status = httpClient.executeMethod(postMethod);

        if (status != HttpStatus.SC_OK) {
            final String message = MessageFormat.format("ACS authentication did not return success: {0}", //$NON-NLS-1$
                    Integer.toString(status));
            throw new Exception(message);
        }

        final SAXParser acsResultParser = SAXUtils.newSAXParser();
        final ACSResultHandler acsResultHandler = new ACSResultHandler();
        acsResultParser.parse(postMethod.getResponseBodyAsStream(), acsResultHandler);

        final String finalTarget = acsResultHandler.getFormAction();
        final NameValuePair[] finalParameters = acsResultHandler.getFormInputs();

        return configureAuthenticationFromACS(finalTarget, finalParameters);
    }

    /*
     * Configure authentication by submitting the ACS result form to TFS, based
     * on the contents of the current browser's document. The current browser
     * document should have the form to post to ACS, which we will do. This is
     * generally the "best" way to handle authentication to TFS service as it
     * allows the browser to handle as much as possible.
     */
    private ACSConfigurationResult configureAuthenticationFromACSPage() throws Exception {
        final Object hasSwtForm = browser
                .evaluate("if(document != null && document.hiddenform != null) { return true; } return false;"); //$NON-NLS-1$

        if (!Boolean.TRUE.equals(hasSwtForm)) {
            throw new Exception("Document does not contain ACS form data"); //$NON-NLS-1$
        }

        /* Determine the location of the hidden ACS form */
        final String postLocation = (String) browser.evaluate("return document.hiddenform.action;"); //$NON-NLS-1$

        /* Sanity check that we're posting back to our original server. */
        if (postLocation == null) {
            throw new Exception("ACS form does not have an action"); //$NON-NLS-1$
        }

        /* Determine the number of elements in the ACS form */
        int elementCount = 0;
        final Object elementCountObj = browser.evaluate("return document.hiddenform.elements.length;"); //$NON-NLS-1$

        if (elementCountObj instanceof Integer) {
            elementCount = ((Integer) elementCountObj).intValue();
        } else if (elementCountObj instanceof Double) {
            elementCount = (int) (((Double) elementCountObj).doubleValue());
        } else {
            throw new Exception(
                    MessageFormat.format("Could not deserialize ACS form element length ({0})", elementCountObj)); //$NON-NLS-1$
        }

        if (elementCount == 0) {
            throw new Exception("No ACS form elements"); //$NON-NLS-1$
        }

        final NameValuePair[] postParameters = new NameValuePair[elementCount];
        for (int i = 0; i < elementCount; i++) {
            postParameters[i] = new NameValuePair(
                    (String) browser.evaluate(MessageFormat.format("return document.hiddenform.elements[{0}].name;", //$NON-NLS-1$
                            Integer.toString(i))), (String) browser.evaluate(MessageFormat
                            .format("return document.hiddenform.elements[{0}].value;", Integer.toString(i)))); //$NON-NLS-1$
        }

        return configureAuthenticationFromACS(postLocation, postParameters);
    }

    private ACSConfigurationResult configureAuthenticationFromACS(final String target,
            final NameValuePair[] formInput) throws Exception {
        final URI targetURI;

        try {
            targetURI = new URI(target);
        } catch (final Exception e) {
            log.warn(MessageFormat.format("ACS form action is not parseable: {0}", target), e); //$NON-NLS-1$
            throw new Exception("ACS form action is not parseable"); //$NON-NLS-1$
        }

        /* Make sure we're posting back to the original host */
        if (!targetURI.getHost().equalsIgnoreCase(serverSigninURL.getHost())) {
            throw new Exception("ACS form location does not match initial target"); //$NON-NLS-1$
        }

        /*
         * Build a connection to the server, just to get the cookies. We'll
         * throw this away.
         */
        final HttpClient httpClient = getHttpClient();
        final PostMethod postMethod = new PostMethod(target);

        /*
         * Ignore cookies, do not follow redirects, do not do authentication. We
         * expect the resultant page (and only the resultant page) to populate
         * our cookies.
         */
        postMethod.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES);
        postMethod.setFollowRedirects(false);
        postMethod.setDoAuthentication(false);

        /*
         * Set the content charset to UTF-8: this is the charset that the post
         * data will be encoded with. The default (according to RFC 2616) is
         * that the data is delivered as ISO-8859-1. IIS, however, appears to
         * want to decode this as UTF-8. The name of the Windows Live provider
         * is currently "Windows Live(tm) ID", which means that we need to
         * ensure that we encode the (tm) correctly.
         */
        postMethod.getParams().setContentCharset("UTF-8"); //$NON-NLS-1$

        /* Add the ACS form elements */
        for (int i = 0; i < formInput.length; i++) {
            postMethod.addParameter(formInput[i]);
        }

        final int status = httpClient.executeMethod(postMethod);

        final ArrayList<Cookie> fedAuthCookies = new ArrayList<Cookie>();

        /* Expect a 302 to the LocationService */
        if (status == HttpStatus.SC_MOVED_TEMPORARILY) {
            final Header[] cookieHeaders = postMethod.getResponseHeaders("Set-Cookie"); //$NON-NLS-1$

            if (cookieHeaders.length == 0) {
                throw new Exception("Team Foundation Server did not return FedAuth tokens"); //$NON-NLS-1$
            }

            /* Parse cookies according to RFC2109 */
            final CookieSpec cookieParser = CookiePolicy.getCookieSpec(CookiePolicy.RFC_2109);

            for (final Header cookieHeader : cookieHeaders) {
                /*
                 * Parse the cookie headers, store the serialized cookies in the
                 * profile.
                 */
                try {
                    final Cookie[] cookies = cookieParser.parse(targetURI, cookieHeader.getValue());

                    for (final Cookie cookie : cookies) {
                        if (cookie.getName().startsWith("FedAuth")) //$NON-NLS-1$
                        {
                            fedAuthCookies.add(cookie);
                        }
                    }

                } catch (final Exception e) {
                    log.warn(
                            MessageFormat.format("Could not parse authentication cookie {0}", //$NON-NLS-1$
                                    cookieHeader.getValue()), e);
                }
            }
        } else {
            String logMessage = "ACS authentication to TFS failed."; //$NON-NLS-1$
            final String exceptionMessage = MessageFormat.format(
                    "Received unexpected HTTP status code during authentication: {0}.", //$NON-NLS-1$
                    Integer.toString(status));

            if (status == HttpStatus.SC_INTERNAL_SERVER_ERROR) {
                try {
                    final Header[] headers = postMethod.getResponseHeaders("X-TFS-ServiceError"); //$NON-NLS-1$

                    for (final Header header : headers) {
                        logMessage += MessageFormat.format("  TFS error was: {0}", //$NON-NLS-1$
                                URLDecoder.decode(header.getValue(), "UTF-8")); //$NON-NLS-1$
                    }
                } catch (final Exception e) {
                    // suppress - must not have had service error headers.
                }
            }

            log.warn(logMessage);
            throw new Exception(exceptionMessage);
        }

        cookies.clear();
        cookies.addAll(fedAuthCookies);

        return ACSConfigurationResult.SUCCESS;
    }

    private HttpClient getHttpClient() {
        if (httpClient == null) {
            final ConnectionInstanceData instanceData = new ConnectionInstanceData(serverURI, GUID.newGUID());

            final UIClientConnectionAdvisor connectionAdvisor = new UIClientConnectionAdvisor();
            httpClient = connectionAdvisor.getHTTPClientFactory(instanceData).newHTTPClient();
        }

        return httpClient;
    }

    public List<Cookie> getFederatedAuthenticationCookies() {
        return cookies;
    }

    @Override
    public CookieCredentials getCredentials() {
        return new CookieCredentials(cookies.toArray(new Cookie[cookies.size()]));
    }

    private String loadInterstitial() {
        try {
            final HTMLIncludeHelper includeHelper = new HTMLIncludeHelper(new HTMLIncludeResourceProvider() {
                @Override
                public InputStream getInputStream(final String filename) {
                    Check.notNull(filename, "filename"); //$NON-NLS-1$

                    /*
                     * Note: class resources always use '/' as a separator, not
                     * local path characters.
                     */
                    final String resourcePath = HTML_RESOURCE_PATH + "/" + filename; //$NON-NLS-1$

                    return ACSCredentialsDialogD11.this.getClass().getResourceAsStream(resourcePath);
                }

                @Override
                public String getMessage(final String key) {
                    return Messages.getString(key);
                }
            });

            return includeHelper.readResource(LOADING_HTML_RESOURCE_NAME);
        } catch (final Exception e) {
            log.warn("Could not load interstitial ACS resource", e); //$NON-NLS-1$
            return Messages.getString("ACSCredentialsDialog.LoadingFallbackMessage"); //$NON-NLS-1$
        }
    }

    private static class ACSConfigurationResult extends TypesafeEnum {
        /**
         * Authentication is not complete, continue running the web-based
         * authentication.
         */
        public static final ACSConfigurationResult CONTINUE = new ACSConfigurationResult(0);

        /** Authentication completed, but failed. Stop processing. */
        public static final ACSConfigurationResult FAILURE = new ACSConfigurationResult(1);

        /** Authentication completed successfully. Stop processing. */
        public static final ACSConfigurationResult SUCCESS = new ACSConfigurationResult(2);

        private ACSConfigurationResult(final int value) {
            super(value);
        }
    }

    private static class ACSResultHandler extends DefaultHandler {
        private final List<String> elementHierarchy = new ArrayList<String>();

        private boolean inForm = false;
        private String formAction;
        private final List<NameValuePair> formInputs = new ArrayList<NameValuePair>();

        @Override
        public void startElement(final String namespaceURI, final String localName, final String qualifiedName,
                final Attributes attributes) throws SAXException {
            elementHierarchy.add(qualifiedName);

            if (isElement("html", "body", "form")) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            {
                if ("hiddenform".equals(attributes.getValue("name"))) //$NON-NLS-1$ //$NON-NLS-2$
                {
                    if (attributes.getValue("action") == null) //$NON-NLS-1$
                    {
                        throw new SAXException("ACS form lacks action"); //$NON-NLS-1$
                    }

                    inForm = true;
                    formAction = attributes.getValue("action"); //$NON-NLS-1$
                }
            } else if (inForm && isElement("html", "body", "form", "input")) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
            {
                if (!"hidden".equalsIgnoreCase(attributes.getValue("type"))) //$NON-NLS-1$ //$NON-NLS-2$
                {
                    throw new SAXException(
                            MessageFormat.format("Expected hidden input type in ACS result form, got: {0}", //$NON-NLS-1$
                                    attributes.getValue("type"))); //$NON-NLS-1$
                }

                if (attributes.getValue("name") == null || attributes.getValue("value") == null) //$NON-NLS-1$ //$NON-NLS-2$
                {
                    throw new SAXException("Malformed input element - missing name / value pair"); //$NON-NLS-1$
                }

                formInputs.add(new NameValuePair(attributes.getValue("name"), attributes.getValue("value"))); //$NON-NLS-1$ //$NON-NLS-2$
            }
        }

        @Override
        public void endElement(final String namespaceURI, final String localName, final String qualifiedName)
                throws SAXException {
            if (elementHierarchy.size() == 0) {
                throw new SAXException(MessageFormat.format("Malformed closing element: {0}", qualifiedName)); //$NON-NLS-1$
            }

            if (!qualifiedName.equalsIgnoreCase(elementHierarchy.get(elementHierarchy.size() - 1))) {
                throw new SAXException(
                        MessageFormat.format("Received unexpected closing element: {0}", qualifiedName)); //$NON-NLS-1$
            }

            if (inForm && isElement("html", "body", "form")) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            {
                inForm = false;
            }

            elementHierarchy.remove(elementHierarchy.size() - 1);
        }

        @Override
        public InputSource resolveEntity(final String publicId, final String systemId) throws SAXException {
            log.warn(MessageFormat.format("Refused to resolve entity {0} / {1}", publicId, systemId)); //$NON-NLS-1$

            return null;
        }

        private boolean isElement(final String... elements) {
            Check.notNull(elements, "elements"); //$NON-NLS-1$

            if (elementHierarchy.size() != elements.length) {
                return false;
            }

            for (int i = 0; i < elements.length; i++) {
                if (!elements[i].equalsIgnoreCase(elementHierarchy.get(i))) {
                    return false;
                }
            }

            return true;
        }

        public String getFormAction() {
            return formAction;
        }

        public NameValuePair[] getFormInputs() {
            return formInputs.toArray(new NameValuePair[formInputs.size()]);
        }
    }
}