com.microsoft.tfs.client.common.credentials.CredentialsHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.tfs.client.common.credentials.CredentialsHelper.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.credentials;

import java.net.URI;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.TimeZone;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.microsoft.alm.auth.Authenticator;
import com.microsoft.alm.auth.PromptBehavior;
import com.microsoft.alm.auth.oauth.DeviceFlowResponse;
import com.microsoft.alm.auth.oauth.OAuth2Authenticator;
import com.microsoft.alm.auth.pat.VstsPatAuthenticator;
import com.microsoft.alm.client.TeeClientHandler;
import com.microsoft.alm.helpers.Action;
import com.microsoft.alm.secret.Token;
import com.microsoft.alm.secret.TokenPair;
import com.microsoft.alm.secret.TokenType;
import com.microsoft.alm.secret.VsoTokenScope;
import com.microsoft.alm.storage.InsecureInMemoryStore;
import com.microsoft.alm.storage.SecretStore;
import com.microsoft.alm.visualstudio.services.account.client.AccountHttpClient;
import com.microsoft.alm.visualstudio.services.delegatedauthorization.SessionToken;
import com.microsoft.tfs.client.common.Messages;
import com.microsoft.tfs.client.common.config.CommonClientConnectionAdvisor;
import com.microsoft.tfs.core.TFSConnection;
import com.microsoft.tfs.core.TFSTeamProjectCollection;
import com.microsoft.tfs.core.credentials.CachedCredentials;
import com.microsoft.tfs.core.credentials.CredentialsManager;
import com.microsoft.tfs.core.httpclient.Credentials;
import com.microsoft.tfs.core.httpclient.JwtCredentials;
import com.microsoft.tfs.core.httpclient.PreemptiveUsernamePasswordCredentials;
import com.microsoft.tfs.core.httpclient.UsernamePasswordCredentials.PatCredentials;
import com.microsoft.tfs.core.util.URIUtils;
import com.microsoft.tfs.jni.helpers.LocalHost;
import com.microsoft.tfs.util.StringUtil;

/**
 * Static methods to manipulate {@link SessionToken}s.
 *
 * @threadsafety thread-safe
 */

public abstract class CredentialsHelper {
    private final static Log log = LogFactory.getLog(CredentialsHelper.class);

    /**
     * Constants for OAuth2 Interactive Browser logon flow
     */
    private final static String CLIENT_ID = "97877f11-0fc6-4aee-b1ff-febb0519dd00"; //$NON-NLS-1$
    private final static String REDIRECT_URL = "https://java.visualstudio.com"; //$NON-NLS-1$

    private final static CredentialsManager gitCredentialsManager = EclipseCredentialsManagerFactory
            .getGitCredentialsManager();
    private final static SecretStore<TokenPair> accessTokenStore = new InsecureInMemoryStore<TokenPair>();
    private final static SecretStore<Token> tokenStore = new EclipseTokenStore();

    public static void refreshCredentialsForGit(final TFSConnection connection) {
        final URI baseURI = connection.getBaseURI();
        final CachedCredentials currentCredentials = new CachedCredentials(baseURI, connection.getCredentials());

        if (currentCredentials.isCookieCredentials() || currentCredentials.isNtlmCredentials()) {
            // The current credentials are not of the UsernamePassword type.
            // We cannot use them for Git.
            return;
        }

        final CachedCredentials cachedCredentials = gitCredentialsManager.getCredentials(baseURI);

        if (cachedCredentials == null) {
            // No credentials are cached for Git.
            // Let's use the current ones. They might be either PAT or
            // Alternative (on hosted)/Basic (on prem.)
            gitCredentialsManager.setCredentials(currentCredentials);
            return;
        }

        if (cachedCredentials.equals(currentCredentials)) {
            // The credentials haven't changed.
            // No need to refresh.
            return;
        }

        if (cachedCredentials.isPatCredentials() == currentCredentials.isPatCredentials()) {
            // The credentials are changed and are of the same type, i.e
            // either both PAT, or both Alternative/Basic. Let's refresh the
            // cached credentials.
            gitCredentialsManager.setCredentials(currentCredentials);
            return;
        }

        log.info("The type of cached credentials does not match to the one of the current credentials."); //$NON-NLS-1$
        log.info("The user has to clean up the cached credentials explicitly."); //$NON-NLS-1$
    }

