package com.sap.jam.oauth.client; import static com.sap.jam.oauth.client.OAuthUtils.OAUTH_CALLBACK; import static com.sap.jam.oauth.client.OAuthUtils.OAUTH_CONSUMER_KEY; import static com.sap.jam.oauth.client.OAuthUtils.OAUTH_NONCE; import static com.sap.jam.oauth.client.OAuthUtils.OAUTH_SIGNATURE; import static com.sap.jam.oauth.client.OAuthUtils.OAUTH_SIGNATURE_METHOD; import static com.sap.jam.oauth.client.OAuthUtils.OAUTH_TIMESTAMP; import static com.sap.jam.oauth.client.OAuthUtils.OAUTH_TOKEN; import static com.sap.jam.oauth.client.OAuthUtils.OAUTH_VERIFIER; import static com.sap.jam.oauth.client.OAuthUtils.OAUTH_VERSION; import static com.sap.jam.oauth.client.OAuthUtils.REALM_PARAM; import java.net.URL; import java.security.PrivateKey; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.UUID; /** * Helper class to allow clients using any web framework to make calls to OAuth1.0a providers. * It is especially designed to be compatible with the OAuth provider provided by Jam. * * Supports: * <ol> * <li> regular three-legged OAuth calls * <li> two-legged OAuth calls * <li> obtaining a request token POST /oauth/request_token * <li> obtaining an access token POST /oauth/access_token * </ol> * * To use this class, create a new OAuthClientHelper object. Then call the builder methods of this class to set the properties required for the call you * are trying to make, and then call the generateAuthorizationHeader method to generate the text to set as the Authorization header * value in making your actual OAuth request. The JUnit tests in OAuthClientHelperTest show you how to set up the calls in each of the * supported cases. * * The PLAINTEXT signature method is not recommended for production use. * Where the client can keep the consumer secret a true secret then the RSA-SHA1 signature method is recommended. * Where the client cannot keep the consumer secret a true secret (but can keep the token secret a true secret per user) then the HMAC-SHA1 signature method is recommended. * * It is also a good idea to not explicitly set the timestamp and nonce (i.e. don't call setTimestamp or setNonce) and use the defaults provided. * * This library also supports setting the OAuth parameters in the request body or query string, but these are not recommended by the OAuth spec. */ public final class OAuthClientHelper { private HttpMethod httpMethod; /** * The targeted endpoint, including any query parameters but not including any OAuth protocol parameters * if the OAuth request parameters are generated for the query string. * For example, https://example.com/v1/activities?page_size=10 */ private URL requestUrl; /** Optional. */ private String realm; /** Required. Identifies the OAuth client to the OAuth provider. */ private String consumerKey; /** Only required and used for PLAINTEXT and HMAC-SHA1 signature types. Otherwise can leave null. */ private String consumerSecret; /** Only required and used for RSA-SHA1 signature types. */ private PrivateKey consumerPrivateKey; /** In the case of two-legged OAuth, should be set to the empty String "", and not null.*/ private String token; /** Not used in the case of the RSA-SHA1 signature type, even if the token is used. */ private String tokenSecret; private SignatureMethod signatureMethod; /** Unix time in seconds. If unset or <=0, OAuth parameter generation will use the current Unix time.*/ private long timestamp; /** If null, OAuth parameter generation will use a randomly generated uuid. */ private String nonce; /** * The default behavior is to include the oauth_version parameter with its only legal value "1.0". Set to null * to omit the oauth_version parameter. */ private String oauthVersion = "1.0"; /** Only used for POST /oauth/access_token requests. Otherwise leave null. */ private String verifier; /** Only used for POST /oauth/request_token requests. A URL or "oob" for out of band. Otherwise leave null. */ private String callback; /** * Extra key-value pairs to be added to the Authorization header along with the OAuth parameters. Do not encode the keys or values. * For use only when the OAuth parameters are to be included in the Authorization header. */ private List<Pair<String, String>> extraAuthorizationHeaderParams; /** * For use only when request has Content-Type application/x-www-form-urlencoded. In this case, the request body parameters * are included in forming the OAuth signature when using the HMAC-SHA1 or RSA-SHA1 signature types. In particular, * this field does not need to be set for the PLAINTEXT signature type, or when the request does not have the above * mentioned Content-Type header value. * Do not encode the keys or values. * Do not include OAuth protocol parameters. */ private List<Pair<String, String>> extraRequestBodyParams; public OAuthClientHelper(){ } public HttpMethod getHttpMethod() { return httpMethod; } public void setHttpMethod(HttpMethod httpMethod) { this.httpMethod = httpMethod; } public URL getRequestUrl() { return requestUrl; } /** * @param requestUrl The targeted endpoint, including any query parameters but not including any OAuth protocol parameters * if the OAuth request parameters are generated for the query string. * For example, https://example.com/v1/activities?page_size=10 */ public void setRequestUrl(URL requestUrl) { this.requestUrl = requestUrl; } public String getRealm() { return realm; } /** * @param realm Optional. */ public void setRealm(String realm) { this.realm = realm; } public String getConsumerKey() { return consumerKey; } /** * @param consumerKey Required. Identifies the OAuth client to the OAuth provider. */ public void setConsumerKey(String consumerKey) { this.consumerKey = consumerKey; } public String getConsumerSecret() { return consumerSecret; } /** * @param consumerSecret Only required and used for PLAINTEXT and HMAC-SHA1 signature types. Otherwise can leave null. */ public void setConsumerSecret(String consumerSecret) { this.consumerSecret = consumerSecret; } public PrivateKey getConsumerPrivateKey() { return consumerPrivateKey; } /** * @param consumerPrivateKey Only required and used for RSA-SHA1 signature types. */ public void setConsumerPrivateKey(PrivateKey consumerPrivateKey) { this.consumerPrivateKey = consumerPrivateKey; } public String getToken() { return token; } /** * @param token In the case of two-legged OAuth, should be set to the empty String "", and not null. */ public void setToken(String token) { this.token = token; } public void setTokenForTwoLeggedOAuth() { setToken(""); } public String getTokenSecret() { return tokenSecret; } /** * @param tokenSecret Not used in the case of the RSA-SHA1 signature type, even if the token is used. */ public void setTokenSecret(String tokenSecret) { this.tokenSecret = tokenSecret; } public SignatureMethod getSignatureMethod() { return signatureMethod; } public void setSignatureMethod(SignatureMethod signatureMethod) { this.signatureMethod = signatureMethod; } public long getTimestamp() { return timestamp; } /** * @param timestamp Unix time in seconds. If unset or <=0, OAuth parameter generation will use the current Unix time. */ public void setTimestamp(long timestamp) { this.timestamp = timestamp; } public String getNonce() { return nonce; } /** * @param nonce If null, OAuth parameter generation will use a randomly generated uuid. */ public void setNonce(String nonce) { this.nonce = nonce; } public String getOauthVersion() { return oauthVersion; } /** * @param oauthVersion The default behavior is to include the oauth_version parameter with its only legal value "1.0". Set to null * to omit the oauth_version parameter. */ public void setOauthVersion(String oauthVersion) { this.oauthVersion = oauthVersion; } public void omitOAuthVersion() { setOauthVersion(null); } public String getVerifier() { return verifier; } /** Only used for POST /oauth/access_token requests. Otherwise leave null. */ public void setVerifier(String verifier) { this.verifier = verifier; } public String getCallback() { return callback; } /** * @param callback Only used for POST /oauth/request_token requests. A URL or "oob" for out of band. Otherwise leave null. */ public void setCallback(String callback) { this.callback = callback; } public void setCallbackUrl(URL callbackUrl) { setCallback(callbackUrl.toString()); } public void setOutOfBandCallback(){ setCallback("oob"); } public List<Pair<String, String>> getExtraAuthorizationHeaderParams() { return extraAuthorizationHeaderParams; } /** * @param extraAuthorizationHeaderParams Extra key-value pairs to be added to the Authorization header along with * the OAuth parameters. Do not encode the keys or values. * For use only when the OAuth parameters are to be included in the Authorization header. */ public void setExtraAuthorizationHeaderParams(List<Pair<String, String>> extraAuthorizationHeaderParams) { this.extraAuthorizationHeaderParams = extraAuthorizationHeaderParams; } public List<Pair<String, String>> getExtraRequestBodyParams() { return extraRequestBodyParams; } /** * @param extraRequestBodyParams For use only when request has Content-Type application/x-www-form-urlencoded. * In this case, the request body parameters * are included in forming the OAuth signature when using the HMAC-SHA1 or RSA-SHA1 signature types. In particular, * this field does not need to be set for the PLAINTEXT signature type, or when the request does not have the above * mentioned Content-Type header value. * Do not encode the keys or values. * Do not include OAuth protocol parameters. */ public void setExtraRequestBodyParams(List<Pair<String, String>> extraRequestBodyParams) { this.extraRequestBodyParams = extraRequestBodyParams; } /** * Generate info for the OAuth request in the case where the OAuth parameters are to be included in the Authorization headers. * This is the recommended location for OAuth parameters. (http://tools.ietf.org/html/rfc5849#section-3.5) * * @return text to set as the Authorization header value in making your actual OAuth request. The JUnit tests in OAuthClientHelperTest show you * how to set up the calls in each of the supported cases. */ public String generateAuthorizationHeader() { verifyPreconditions(); String nonce = this.nonce == null ? UUID.randomUUID().toString() : this.nonce; long timestamp = this.timestamp <= 0 ? System.currentTimeMillis()/1000L : this.timestamp; String signature = generateSignature(nonce, timestamp); StringBuilder sb = new StringBuilder(); sb.append("OAuth "); if (realm != null) { sb.append(REALM_PARAM).append("=\"").append(OAuthUtils.oauthEncode(realm)).append("\", "); } sb.append(OAUTH_CONSUMER_KEY).append("=\"").append(OAuthUtils.oauthEncode(consumerKey)).append("\", "); if (token != null) { sb.append(OAUTH_TOKEN).append("=\"").append(OAuthUtils.oauthEncode(token)).append("\", "); } sb.append(OAUTH_SIGNATURE_METHOD).append("=\"").append(signatureMethod.toString()).append("\", "); sb.append(OAUTH_SIGNATURE).append("=\"").append(OAuthUtils.oauthEncode(signature)).append("\", "); sb.append(OAUTH_TIMESTAMP).append("=\"").append(timestamp).append("\", "); sb.append(OAUTH_NONCE).append("=\"").append(OAuthUtils.oauthEncode(nonce)).append('"'); if (oauthVersion != null) { sb.append(", ").append(OAUTH_VERSION).append("=\"").append(OAuthUtils.oauthEncode(oauthVersion)).append('"'); } if (callback != null) { //must be a POST /oauth/request_token request sb.append(", ").append(OAUTH_CALLBACK).append("=\"").append(OAuthUtils.oauthEncode(callback)).append('"'); } if (verifier != null) { sb.append(", ").append(OAUTH_VERIFIER).append("=\"").append(OAuthUtils.oauthEncode(verifier)).append('"'); } if (extraAuthorizationHeaderParams != null) { for (Pair<String, String> entry : extraAuthorizationHeaderParams) { sb.append(", ").append(OAuthUtils.oauthEncode(entry.fst())).append("=\"").append(OAuthUtils.oauthEncode(entry.snd())).append('"'); } } return sb.toString(); } /** * Generate info for the OAuth request in the case where the OAuth parameters are to be included in the request body. * This is the second-favored location for OAuth parameters. (http://tools.ietf.org/html/rfc5849#section-3.5) * * @return escaped text to set as the request body value in making your actual OAuth request. */ public String generateRequestBody() { return generateRequestQueryOrBody(false); } /** * Generate info for the OAuth request in the case where the OAuth parameters are to be included in the query string. * This is the least-favored location for OAuth parameters. (http://tools.ietf.org/html/rfc5849#section-3.5) * * @return escaped text to set as the query string value in making your actual OAuth request. */ public String generateRequestQuery() { return generateRequestQueryOrBody(true); } private String generateRequestQueryOrBody(boolean isQuery) { verifyPreconditions(); String nonce = this.nonce == null ? UUID.randomUUID().toString() : this.nonce; long timestamp = this.timestamp <= 0 ? System.currentTimeMillis()/1000L : this.timestamp; String signature = generateSignature(nonce, timestamp); StringBuilder sb = new StringBuilder(); if (isQuery) { String query = requestUrl.getQuery(); if (query != null) { sb.append(query).append('&'); } } else { if (extraRequestBodyParams != null) { for (Pair<String, String> entry : extraRequestBodyParams) { sb.append(OAuthUtils.urlEncode(entry.fst())).append('=').append(OAuthUtils.urlEncode(entry.snd())).append('&'); } } } sb.append(OAUTH_CONSUMER_KEY).append('=').append(OAuthUtils.urlEncode(consumerKey)).append('&'); if (token != null) { sb.append(OAUTH_TOKEN).append('=').append(OAuthUtils.urlEncode(token)).append('&'); } sb.append(OAUTH_SIGNATURE_METHOD).append('=').append(signatureMethod.toString()).append('&'); sb.append(OAUTH_SIGNATURE).append('=').append(OAuthUtils.urlEncode(signature)).append('&'); sb.append(OAUTH_TIMESTAMP).append('=').append(timestamp).append('&'); sb.append(OAUTH_NONCE).append('=').append(OAuthUtils.urlEncode(nonce)); if (oauthVersion != null) { sb.append('&').append(OAUTH_VERSION).append('=').append(OAuthUtils.urlEncode(oauthVersion)); } if (callback != null) { //must be a POST /oauth/request_token request sb.append('&').append(OAUTH_CALLBACK).append('=').append(OAuthUtils.urlEncode(callback)); } if (verifier != null) { sb.append('&').append(OAUTH_VERIFIER).append('=').append(OAuthUtils.urlEncode(verifier)); } return sb.toString(); } private void verifyPreconditions() { if (consumerKey == null) { throw new IllegalStateException("consumerKey should not be null."); } if (signatureMethod == null) { throw new IllegalStateException("signatureMethod should not be null."); } } /** * This method is used as part of the implementation of generateAuthorizationHeader, generateRequestBody, and * generateRequestQuery that clients would normally call. * It is useful as a public method mainly for debugging OAuth calls in the situation where the client developer * has debug access to the provider. In that case, a useful technique is to compare the signature base string at the * client and provider side and see where they differ. */ public String generateSignatureBaseString(String nonce, long timestamp) { if (signatureMethod == SignatureMethod.PLAINTEXT) { return OAuthUtils.baseOAuthSignature(consumerSecret, tokenSecret); } if (timestamp <= 0) { throw new IllegalArgumentException("Argument timestamp must be > 0."); } List<Pair<String, String>> signatureBaseParams = new ArrayList<Pair<String, String>>(); signatureBaseParams.add(new Pair<String, String>(OAUTH_CONSUMER_KEY, OAuthUtils.oauthEncode(consumerKey))); signatureBaseParams.add(new Pair<String, String>(OAUTH_NONCE, OAuthUtils.oauthEncode(nonce))); signatureBaseParams.add(new Pair<String, String>(OAUTH_SIGNATURE_METHOD, signatureMethod.toString())); signatureBaseParams.add(new Pair<String, String>(OAUTH_TIMESTAMP, Long.toString(timestamp))); if (oauthVersion != null) { signatureBaseParams.add(new Pair<String, String>(OAUTH_VERSION, OAuthUtils.oauthEncode(oauthVersion))); } if (token != null) { signatureBaseParams.add(new Pair<String, String>(OAUTH_TOKEN, OAuthUtils.oauthEncode(token))); } if (callback != null) { signatureBaseParams.add(new Pair<String, String>(OAUTH_CALLBACK, OAuthUtils.oauthEncode(callback))); } if (verifier != null) { signatureBaseParams.add(new Pair<String, String>(OAUTH_VERIFIER, OAuthUtils.oauthEncode(verifier))); } String queryString = requestUrl.getQuery(); if (queryString != null) { String[] queryParams = queryString.split("&"); for (int i = 0, nQueryParams = queryParams.length; i < nQueryParams; ++i) { String queryParam = queryParams[i]; int equalPos = queryParam.indexOf("="); Pair<String, String> encodedQueryParam; if (equalPos == -1) { encodedQueryParam = new Pair<String, String>(OAuthUtils.oauthEncode(OAuthUtils.urlDecode(queryParam)), ""); } else { encodedQueryParam = new Pair<String, String>(OAuthUtils.oauthEncode(OAuthUtils.urlDecode(queryParam.substring(0, equalPos))), OAuthUtils.oauthEncode(OAuthUtils.urlDecode(queryParam.substring(equalPos + 1, queryParam.length())))); } signatureBaseParams.add(encodedQueryParam); } } if (extraAuthorizationHeaderParams != null) { for (Pair<String, String> entry : extraAuthorizationHeaderParams) { signatureBaseParams.add(new Pair<String, String>(OAuthUtils.oauthEncode(entry.fst()), OAuthUtils.oauthEncode(entry.snd()))); } } if (extraRequestBodyParams != null) { for (Pair<String, String> entry : extraRequestBodyParams) { signatureBaseParams.add(new Pair<String, String>(OAuthUtils.oauthEncode(entry.fst()), OAuthUtils.oauthEncode(entry.snd()))); } } //sort according to lexicographic order of the pairs. Note it is important to sort the pairs and not just the Strings //joined with an '=' as the sort order can differ. For example, //a1=3, a=2 sorts in OAuth order as a=2, a1=3, but as Strings it is a1=3, a=2 since '1' < '='. Collections.sort(signatureBaseParams, OAuthUtils.STRING_PAIR_COMPARATOR); return httpMethod.toString() + "&" + OAuthUtils.oauthEncode(OAuthUtils.baseStringUrl(requestUrl)) + "&" + OAuthUtils.oauthEncode(OAuthUtils.joinPostBodyParams(signatureBaseParams)); } /** * This method is used as part of the implementation of generateAuthorizationHeader, generateRequestBody, and * generateRequestQuery that clients would normally call. * It is useful as a public method mainly for debugging OAuth calls in the situation where the client developer * has debug access to the provider. */ public String generateSignature(String nonce, long timestamp) { String signatureBaseString = generateSignatureBaseString(nonce, timestamp); switch (signatureMethod) { case PLAINTEXT: return signatureBaseString; case HMAC_SHA1: return OAuthUtils.calculateHmacSha1Signature(signatureBaseString, OAuthUtils.baseOAuthSignature(consumerSecret, tokenSecret)); case RSA_SHA1: return OAuthUtils.calculateRsaSha1Signature(signatureBaseString, consumerPrivateKey); default: throw new IllegalStateException("Unknown signatureMethod."); } } }