to.flows.googledrive.CredentialMediator.java Source code

Java tutorial

Introduction

Here is the source code for to.flows.googledrive.CredentialMediator.java

Source

/*
 * Copyright (c) 2012 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package to.flows.googledrive;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.logging.Logger;

import javax.servlet.http.HttpServletRequest;

import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.auth.oauth2.CredentialStore;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeRequestUrl;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson.JacksonFactory;
import com.google.api.services.oauth2.Oauth2;
import com.google.api.services.oauth2.model.Userinfo;
//import com.google.api.client.extensions.appengine.auth.oauth2.AppEngineCredentialStore;

/**
 * Object that manages credentials associated with this Drive application and
 * its users. Performs all OAuth 2.0 authorization, authorization code
 * upgrades, and token storage/retrieval.
 *
 * @author vicfryzel@google.com (Vic Fryzel)
 */
public class CredentialMediator {
    /**
     * The HTTP request used to make a request to this Drive application.
     * Required so that we can manage a session for the active user, and keep
     * track of their email address which is used to identify their credentials.
     * We also need this in order to access a bunch of request parameters like
     * {@code state} and {@code code}.
     */
    private HttpServletRequest request;

    /**
     * Scopes for which to request authorization.
     */
    private Collection<String> scopes;

    /**
     * Loaded data from war/WEB-INF/client_secrets.json.
     */
    private GoogleClientSecrets secrets;

    /**
     * CredentialStore at which Credential objects are stored.
     */
    private CredentialStore credentialStore;

    /**
     * JsonFactory to use in parsing JSON.
     */
    private static final JsonFactory JSON_FACTORY = new JacksonFactory();

    /**
     * HttpTransport to use for external requests.
     */
    private static final HttpTransport TRANSPORT = new NetHttpTransport();

    /**
     * Key of session variable to store user IDs.
     */
    private static final String USER_ID_KEY = "userId";

    /**
     * Key of session variable to store user email addresses.
     */
    private static final String EMAIL_KEY = "emailAddress";

    static final Logger LOGGER = Logger.getLogger(CredentialMediator.class.getName());

    /**
     * Creates a new CredentialsManager for the given HTTP request.
     *
     * @param request Request in which session credentials are stored.
     * @param clientSecretsStream Stream of client_secrets.json.
     * @throws InvalidClientSecretsException
     */
    public CredentialMediator(HttpServletRequest request, InputStream clientSecretsStream,
            Collection<String> scopes) throws InvalidClientSecretsException {
        this.request = request;
        this.scopes = scopes;
        //this.credentialStore = new AppEngineCredentialStore();
        // this.credentialStore = new MemoryBasedCredentialStore();
        this.credentialStore = JDOPersistenceHelper.createJDOCredentialStore();
        try {
            secrets = GoogleClientSecrets.load(JSON_FACTORY, clientSecretsStream);
        } catch (IOException e) {
            throw new InvalidClientSecretsException("client_secrets.json is missing or invalid.");
        }
    }

    /**
     * @return Client information parsed from client_secrets.json.
     */
    protected GoogleClientSecrets getClientSecrets() {
        return secrets;
    }

    /**
     * Builds an empty GoogleCredential, configured with appropriate
     * HttpTransport, JsonFactory, and client information.
     */
    private Credential buildEmptyCredential() {
        return new GoogleCredential.Builder().setClientSecrets(this.secrets).setTransport(TRANSPORT)
                .setJsonFactory(JSON_FACTORY).build();
    }

    /**
     * Retrieves stored credentials for the provided email address.
     * 
     * @param userId
     *            User's Google ID.
     * @return Stored GoogleCredential if found, {@code null} otherwise.
     * @throws IOException
     */
    private Credential getStoredCredential(String userId) throws IOException {
        Credential credential = buildEmptyCredential();

        SessionBasedCredentialStore sbcs = new SessionBasedCredentialStore(request.getSession());

        if (sbcs.load(userId, credential)) {
            LOGGER.info("Found credential in session");
            return credential;
        }

        if (credentialStore.load(userId, credential)) {
            LOGGER.info("Found credential in database");
            sbcs.store(userId, credential);
            return credential;
        }
        return null;
    }

