/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 com.google.wave.api.oauth.impl;
import com.google.wave.api.Wavelet;
import com.google.wave.api.oauth.LoginFormHandler;
import com.google.wave.api.oauth.OAuthService;
import net.oauth.OAuth;
import net.oauth.OAuthAccessor;
import net.oauth.OAuthConsumer;
import net.oauth.OAuthException;
import net.oauth.OAuthMessage;
import net.oauth.OAuthServiceProvider;
import net.oauth.client.OAuthClient;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.logging.Logger;
import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;
/**
* Implements the OAuthService interface. {@link OAuthService}
*
* @author kimwhite@google.com (Kimberly White)
* @author elizabethford@google.com (Elizabeth Ford)
*/
public class OAuthServiceImpl implements OAuthService {
/** Writes application logs. */
private static final Logger LOG = Logger.getLogger(OAuthServiceImpl.class.getName());
/** Key for OAuth request token query parameter. */
private static final String TOKEN_KEY = "oauth_token";
/** Key for OAuth callback url query parameter. */
private static final String CALLBACK_KEY = "oauth_callback";
/** Key to force user to enter credentials. */
private static final String FORCE_LOGIN_KEY = "force_login";
/** Key for HTTP GET method. */
private static final String GET = "GET";
/** Key for HTTP POST method. */
private static final String POST = "POST";
/** Key for Datastore object. Consists of wave creator id and wave id. */
private final String userRecordKey;
/** OpenAuth client used to negotiate request/access tokens. */
private final OAuthClient oauthClient;
/** OpenAuth accessor that stores request/access/secret tokens. */
private final OAuthAccessor accessor;
/** Persistence Manager Factory to retrieve and store Datastore objects. */
private final PersistenceManagerFactory pmf;
/**
* Factory method. Initializes OAuthServiceProvider with necessary tokens and
* urls.
*
* @param userRecordKey key consisting of user id and wave id.
* @param consumerKey service provider OAuth consumer key.
* @param consumerSecret service provider OAuth consumer secret.
* @param requestTokenUrl url to get service provider request token.
* @param authorizeUrl url to service provider authorize page.
* @param callbackUrl url to callback page.
* @param accessTokenUrl url to get service provider access token.
* @return OAuthService instance.
*/
public static OAuthService newInstance(String userRecordKey, String consumerKey,
String consumerSecret, String requestTokenUrl, String authorizeUrl, String callbackUrl,
String accessTokenUrl) {
OAuthServiceProvider provider =
new OAuthServiceProvider(requestTokenUrl, authorizeUrl, accessTokenUrl);
OAuthConsumer consumer = new OAuthConsumer(callbackUrl, consumerKey, consumerSecret, provider);
OAuthAccessor accessor = new OAuthAccessor(consumer);
OAuthClient client = new OAuthClient(new OpenSocialHttpClient());
PersistenceManagerFactory pmf = SingletonPersistenceManagerFactory.get();
return new OAuthServiceImpl(accessor, client, pmf, userRecordKey);
}
/**
* Initializes necessary OAuthClient and accessor objects for OAuth handling.
*
* @param accessor Used to store tokesn for OAuth authorization.
* @param client Handles OAuth authorization.
* @param pmf Manages datastore fetching and storing.
* @param recordKey User id for datastore object.
*/
OAuthServiceImpl(OAuthAccessor accessor, OAuthClient client,
PersistenceManagerFactory pmf, String recordKey) {
this.userRecordKey = recordKey;
this.pmf = pmf;
this.accessor = accessor;
this.oauthClient = client;
}
@Override
public boolean checkAuthorization(Wavelet wavelet, LoginFormHandler loginForm) {
OAuthUser user = retrieveUserProfile();
// Return true if the user already has an access token.
if (user != null && user.getAccessToken() != null) {
return true;
}
// If the user doesn't have an access token but already has a request token,
// exchange the tokens.
if (user != null && user.getRequestToken() != null) {
String accessToken = exchangeTokens(user);
if (accessToken != null) {
// Yay, we're authorized.
return true;
}
}
// Need to login.
String requestToken = getAndStoreRequestToken();
LOG.info("Request token: " + requestToken);
buildAuthUrl();
loginForm.renderLogin(userRecordKey, wavelet);
return false;
}
@Override
public boolean hasAuthorization() {
OAuthUser user = retrieveUserProfile();
return (user != null && user.getAccessToken() != null);
}
/**
* Applies for a request token from the service provider.
*
* @return the request token.
*/
private String getAndStoreRequestToken() {
// Get the request token.
try {
oauthClient.getRequestToken(accessor);
} catch (IOException e) {
LOG.severe("Could not reach service provider to get request token: " + e);
} catch (OAuthException e) {
LOG.severe("Unable to fetch request token. Authentication error: " + e);
} catch (URISyntaxException e) {
LOG.severe("Unable to fetch request token. Invalid url: " + e);
}
// Store request token in Datastore via a tokenData object.
String requestToken = accessor.requestToken;
storeUserProfile(new OAuthUser(userRecordKey, requestToken));
return accessor.requestToken;
}
/**
* Builds the url to authenticate the request token with necessary query
* parameters and stores in Datastore.
*/
private void buildAuthUrl() {
OAuthUser userProfile = retrieveUserProfile();
OpenSocialUrl url = new OpenSocialUrl(accessor.consumer.serviceProvider.userAuthorizationURL);
url.addQueryStringParameter(TOKEN_KEY, userProfile.getRequestToken());
url.addQueryStringParameter(CALLBACK_KEY, accessor.consumer.callbackURL);
url.addQueryStringParameter(FORCE_LOGIN_KEY, "true");
userProfile.setAuthUrl(url.toString());
storeUserProfile(userProfile);
}
/**
* Exchanges a signed request token for an access token from the service
* provider and stores it in the user profile if access token is successfully
* acquired.
*
* @return String of the access token, might return null if failed.
*/
private String exchangeTokens(OAuthUser userProfile) {
// Re-initialize an accessor with the request token and secret token.
// Request token needs to be in access token field for exchange to work.
accessor.accessToken = userProfile.getRequestToken();
accessor.tokenSecret = accessor.consumer.consumerSecret;
// Perform the exchange.
String accessToken = null;
String tokenSecret = null;
try {
OAuthMessage message =
oauthClient.invoke(accessor, GET, accessor.consumer.serviceProvider.accessTokenURL, null);
accessToken = message.getToken();
tokenSecret = message.getParameter(OAuth.OAUTH_TOKEN_SECRET);
} catch (IOException e) {
LOG.warning("Failed to retrieve access token: " + e.getMessage());
} catch (OAuthException e) {
LOG.warning("Failed to retrieve access token: " + e.getMessage());
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
if (accessToken != null) {
// Store the access token in the user profile object and put profile back
// in the datastore.
userProfile.setAccessToken(accessToken);
userProfile.setTokenSecret(tokenSecret);
storeUserProfile(userProfile);
}
return accessToken;
}
@Override
public String post(String url, Map<String, String> parameters) throws OAuthServiceException {
return requestResources(url, POST, parameters);
}
@Override
public String get(String url, Map<String, String> parameters) throws OAuthServiceException {
return requestResources(url, GET, parameters);
}
/**
* Get secure resources from the service provider.
*
* @param url service provider resource url.
* @param method Http method.
* @param parameters service provider parameters.
* @return String of the service provider response message.
* @throws OAuthServiceException the HTTP response code was not OK
*/
private String requestResources(String url, String method, Map<String, String> parameters)
throws OAuthServiceException {
// Convert parameters to OAuth parameters.
Collection<OAuth.Parameter> queryParameters = new ArrayList<OAuth.Parameter>();
for (Map.Entry<String, String> parameter : parameters.entrySet()) {
queryParameters.add(new OAuth.Parameter(parameter.getKey(), parameter.getValue()));
}
// Set the accessor's access token with the access token in Datastore.
OAuthUser userProfile = retrieveUserProfile();
accessor.accessToken = userProfile.getAccessToken();
accessor.tokenSecret = userProfile.getTokenSecret();
// Send request and receive response from service provider.
String messageString = "";
OAuthMessage message;
try {
message = oauthClient.invoke(accessor, method, url, queryParameters);
messageString = message.readBodyAsString();
} catch (IOException e) {
LOG.severe("Response message has no body: " + e);
throw new OAuthServiceException(e);
} catch (URISyntaxException e) {
LOG.severe("Unable to fetch resources. Invalid url: " + e);
throw new OAuthServiceException(e);
} catch (OAuthException e){
throw new OAuthServiceException(e);
}
return messageString;
}
/**
* Stores user-specific oauth token information in Datastore.
*
* @param user profile consisting of user's request token, access token, and
* consumer secret.
*/
private void storeUserProfile(OAuthUser user) {
PersistenceManager pm = pmf.getPersistenceManager();
try {
pm.makePersistent(user);
} finally {
pm.close();
}
}
/**
* Retrieves user's oauth information from Datastore.
*
* @return the user profile (or null if not found).
*/
private OAuthUser retrieveUserProfile() {
PersistenceManager pm = pmf.getPersistenceManager();
OAuthUser userProfile = null;
try {
userProfile = pm.getObjectById(OAuthUser.class, userRecordKey);
} catch (JDOObjectNotFoundException e) {
LOG.info("Datastore object not yet initialized with key: " + userRecordKey);
} finally {
pm.close();
}
return userProfile;
}
}