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

Java tutorial

Introduction

Here is the source code for com.microsoft.tfs.client.common.ui.dialogs.connect.ACSCredentialsDialog.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.net.URI;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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.Browser;
import org.eclipse.swt.browser.BrowserFunction;
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 com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.microsoft.tfs.client.common.credentials.EclipseCredentialsManagerFactory;
import com.microsoft.tfs.client.common.ui.Messages;
import com.microsoft.tfs.client.common.ui.TFSCommonUIClientPlugin;
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.persistence.DefaultPersistenceStoreProvider;
import com.microsoft.tfs.core.credentials.CachedCredentials;
import com.microsoft.tfs.core.credentials.CredentialsManager;
import com.microsoft.tfs.core.httpclient.Cookie;
import com.microsoft.tfs.core.httpclient.CookieCredentials;
import com.microsoft.tfs.core.httpclient.Credentials;
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.Platform;
import com.microsoft.tfs.util.StringUtil;
import com.microsoft.tfs.util.TypesafeEnum;
import com.microsoft.tfs.util.listeners.SingleListenerFacade;

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

    private static final String COOKIE_PREFIX = "FedAuth"; //$NON-NLS-1$

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

    /*
     * 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 final List<Cookie> cookies = new ArrayList<Cookie>();

    private Text locationText;
    private FullFeaturedBrowser browser;
    private BrowserFunction notifyToken;

    private ACSConfigurationResult configurationResult = ACSConfigurationResult.CONTINUE;

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

    public ACSCredentialsDialog(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$

        this.serverURI = serverURI;
        this.serverSigninURL = getSignInURI(serverSigninURL);
        this.exception = exception;

        log.trace("serverURI = " + serverURI); //$NON-NLS-1$
        log.trace("serverSigninURL = " + serverSigninURL); //$NON-NLS-1$
        if (exception != null) {
            log.trace("exception: " + exception.getMessage()); //$NON-NLS-1$
            log.trace("    FedAuthIssuer = " + exception.getFedAuthIssuer()); //$NON-NLS-1$
            log.trace("    FedAuthRealm = " + exception.getFedAuthRealm()); //$NON-NLS-1$
            log.trace("    ServerURI = " + exception.getServerURI()); //$NON-NLS-1$
            log.trace("    Credentials = " //$NON-NLS-1$
                    + (exception.getCredentials() != null ? exception.getCredentials() : "null")); //$NON-NLS-1$
            log.trace("    Mechanisms = " + StringUtil.join(exception.getMechanisms(), ", ")); //$NON-NLS-1$ //$NON-NLS-2$
        }

        setOptionIncludeDefaultButtons(false);
        setOptionEnforceMinimumSize(false);
    }

    protected URI getSignInURI(final URI serverSigninURL) {
        String query = serverSigninURL.getQuery();

        if (query.indexOf("protocol=") < 0) //$NON-NLS-1$
        {
            query += "&protocol=javascriptnotify"; //$NON-NLS-1$
        }

        if (query.indexOf("force=") < 0) //$NON-NLS-1$
        {
            query += "&force=1"; //$NON-NLS-1$
        }

        if (query.indexOf("compact=") < 0) //$NON-NLS-1$
        {
            query += "&compact=1"; //$NON-NLS-1$
        }

        return URIUtils.newURI(serverSigninURL.getScheme(), serverSigninURL.getAuthority(),
                serverSigninURL.getPath(), query, serverSigninURL.getFragment());
    }

    /**
     * Gets whether {@link ACSCredentialsDialogBase} is available for the
     * running platform (browser requirements are met). This status is advisory
     * - you may still open a {@link ACSCredentialsDialogBase}, however it will
     * likely not provide authentication services to the client (likely due to
     * lack of Javascript.)
     *
     * @return <code>true</code> if {@link ACSCredentialsDialogBase} 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), 488);
        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()) {
                            log.debug("Go to the ACS authentication URL: " + serverSigninURL.toString()); //$NON-NLS-1$
                            browser.setUrl(serverSigninURL.toString());

                            /*
                             * Expect that a page may contain JavaScript
                             * notification providing FedAuth token. Define a
                             * handler for the JavaScript call of the
                             * window.notifyToken(String) method. The String
                             * parameter will contain a JSon object:
                             * {"securityToken":
                             * ["fedAuthCookie","fedAuthCookie1"
                             * ,...,"fedAuthCookieN"]}
                             */
                            notifyToken = new NotifyTokenBrowserFunction(browser.getBrowser(), "notifyToken"); //$NON-NLS-1$
                        }
                    }
                });
            }
        }).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);

        log.trace("get browser instance"); //$NON-NLS-1$
        browser = new FullFeaturedBrowser(dialogArea, SWT.NONE, getBrowserStyle());

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

        final String interstitialText = loadInterstitial();
        log.trace("set browser text to: " + interstitialText); //$NON-NLS-1$
        browser.setText(loadInterstitial());

        log.trace("add location listener"); //$NON-NLS-1$
        final LocationListener locationListener = getLocationListener();
        if (locationListener != null) {
            browser.addLocationListener(locationListener);
        }

        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);
    }

    protected LocationListener getLocationListener() {
        return new LocationListener() {
            @Override
            public void changing(final LocationEvent event) {
                log.debug("The browser redirects to the URL: " + event.location); //$NON-NLS-1$
                locationText.setText(event.location);
            }

            @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())) {
                    log.trace("location matches to serverSigninURL:" + serverSigninURL); //$NON-NLS-1$
                    if (exception != null && 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$
                }

                log.trace("set browser text to: " + message); //$NON-NLS-1$
                locationText.setText(message);
            }
        };
    }

    private void checkCredentials() {
        setReturnCode(ACSConfigurationResult.SUCCESS.equals(configurationResult) ? IDialogConstants.OK_ID
                : IDialogConstants.CANCEL_ID);

        log.debug("Close the browser: " //$NON-NLS-1$
                + (getReturnCode() == IDialogConstants.OK_ID ? "FedAuth* cookies found" //$NON-NLS-1$
                        : "the user has cancelled the browser dialod")); //$NON-NLS-1$

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

        browser.stop();
        close();
    }

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

    @Override
    protected void hookDialogAboutToClose() {
        if (notifyToken != null && !notifyToken.isDisposed()) {
            notifyToken.dispose();
            notifyToken = null;
        }

        if (cookies.size() > 0) {
            final Credentials newCredentials = new CookieCredentials(cookies.toArray(new Cookie[cookies.size()]));

            log.debug("New Cookie Credentials created"); //$NON-NLS-1$

            final CredentialsManager credentialsManager = EclipseCredentialsManagerFactory
                    .getCredentialsManager(DefaultPersistenceStoreProvider.INSTANCE);

            log.debug("Save the new Cookie Credentials in the Eclipse secure storage for future sessions."); //$NON-NLS-1$
            credentialsManager.setCredentials(new CachedCredentials(serverURI, newCredentials));
            log.debug("The new Cookie Credentials are saved to the Eclipse secure storage."); //$NON-NLS-1$
        }

        ((CredentialsCompleteListener) credentialsCompleteListeners.getListener()).credentialsComplete();
    }

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

    private void onNotifyToken(final List<String> fedAuthCookies) {
        Check.isTrue(fedAuthCookies.size() > 0, "fedAuthCookies.size > 0"); //$NON-NLS-1$

        log.debug("Generating FedAuth* cookies..."); //$NON-NLS-1$

        final String domain = serverURI.getHost();
        int port = serverURI.getPort();
        final boolean secure = "https".equalsIgnoreCase(serverURI.getScheme()); //$NON-NLS-1$
        if (port < 0) {
            if (secure) {
                port = 443;
            } else {
                port = 80;
            }
        }

        log.debug("   domain = " + domain); //$NON-NLS-1$
        log.debug("   port   = " + port); //$NON-NLS-1$

        cookies.clear();

        for (int k = 0; k < fedAuthCookies.size(); k++) {
            final Cookie cookie = new Cookie(domain, COOKIE_PREFIX + (k == 0 ? "" : String.valueOf(k)), //$NON-NLS-1$
                    fedAuthCookies.get(k));
            cookie.setPath("/"); //$NON-NLS-1$
            cookie.setPathAttributeSpecified(true);
            cookie.setSecure(secure);

            log.debug(cookie.getName() + " = " + cookie.toExternalForm()); //$NON-NLS-1$

            cookies.add(cookie);
        }

        configurationResult = ACSConfigurationResult.SUCCESS;
    }

    @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 ACSCredentialsDialog.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 class NotifyTokenBrowserFunction extends BrowserFunction {
        public NotifyTokenBrowserFunction(final Browser browser, final String name) {
            super(browser, name);
        }

        @Override
        public Object function(final Object[] arguments) {
            log.debug("window.notifyToken(...) invoked"); //$NON-NLS-1$

            Check.notNull(arguments, "arguments"); //$NON-NLS-1$
            Check.isTrue(arguments.length == 1, "arguments length"); //$NON-NLS-1$
            Check.isTrue(arguments[0] instanceof String, "argument[0]"); //$NON-NLS-1$

            final String arg = (String) arguments[0];
            log.debug("   argument = " + arg); //$NON-NLS-1$

            try {
                @SuppressWarnings("unchecked")
                final Map<String, List<String>> tokenData = getObjectMapper().readValue(arg, Map.class);
                onNotifyToken(tokenData.get("securityToken")); //$NON-NLS-1$
            } catch (final Exception e) {
            }

            checkCredentials();

            return null;
        }

        private ObjectMapper getObjectMapper() {
            final ObjectMapper objectMapper = new ObjectMapper();

            objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
            objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);

            return objectMapper;
        }
    }

    public static class VSTSCredentialsDialog extends ACSCredentialsDialog {

        public VSTSCredentialsDialog(final Shell parentShell) {
            super(parentShell, URIUtils.VSTS_ROOT_URL, URIUtils.VSTS_ROOT_URL, null);
        }

        @Override
        protected LocationListener getLocationListener() {
            return null;
        }

        @Override
        protected URI getSignInURI(final URI serverSigninURL) {
            final Map<String, String> queryParameters = new HashMap<String, String>();
            queryParameters.put("realm", URIUtils.TFS_REALM_URL_STRING); //$NON-NLS-1$
            queryParameters.put("protocol", "javascriptnotify"); //$NON-NLS-1$ //$NON-NLS-2$
            queryParameters.put("force", "1"); //$NON-NLS-1$ //$NON-NLS-2$
            queryParameters.put("compact", "1"); //$NON-NLS-1$ //$NON-NLS-2$

            return URIUtils.addQueryParameters(URIUtils.newURI(URIUtils.VSTS_ROOT_SIGNIN_URL_STRING),
                    queryParameters);
        }
    }
}