/* Copyright (c) 2008 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 com.google.gdata.client.authn.oauth; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Helper methods to support the entire OAuth lifecycle, including generating * the user authorization url, exchanging the user authenticated request token * for an access token, and generating the Authorization http header. * * @see <a href="http://oauth.net/core/1.0/">OAuth Core 1.0</a> * * */ public class OAuthHelper { private String requestTokenUrl; private String userAuthorizationUrl; private String accessTokenUrl; private String revokeTokenUrl; private OAuthHttpClient httpClient; private OAuthSigner signer; /** * An abstract helper class for generating a string of key/value pairs that * are separated by string delimiters. For example, suppose there is a set of * key value pairs: key1/value1, key2/value2, etc. Using the * {@link QueryKeyValuePair} class, the resulting string would look like: * key1=value1&key2=value2. There is no trailing ampersand at the end and * each key and value will be encoded according to the OAuth spec * (<a href="http://oauth.net/core/1.0/#encoding_parameters">Section 5.1</a>). * * */ static abstract class KeyValuePair { private List<String> keys; private List<String> values; private String keyValueStartDelimiter; private String keyValueEndDelimiter; private String pairDelimiter; /** * Create a new instance. The string delimiters specified as inputs are * used by the {@link #toString} method to generating the string. * * @param keyValueStartDelimiter the delimiter placed between the key and * the value * @param keyValueEndDelimiter the delimiter placed after the value * @param pairDelimiter the delimiter placed in between each key/value pair */ protected KeyValuePair(String keyValueStartDelimiter, String keyValueEndDelimiter, String pairDelimiter) { this.keyValueStartDelimiter = keyValueStartDelimiter; this.keyValueEndDelimiter = keyValueEndDelimiter; this.pairDelimiter = pairDelimiter; keys = new ArrayList<String>(); values = new ArrayList<String>(); } /** * Add a key/value pair * * @param key the key of the pair * @param value the value of the pair */ public void add(String key, String value) { keys.add(key); values.add(value); } /** * Get the key at the input position. * * @param i the position to retrieve the key * @return the key at the input position */ public String getKey(int i) { return keys.get(i); } /** * Get the value at the input position. * * @param i the position to retrieve the value * @return the value at the input position */ public String getValue(int i) { return values.get(i); } /** * Get the number of key/value pairs. * * @return the number of key/value pairs. */ public int size() { return keys.size(); } /** * Concatenates the key/value pairs into a string. For example, suppose * there is a set of key value pairs: key1/value1, key2/value2, etc. Using * the {@link QueryKeyValuePair} class, the resulting string would look * like: key1=value1&key2=value2. There is no trailing ampersand at the end * and each key and value will be encoded according to the OAuth spec * (<a href="http://oauth.net/core/1.0/#encoding_parameters">Section * 5.1</a>). */ @Override public String toString() { StringBuilder keyValueString = new StringBuilder(); for (int i = 0, length = size(); i < length; i++) { if (i > 0) { keyValueString.append(pairDelimiter); } keyValueString.append(OAuthUtil.encode(getKey(i))) .append(keyValueStartDelimiter) .append(OAuthUtil.encode(getValue(i))).append(keyValueEndDelimiter); } return keyValueString.toString(); } } /** * Generates a key/value string appropriate for a url's query string. For * example: key1=value1&key2=value2&key3=value3. * * */ private static class QueryKeyValuePair extends KeyValuePair { public QueryKeyValuePair() { super("=", "", "&"); } } /** * Generates a key/value string appropriate for an Authorization header. * For example: key1="value1", key2="value2", key3="value3". * * */ static class HeaderKeyValuePair extends KeyValuePair { public HeaderKeyValuePair() { super("=\"", "\"", ", "); } } /** * Create a new {@link OAuthHelper} object. * * @param requestTokenUrl the url used to obtain an unauthorized request token * @param userAuthorizationUrl the url used to obtain user authorization for * consumer access * @param accessTokenUrl the url used to exchange the user-authorized request * token for an access token * @param signer the {@link OAuthSigner} to use when signing the request */ @Deprecated public OAuthHelper(String requestTokenUrl, String userAuthorizationUrl, String accessTokenUrl, OAuthSigner signer) { this(requestTokenUrl, userAuthorizationUrl, accessTokenUrl, signer, new OAuthHttpClient()); } /** * Create a new {@link OAuthHelper} object. This version of the constructor * is primarily for testing purposes, where a mocked {@link OAuthHttpClient} * and {@link OAuthSigner} can be specified. * * @param requestTokenUrl the url used to obtain an unauthorized request token * @param userAuthorizationUrl the url used to obtain user authorization for * consumer access * @param accessTokenUrl the url used to exchange the user-authorized request * token for an access token * @param signer the {@link OAuthSigner} to use when signing the request * @param httpClient the {@link OAuthHttpClient} to use when making http * requests */ @Deprecated public OAuthHelper(String requestTokenUrl, String userAuthorizationUrl, String accessTokenUrl, OAuthSigner signer, OAuthHttpClient httpClient) { this.requestTokenUrl = requestTokenUrl; this.userAuthorizationUrl = userAuthorizationUrl; this.accessTokenUrl = accessTokenUrl; this.signer = signer; this.httpClient = httpClient; } /** * Create a new {@link OAuthHelper} object. * * @param requestTokenUrl the url used to obtain an unauthorized request token * @param userAuthorizationUrl the url used to obtain user authorization for * consumer access * @param accessTokenUrl the url used to exchange the user-authorized request * token for an access token * @param revokeTokenUrl the url used to revoke the OAuth token * @param signer the {@link OAuthSigner} to use when signing the request */ public OAuthHelper(String requestTokenUrl, String userAuthorizationUrl, String accessTokenUrl, String revokeTokenUrl, OAuthSigner signer) { this(requestTokenUrl, userAuthorizationUrl, accessTokenUrl, revokeTokenUrl, signer, new OAuthHttpClient()); } /** * Create a new {@link OAuthHelper} object. This version of the constructor * is primarily for testing purposes, where a mocked {@link OAuthHttpClient} * and {@link OAuthSigner} can be specified. * * @param requestTokenUrl the url used to obtain an unauthorized request token * @param userAuthorizationUrl the url used to obtain user authorization for * consumer access * @param accessTokenUrl the url used to exchange the user-authorized request * token for an access token * @param revokeTokenUrl the url used to revoke the OAuth token * @param signer the {@link OAuthSigner} to use when signing the request * @param httpClient the {@link OAuthHttpClient} to use when making http * requests */ public OAuthHelper(String requestTokenUrl, String userAuthorizationUrl, String accessTokenUrl, String revokeTokenUrl, OAuthSigner signer, OAuthHttpClient httpClient) { this.requestTokenUrl = requestTokenUrl; this.userAuthorizationUrl = userAuthorizationUrl; this.accessTokenUrl = accessTokenUrl; this.revokeTokenUrl = revokeTokenUrl; this.signer = signer; this.httpClient = httpClient; } /** Get the access token url */ public String getAccessTokenUrl() { return accessTokenUrl; } /** Set the access token url */ public void setAccessTokenUrl(String url) { accessTokenUrl = url; } /** Get the request token url */ public String getRequestTokenUrl() { return requestTokenUrl; } /** Set the request token url */ public void setRequestTokenUrl(String url) { requestTokenUrl = url; } /** Get the user authorization url */ public String getUserAuthorizationUrl() { return userAuthorizationUrl; } /** Set the user authorization url */ public void setUserAuthorizationUrl(String url) { userAuthorizationUrl = url; } /** Get the revoke token url */ public String getRevokeTokenUrl() { return revokeTokenUrl; } /** Set the revoke token url */ public void setRevokeTokenUrl(String url) { revokeTokenUrl = url; } /** * Retrieves the unauthorized request token and token secret from the remote * server and sets the parameters in the {@link OAuthParameters} object. * <p> * The following parameter is required in {@link OAuthParameters}: * <ul><li>consumer_key * </ul> * <p> * If the request is successful, the following parameters will be set in * {@link OAuthParameters}: * <ul> * <li>oauth_token * <li>oauth_token_secret (if signing with HMAC) * </ul> * <p> * @see <a href="http://oauth.net/core/1.0/#auth_step1">OAuth Step 1</a> * * @param oauthParameters the OAuth parameters necessary for this request. * @throws OAuthException if there is an error with the OAuth request */ public void getUnauthorizedRequestToken(OAuthParameters oauthParameters) throws OAuthException { TwoLeggedOAuthHelper helper = new TwoLeggedOAuthHelper(signer, oauthParameters); helper.validateInputParameters(); // If the callback is present in this step, assume the user is using // OAuth v1.0a, and include the url in the base parameters. boolean oauthCallbackExists = false; if (oauthParameters.checkOAuthCallbackExists()) { String callback = oauthParameters.getOAuthCallback(); oauthParameters.addCustomBaseParameter(OAuthParameters.OAUTH_CALLBACK_KEY, callback); oauthCallbackExists = true; } // Generate a signed URL that allows the consumer to retrieve the // unauthorized request token. URL url = getOAuthUrl(requestTokenUrl, "GET", oauthParameters); // Retrieve the unauthorized request token and store it in the // oauthParameters String response = httpClient.getResponse(url); Map<String, String> queryString = OAuthUtil.parseQuerystring(response); oauthParameters.setOAuthToken( queryString.get(OAuthParameters.OAUTH_TOKEN_KEY)); oauthParameters.setOAuthTokenSecret( queryString.get(OAuthParameters.OAUTH_TOKEN_SECRET_KEY)); if (oauthCallbackExists) { // OAuth callback can be completely removed from parameters here, // but leave it in for now in order to be compatible with both the // old and new OAuth protocol. oauthParameters.removeCustomBaseParameter( OAuthParameters.OAUTH_CALLBACK_KEY); } // clear the request-specific parameters set in getOAuthUrl(), such as // nonce, timestamp and signature, which are only needed for a single // request. oauthParameters.reset(); } /** * Generates the url which the user should visit in order to authenticate and * authorize with the Service Provider. The url will look something like this: * https://www.google.com/accounts/OAuthAuthorizeToken?oauth_token=[OAUTHTOKENSTRING]&oauth_callback=http%3A%2F%2Fwww.google.com%2F * This method first calls * {@link #getUnauthorizedRequestToken(OAuthParameters)} to retrieve the * unauthorized request token, and then calls * {@link #createUserAuthorizationUrl(OAuthParameters)}. Users who wish to * add a token secret to the callback url should call * {@link #getUnauthorizedRequestToken(OAuthParameters)} first, append the * retrieved token secret to the callback url, and then call * {@link #createUserAuthorizationUrl(OAuthParameters)}. * <p> * The following parameter is required in {@link OAuthParameters}: * <ul> * <li>oauth_consumer_key * </ul> * <p> * The following parameter is optional: * <ul> * <li>oauth_callback * </ul> * <p> * @see <a href="http://oauth.net/core/1.0/#auth_step1">OAuth Step 1</a> * * @param oauthParameters the OAuth parameters necessary for this request * @return The full authorization url the user should visit. The method also * modifies the oauthParameters object by adding the request token and * token secret. * @throws OAuthException if there is an error with the OAuth request * @deprecated Call a combination of {@link #getUnauthorizedRequestToken} and * {@link #createUserAuthorizationUrl} instead. */ @Deprecated public String getUserAuthorizationUrl(OAuthParameters oauthParameters) throws OAuthException { getUnauthorizedRequestToken(oauthParameters); return createUserAuthorizationUrl(oauthParameters); } /** * Generates the url which the user should visit in order to authenticate and * authorize with the Service Provider. This method does not modify the * {@link OAuthParameters} object. The url will look something like this: * https://www.google.com/accounts/OAuthAuthorizeToken?oauth_token=[OAUTHTOKENSTRING]&oauth_callback=http%3A%2F%2Fwww.google.com%2F * <p> * The following parameter is required in {@link OAuthParameters}: * <ul> * <li>oauth_token * </ul> * <p> * The following parameter is optional: * <ul> * <li>oauth_callback * </ul> * * @param oauthParameters the OAuth parameters necessary for this request * @return The full authorization url the user should visit. The method also * modifies the oauthParameters object by adding the request token and * token secret. */ public String createUserAuthorizationUrl(OAuthParameters oauthParameters) { // Format and return the user authorization url. KeyValuePair queryParams = new QueryKeyValuePair(); queryParams.add(OAuthParameters.OAUTH_TOKEN_KEY, oauthParameters.getOAuthToken()); if (oauthParameters.getOAuthCallback().length() > 0) { queryParams.add(OAuthParameters.OAUTH_CALLBACK_KEY, oauthParameters.getOAuthCallback()); } return (new StringBuilder()).append(userAuthorizationUrl).append("?") .append(queryParams.toString()).toString(); } /** * Helper method which parses a url for the OAuth related parameters. * It expects an OAuth token parameter to exist, while the OAuth token secret * may or may not exist, depending on the implementation. The parameters are * set in the {@link OAuthParameters} object. * * @param url The url containing the OAuth parameters. * @param oauthParameters OAuth parameters for this request */ public void getOAuthParametersFromCallback(URL url, OAuthParameters oauthParameters) { getOAuthParametersFromCallback(url.getQuery(), oauthParameters); } /** * Helper method which parses a querystring for the OAuth related parameters. * It expects an OAuth token parameter to exist, while the OAuth token secret * may or may not exist, depending on the implementation. The parameters are * set in the {@link OAuthParameters} object. * * @param queryString the query string containing the OAuth parameters * @param oauthParameters OAuth parameters for this request */ public void getOAuthParametersFromCallback(String queryString, OAuthParameters oauthParameters) { // parse the querystring, and store the parsed values in oauthParameters. Map<String, String> params = OAuthUtil.parseQuerystring(queryString); oauthParameters.setOAuthToken(params.get(OAuthParameters.OAUTH_TOKEN_KEY)); if (params.get(OAuthParameters.OAUTH_TOKEN_SECRET_KEY) != null) { oauthParameters.setOAuthTokenSecret( params.get(OAuthParameters.OAUTH_TOKEN_SECRET_KEY)); } if (params.get(OAuthParameters.OAUTH_VERIFIER_KEY) != null) { oauthParameters.setOAuthVerifier( params.get(OAuthParameters.OAUTH_VERIFIER_KEY)); } } /** * Exchanges the user-authorized request token for an access token. This * method parses the user-authorized request token from the authorization * response url, and passes it on to * {@link #getAccessToken(String, OAuthParameters)}. * <p> * The following parameters are required in {@link OAuthParameters}: * <ul> * <li>oauth_consumer_key * </ul> * @see <a href="http://oauth.net/core/1.0/#auth_step3">OAuth Step 3</a> * * @param url the url to parse the request token from * @param oauthParameters OAuth parameters for this request * @return the access token * @throws OAuthException if there is an error with the OAuth request */ public String getAccessToken(URL url, OAuthParameters oauthParameters) throws OAuthException { return getAccessToken(url.getQuery(), oauthParameters); } /** * Exchanges the user-authorized request token for an access token. This * method parses the user-authorized request token from the authorization * response's query string, and passes it on to * {@link #getAccessToken(OAuthParameters)} (The query string is everything * in the authorization response URL after the question mark). * <p> * The following parameters are required in {@link OAuthParameters}: * <ul> * <li>oauth_consumer_key * </ul> * @see <a href="http://oauth.net/core/1.0/#auth_step3">OAuth Step 3</a> * * @param queryString the query string containing the request token * @param oauthParameters OAuth parameters for this request * @return the access token * @throws OAuthException if there is an error with the OAuth request */ public String getAccessToken(String queryString, OAuthParameters oauthParameters) throws OAuthException { getOAuthParametersFromCallback(queryString, oauthParameters); return getAccessToken(oauthParameters); } /** * Exchanges the user-authorized request token for an access token. * Typically, this method is called immediately after you extract the * user-authorized request token from the authorization response, but it can * also be triggered by a user action indicating they've successfully * completed authorization with the service provider. * <p> * The following parameters are required in {@link OAuthParameters}: * <ul> * <li>oauth_consumer_key * <li>oauth_token * <li>oauth_token_secret (if signing with HMAC) * </ul> * <p> * If the request is successful, the following parameters will be set in * {@link OAuthParameters}: * <ul> * <li>oauth_token * <li>oauth_token_secret (if signing with HMAC) * </ul> * <p> * @see <a href="http://oauth.net/core/1.0/#auth_step3">OAuth Step 3</a> * * @param oauthParameters OAuth parameters for this request * @return The access token. This method also replaces the request token * with the access token in the oauthParameters object. * @throws OAuthException if there is an error with the OAuth request */ public String getAccessToken(OAuthParameters oauthParameters) throws OAuthException { // // STEP 1: Validate the input parameters TwoLeggedOAuthHelper helper = new TwoLeggedOAuthHelper(signer, oauthParameters); helper.validateInputParameters(); oauthParameters.assertOAuthTokenExists(); if (signer instanceof OAuthHmacSha1Signer) { oauthParameters.assertOAuthTokenSecretExists(); } // STEP 2: Generate the OAuth request url based on the input parameters. URL url = getOAuthUrl(accessTokenUrl, "GET", oauthParameters); // STEP 3: Make a request for the access token, and store it in // oauthParameters String response = httpClient.getResponse(url); Map<String, String> queryString = OAuthUtil.parseQuerystring(response); oauthParameters.setOAuthToken( queryString.get(OAuthParameters.OAUTH_TOKEN_KEY)); oauthParameters.setOAuthTokenSecret( queryString.get(OAuthParameters.OAUTH_TOKEN_SECRET_KEY)); // clear the request-specific parameters set in getOAuthUrl(), such as // nonce, timestamp and signature, which are only needed for a single // request. oauthParameters.reset(); return oauthParameters.getOAuthToken(); } /** * Generates the string to be used as the HTTP authorization header. A * typical authorization header will look something like this: * <p> * OAuth realm="", oauth_signature="SOME_LONG_STRING", oauth_nonce="123456", * oauth_signature_method="RSA-SHA1", oauth_consumer_key="www.example.com", * oauth_token="abc123", oauth_timestamp="123456" * <p> * The following parameters are required in {@link OAuthParameters}: * <ul> * <li>oauth_consumer_key * <li>oauth_token * <li>oauth_token_secret (if signing with HMAC) * </ul> * @see <a href="http://oauth.net/core/1.0/#auth_header_authorization">OAuth * Authorization Header</a> * * @param requestUrl the url of the request * @param httpMethod the http method of the request (for example GET) * @param oauthParameters OAuth parameters for this request * @return the full authorization header * @throws OAuthException if there is an error with the OAuth request */ public String getAuthorizationHeader(String requestUrl, String httpMethod, OAuthParameters oauthParameters) throws OAuthException { TwoLeggedOAuthHelper helper = new TwoLeggedOAuthHelper(signer, oauthParameters); helper.validateInputParameters(); // If a user is present in the request, it normally means that it is a // Two-legged OAuth request. Clients should use class // {@link TwoLeggedOAuthHelper} instead. if (!containsUser(requestUrl)) { oauthParameters.assertOAuthTokenExists(); if (signer instanceof OAuthHmacSha1Signer) { oauthParameters.assertOAuthTokenSecretExists(); } } return helper.addParametersAndRetrieveHeader(requestUrl, httpMethod); } /** * Revokes the user's OAuth token. The following parameters are required in * {@link OAuthParameters}: * <ul> * <li>oauth_consumer_key * <li>oauth_token * <li>oauth_token_secret (if signing with HMAC) * </ul> * Only tokens obtained by three-legged OAuth can be revoked (in other words, * this method is not applicable to two-legged OAuth). * * @param oauthParameters OAuth parameters for this request. * @throws OAuthException if there is an error with the OAuth request */ public void revokeToken(OAuthParameters oauthParameters) throws OAuthException { Map<String, String> headers = new HashMap<String, String>(); headers.put("Authorization", getAuthorizationHeader(revokeTokenUrl, "GET", oauthParameters)); try { httpClient.getResponse(new URL(revokeTokenUrl), headers); } catch (MalformedURLException mue) { throw new OAuthException(mue); } } /** * Returns a properly formatted and signed OAuth request url, with the * appropriate parameters. * * @param baseUrl the url to make the request to * @param httpMethod the http method of this request (for example, "GET") * @param oauthParameters OAuth parameters for this request * @return the OAuth request url * @throws OAuthException if there is an error with the OAuth request */ public URL getOAuthUrl(String baseUrl, String httpMethod, OAuthParameters oauthParameters) throws OAuthException { TwoLeggedOAuthHelper helper = new TwoLeggedOAuthHelper(signer, oauthParameters); // add request-specific parameters helper.addCommonRequestParameters(baseUrl, httpMethod); // add all query string information KeyValuePair queryParams = new QueryKeyValuePair(); for (Map.Entry<String, String> e : oauthParameters.getBaseParameters().entrySet()) { if (e.getValue().length() > 0) { queryParams.add(e.getKey(), e.getValue()); } } queryParams.add(OAuthParameters.OAUTH_SIGNATURE_KEY, oauthParameters.getOAuthSignature()); // build the url string StringBuilder fullUrl = new StringBuilder(baseUrl); fullUrl.append(baseUrl.indexOf("?") > 0 ? "&" : "?"); fullUrl.append(queryParams.toString()); try { return new URL(fullUrl.toString()); } catch (MalformedURLException mue) { throw new OAuthException(mue); } } /** Returns whether the request is a Two-Legged OAuth request. * the oauthParameter type - 2LO is performed in TwoLeggedOAuthHelper now. */ private boolean containsUser(String requestUrl) { return requestUrl.contains(OAuthParameters.XOAUTH_REQUESTOR_ID_KEY); } }