    /**
     * Deletes stored credentials for the provided email address.
     *
     * @param userId User's Google ID.
     * @throws IOException 
     */
    private void deleteStoredCredential(String userId) throws IOException {
        if (userId != null) {
            Credential credential = getStoredCredential(userId);
            credentialStore.delete(userId, credential);
        }
    }

    /**
     * Exchange an authorization code for a credential.
     *
     * @param authorizationCode Authorization code to exchange for OAuth 2.0
     *        credentials.
     * @return Credential representing the upgraded authorizationCode.
     * @throws CodeExchangeException An error occurred.
     */
    private Credential exchangeCode(String authorizationCode) throws CodeExchangeException {
        // Talk to Google and upgrade the given authorization code to an access
        // token and hopefully a refresh token.
        LOGGER.info("Trying to upgrade authorization code " + authorizationCode
                + " to access token for redirect URI " + secrets.getWeb().getRedirectUris().get(0));
        try {
            GoogleTokenResponse response = new GoogleAuthorizationCodeTokenRequest(TRANSPORT, JSON_FACTORY,
                    secrets.getWeb().getClientId(), secrets.getWeb().getClientSecret(), authorizationCode,
                    secrets.getWeb().getRedirectUris().get(0)).execute();
            return buildEmptyCredential().setFromTokenResponse(response);
        } catch (IOException e) {
            e.printStackTrace();
            throw new CodeExchangeException();
        }
    }

