403 lines
15 KiB
Java
Executable File
403 lines
15 KiB
Java
Executable File
package com.urrsm.sng.udiary;
|
|
|
|
import com.google.api.client.auth.oauth2.Credential;
|
|
import com.google.api.client.auth.oauth2.StoredCredential;
|
|
import com.google.api.client.extensions.appengine.datastore.AppEngineDataStoreFactory;
|
|
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.jackson2.JacksonFactory;
|
|
import com.google.api.client.util.store.DataStore;
|
|
import com.google.api.client.util.store.DataStoreFactory;
|
|
import com.google.api.services.oauth2.Oauth2;
|
|
import com.google.api.services.oauth2.model.Userinfoplus;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.InputStreamReader;
|
|
import java.util.Collection;
|
|
import javax.servlet.http.HttpServletRequest;
|
|
|
|
/**
|
|
* 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;
|
|
|
|
/**
|
|
* DataStore of StoredCredentials.
|
|
*/
|
|
private DataStore<StoredCredential> credentialsStore;
|
|
|
|
/**
|
|
* 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";
|
|
|
|
/**
|
|
* 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, IOException
|
|
{
|
|
this.request = request;
|
|
this.scopes = scopes;
|
|
DataStoreFactory dataStoreFactory = new AppEngineDataStoreFactory();
|
|
credentialsStore = StoredCredential.getDefaultDataStore(dataStoreFactory);
|
|
try
|
|
{
|
|
secrets = GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(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.
|
|
*/
|
|
private StoredCredential getStoredCredential(String userId)throws IOException
|
|
{
|
|
if(credentialsStore.containsKey(userId))
|
|
{
|
|
return credentialsStore.get(userId);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Deletes stored credentials for the provided email address.
|
|
*
|
|
* @param userId User's Google ID.
|
|
*/
|
|
private void deleteStoredCredential(String userId)throws IOException
|
|
{
|
|
if(userId != null)
|
|
{
|
|
credentialsStore.delete(userId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
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 Userinfoplus getUserInfo(Credential credential)throws NoUserIdException
|
|
{
|
|
Userinfoplus 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.
|
|
*/
|
|
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.
|
|
*/
|
|
public Credential getActiveCredential()throws NoRefreshTokenException,IOException
|
|
{
|
|
String userId = (String) request.getSession().getAttribute(USER_ID_KEY);
|
|
System.out.println("Session="+request.getSession().getId()+" : isCookie="+request.isRequestedSessionIdFromCookie()+" : isURL="+request.isRequestedSessionIdFromURL()+" :isNew="+request.getSession().isNew());
|
|
System.out.println("UserID="+userId);
|
|
StoredCredential strdCredentials = null;
|
|
Credential credentials = null;
|
|
try
|
|
{
|
|
// Only bother looking for a Credential if the user has an existing
|
|
// session with their email address stored.
|
|
if(userId != null)
|
|
{
|
|
strdCredentials = this.getStoredCredential(userId);
|
|
credentials = buildEmptyCredential().setAccessToken(strdCredentials.getAccessToken());
|
|
if(strdCredentials.getRefreshToken() != null)
|
|
credentials = credentials.setRefreshToken(strdCredentials.getRefreshToken());
|
|
credentials = credentials.setExpirationTimeMilliseconds(strdCredentials.getExpirationTimeMilliseconds());
|
|
System.out.println("Storedcrdentials="+((credentials!=null)?credentials.getAccessToken():credentials));
|
|
}
|
|
// 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((strdCredentials == null || strdCredentials.getRefreshToken() == null) && (request.getParameter("code") != null))
|
|
{
|
|
credentials = exchangeCode(request.getParameter("code"));
|
|
System.out.println("crdentials="+((credentials!=null)?credentials.getAccessToken():credentials));
|
|
strdCredentials = new StoredCredential(credentials);
|
|
if(credentials != null)
|
|
{
|
|
Userinfoplus userInfo = getUserInfo(credentials);
|
|
userId = userInfo.getId();
|
|
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(strdCredentials.getRefreshToken() != null)
|
|
{
|
|
credentialsStore.set(userId, strdCredentials);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(strdCredentials == null || strdCredentials.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 credentials;
|
|
}
|
|
|
|
/**
|
|
* Exception thrown when no refresh token has been found.
|
|
*/
|
|
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.
|
|
*/
|
|
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.
|
|
*/
|
|
private static class NoUserIdException extends Exception{}
|
|
|
|
/**
|
|
* Exception thrown when a code exchange has failed.
|
|
*/
|
|
private static class CodeExchangeException extends Exception {}
|
|
}
|