    public static Credentials getOAuthCredentials(final URI serverURI, final JwtCredentials accessToken,
            final Action<DeviceFlowResponse> callback) {
        removeStaleOAuth2Token();

        final Authenticator authenticator;
        final OAuth2Authenticator oauth2Authenticator = OAuth2Authenticator.getAuthenticator(CLIENT_ID,
                REDIRECT_URL, accessTokenStore, callback);
        final Token token;

        if (serverURI != null) {
            log.debug("Interactively retrieving credential based on oauth2 flow for " + serverURI.toString()); //$NON-NLS-1$
            log.debug("Trying to persist credential, generating a PAT"); //$NON-NLS-1$

            authenticator = new VstsPatAuthenticator(oauth2Authenticator, tokenStore);

            final String tokenKey = authenticator.getUriToKeyConversion().convert(serverURI,
                    authenticator.getAuthType());
            removeStalePersonalAccessToken(tokenKey, serverURI);

            final TokenPair oauth2Token = (accessToken == null) ? null
                    : new TokenPair(accessToken.getAccessToken(), "null"); //$NON-NLS-1$

            token = authenticator.getPersonalAccessToken(serverURI, VsoTokenScope.AllScopes,
                    getAccessTokenDescription(serverURI.toString()), PromptBehavior.AUTO, oauth2Token);
        } else {
            log.debug("Interactively retrieving credential based on oauth2 flow for VSTS"); //$NON-NLS-1$
            log.debug("Do not try to persist, generating oauth2 token."); //$NON-NLS-1$

            authenticator = oauth2Authenticator;

            final TokenPair tokenPair = authenticator.getOAuth2TokenPair();
            token = tokenPair != null ? tokenPair.AccessToken : null;
        }

        if (token != null && token.Type != null && !StringUtil.isNullOrEmpty(token.Value)) {
            switch (token.Type) {
            case Personal:
                return new PatCredentials(token.Value);
            case Access:
                return new JwtCredentials(token.Value);
            }
        }

        log.warn(Messages.getString("CredentialsHelper.InteractiveAuthenticationFailedDetailedLog1")); //$NON-NLS-1$
        log.warn(Messages.getString("CredentialsHelper.InteractiveAuthenticationFailedDetailedLog2")); //$NON-NLS-1$
        log.warn(Messages.getString("CredentialsHelper.InteractiveAuthenticationFailedDetailedLog3")); //$NON-NLS-1$

        removeOAuth2Token(true);

        // Failed to get credential, return null
        return null;
    }

    private static void removeStaleOAuth2Token() {
        removeOAuth2Token(false);
    }

    public static void removeOAuth2Token(final boolean force) {
        final Authenticator authenticator = OAuth2Authenticator.getAuthenticator(CLIENT_ID, REDIRECT_URL,
                accessTokenStore);

        final String tokenKey = authenticator.getUriToKeyConversion().convert(URIUtils.VSTS_ROOT_URL,
                authenticator.getAuthType());
        final TokenPair oauth2TokenPair = accessTokenStore.get(tokenKey);

        if (oauth2TokenPair != null && oauth2TokenPair.AccessToken != null) {
            final String token = oauth2TokenPair.AccessToken.Value;

            if (force || !isOAuth2TokenValid(token)) {
                accessTokenStore.delete(tokenKey);
            }
        }
    }

    private static void removeStalePersonalAccessToken(final String tokenKey, final URI serverURI) {
        final Token token = tokenStore.get(tokenKey);

        if (token != null && !StringUtil.isNullOrEmpty(token.Value)
                && !isAccessTokenValid(token.Value, serverURI)) {
            tokenStore.delete(tokenKey);
        }
    }

    private static boolean isOAuth2TokenValid(final String token) {
        if (StringUtil.isNullOrEmpty(token)) {
            return false;
        } else {
            final URI serverURI = URIUtils.VSTS_ROOT_URL;
            final JwtCredentials credentials = new JwtCredentials(token);

            return isCredentialsValid(serverURI, credentials);
        }
    }

    private static boolean isAccessTokenValid(String token, URI serverURI) {
        final PatCredentials patCredentials = new PatCredentials(token);
        return isCredentialsValid(serverURI, PreemptiveUsernamePasswordCredentials.newFrom(patCredentials));
    }

    private static boolean isCredentialsValid(URI baseURI, Credentials credentials) {

        /*
         * At this point we do not have any connection which HTTPClient we might
         * use to create a TeeClientHandler. Let's create a fake one. We do not
         * use the connection we create here as a real TFSTeamProjectColection.
         * We only use this fake connection object as a source of an HTTPClient
         * configured to use the VSTS credentials provided.
         */
        final TFSTeamProjectCollection rootConnection = new TFSTeamProjectCollection(baseURI, credentials,
                new CommonClientConnectionAdvisor(Locale.getDefault(), TimeZone.getDefault()));
        final AccountHttpClient client = new AccountHttpClient(new TeeClientHandler(rootConnection.getHTTPClient()),
                baseURI);

        try {
            return client.checkConnection();
        } finally {
            /*
             * We didn't use any features of the vstsConnection but the
             * HTTPClient. However to release all resources and the
             * infrastructure created for the connection (e.g.
             * ShoultDownManager, HTTPClientReference, Service Clients, etc.),
             * we still should close this connection when leaving the try-catch
             * block.
             */
            try {
                rootConnection.close();
            } catch (final Exception e) {
                log.error("Absolutelly unexpected error while closing not opened connection", e); //$NON-NLS-1$
            }
        }
    }

    private static String getAccessTokenDescription(final String uri) {
        final String tokenDescription = MessageFormat.format(PatCredentials.TOKEN_DESCRIPTION, uri,
                LocalHost.getShortName());

        return tokenDescription;
    }

    private static class EclipseTokenStore implements SecretStore<Token> {
        /**
         * {@inheritDoc}
         */
        @Override
        public boolean add(String key, Token token) {
            final URI serverURI = URIUtils.newURI(key.split(":", 2)[1]); //$NON-NLS-1$
            return gitCredentialsManager.setCredentials(new CachedCredentials(serverURI, token.Value));
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean delete(String key) {
            final URI serverURI = URIUtils.newURI(key.split(":", 2)[1]); //$NON-NLS-1$
            return gitCredentialsManager.removeCredentials(serverURI);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Token get(String key) {
            final URI serverURI = URIUtils.newURI(key.split(":", 2)[1]); //$NON-NLS-1$
            CachedCredentials cachedCredentials = gitCredentialsManager.getCredentials(serverURI);
            if (cachedCredentials != null) {
                return new Token(cachedCredentials.getPassword(), TokenType.Personal);
            } else {
                return null;
            }

        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean isSecure() {
            return true;
        }
    }
}