    /**
     * Send a request to the UserInfo API to retrieve user e-mail address
     * associated with the given credential.
     *
     * @param credential Credential to authorize the request.
     * @return User's e-mail address.
     * @throws NoUserIdException An error occurred, and the retrieved email
     *                                 address was null.
     */
    private Userinfo getUserInfo(Credential credential) throws NoUserIdException {
        Userinfo userInfo = null;

        // Create a user info service, and make a request to get the user's info.
        Oauth2 userInfoService = new Oauth2.Builder(TRANSPORT, JSON_FACTORY, credential).build();
        try {
            userInfo = userInfoService.userinfo().get().execute();
            if (userInfo == null) {
                throw new NoUserIdException();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return userInfo;
    }

    /**
     * Retrieve the authorization URL to authorize the user with the given
     * email address.
     *
     * @param emailAddress User's e-mail address.
     * @return Authorization URL to redirect the user to.
     */
    private String getAuthorizationUrl(String emailAddress) {
        // Generate an authorization URL based on our client settings,
        // the user's email address, and the state parameter, if present.
        GoogleAuthorizationCodeRequestUrl urlBuilder = new GoogleAuthorizationCodeRequestUrl(
                secrets.getWeb().getClientId(), secrets.getWeb().getRedirectUris().get(0), scopes)
                        .setAccessType("offline").setApprovalPrompt("force");
        // Propagate through the current state parameter, so that when the
        // user gets redirected back to our app, they see the file(s) they
        // were originally supposed to see before we realized we weren't
        // authorized.
        if (request.getParameter("state") != null) {
            urlBuilder.set("state", request.getParameter("state"));
        }
        if (emailAddress != null) {
            urlBuilder.set("user_id", emailAddress);
        }
        return urlBuilder.build();
    }

    /**
     * Deletes the credential of the active session.
     * @throws IOException 
     */
    public void deleteActiveCredential() throws IOException {
        String userId = (String) request.getSession().getAttribute(USER_ID_KEY);
        this.deleteStoredCredential(userId);
    }

    /**
     * Retrieve credentials using the provided authorization code.
     *
     * This function exchanges the authorization code for an access token and
     * queries the UserInfo API to retrieve the user's e-mail address. If a
     * refresh token has been retrieved along with an access token, it is stored
     * in the application database using the user's e-mail address as key. If no
     * refresh token has been retrieved, the function checks in the application
     * database for one and returns it if found or throws a
     * NoRefreshTokenException with the authorization URL to redirect the user
     * to.
     *
     * @return Credential containing an access and refresh token.
     * @throws NoRefreshTokenException No refresh token could be retrieved from
     *         the available sources.
     * @throws IOException 
     */
    public Credential getActiveCredential() throws NoRefreshTokenException, IOException {
        String userId = (String) request.getSession().getAttribute(USER_ID_KEY);
        LOGGER.info("Checking getActiveCredential for " + userId);
        Credential credential = null;
        try {
            // Only bother looking for a Credential if the user has an existing
            // session with their email address stored.
            if (userId != null) {
                LOGGER.info("Trying to find stored credential for " + userId);
                credential = getStoredCredential(userId);
                LOGGER.info("getStoredCredential = " + credential);
            }

            // No Credential was stored for the current user or no refresh token is
            // available.
            // If an authorizationCode is present, upgrade it into an
            // access token and hopefully a refresh token.
            if ((credential == null || credential.getRefreshToken() == null)
                    && request.getParameter("code") != null) {
                credential = exchangeCode(request.getParameter("code"));
                if (credential != null) {
                    Userinfo userInfo = getUserInfo(credential);
                    userId = userInfo.getId();
                    LOGGER.info("Setting user ID in sesson " + userId + " " + userInfo.getEmail());
                    request.getSession().setAttribute(USER_ID_KEY, userId);
                    request.getSession().setAttribute(EMAIL_KEY, userInfo.getEmail());
                    // Sometimes we won't get a refresh token after upgrading a code.
                    // This won't work for our app, because the user can land directly
                    // at our app without first visiting Google Drive. Therefore,
                    // only bother to store the Credential if it has a refresh token.
                    // If it doesn't, we'll get one below.                             
                    if (credential.getRefreshToken() != null) {
                        LOGGER.info("Got a refresh token - storing it.");
                        credentialStore.store(userId, credential);
                    } else {
                        // ME: Since we now have userInfo let's try to retrieve the Credentials with the refreshToken again
                        credential = getStoredCredential(userId);
                    }
                }
            }

            if (credential == null || credential.getRefreshToken() == null) {
                // No refresh token has been retrieved.
                // Start a "fresh" OAuth 2.0 flow so that we can get a refresh token.
                String email = (String) request.getSession().getAttribute(EMAIL_KEY);
                String authorizationUrl = getAuthorizationUrl(email);
                throw new NoRefreshTokenException(authorizationUrl);
            }
        } catch (CodeExchangeException e) {
            // The code the user arrived here with was bad.  This pretty much never
            // happens. In a production application, we'd either redirect the user
            // somewhere like a home page, or show them a vague error mentioning
            // that they probably didn't arrive to our app from Google Drive.       
            e.printStackTrace();
        } catch (NoUserIdException e) {
            // This is bad because it means the user either denied us access
            // to their email address, or we couldn't fetch it for some reason.
            // This is unrecoverable. In a production application, we'd show the
            // user a message saying that we need access to their email address
            // to work.
            e.printStackTrace();
        }
        return credential;
    }

    /**
     * Exception thrown when no refresh token has been found.
     */
    @SuppressWarnings("serial")
    public static class NoRefreshTokenException extends Exception {

        /**
         * Authorization URL to which to redirect the user.
         */
        private String authorizationUrl;

        /**
         * Construct a NoRefreshTokenException.
         *
         * @param authorizationUrl The authorization URL to redirect the user to.
         */
        public NoRefreshTokenException(String authorizationUrl) {
            this.authorizationUrl = authorizationUrl;
        }

        /**
         * @return Authorization URL to which to redirect the user.
         */
        public String getAuthorizationUrl() {
            return authorizationUrl;
        }
    }

    /**
     * Exception thrown when client_secrets.json is missing or invalid.
     */
    @SuppressWarnings("serial")
    public static class InvalidClientSecretsException extends Exception {
        /**
         * Construct an InvalidClientSecretsException with a message.
         *
         * @param message Message to escalate.
         */
        public InvalidClientSecretsException(String message) {
            super(message);
        }
    }

    /**
     * Exception thrown when no email address could be retrieved.
     */
    @SuppressWarnings("serial")
    private static class NoUserIdException extends Exception {
    }

    /**
     * Exception thrown when a code exchange has failed.
     */
    @SuppressWarnings("serial")
    private static class CodeExchangeException extends Exception {
    }
}