UDiary/src/main/java/com/urrsm/sng/udiary/CredentialMediator.java

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 {}
}