/* * Firetweet - Twitter client for Android * * Copyright (C) 2012-2015 Mariotaku Lee <mariotaku.lee@gmail.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package twitter4j.auth; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Random; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import twitter4j.TwitterException; import twitter4j.conf.Configuration; import twitter4j.http.BASE64Encoder; import twitter4j.http.HttpClientWrapper; import twitter4j.http.HttpParameter; import twitter4j.http.HttpRequest; import twitter4j.internal.logging.Logger; import twitter4j.internal.util.InternalStringUtil; /** * @author Yusuke Yamamoto - yusuke at mac.com * @see <a href="http://oauth.net/core/1.0a/">OAuth Core 1.0a</a> */ public class OAuthAuthorization implements Authorization, OAuthSupport { private final Configuration conf; private transient static HttpClientWrapper http; private static final String HMAC_SHA1 = "HmacSHA1"; private static final HttpParameter OAUTH_SIGNATURE_METHOD = new HttpParameter("oauth_signature_method", "HMAC-SHA1"); private static final Logger logger = Logger.getLogger(OAuthAuthorization.class); private String consumerKey = ""; private String consumerSecret; private String realm = null; private OAuthToken oauthToken = null; // constructors private static Random RAND = new Random(); /** * @param conf configuration */ public OAuthAuthorization(final Configuration conf) { this.conf = conf; http = new HttpClientWrapper(conf); setOAuthConsumer(conf.getOAuthConsumerKey(), conf.getOAuthConsumerSecret()); if (conf.getOAuthAccessToken() != null && conf.getOAuthAccessTokenSecret() != null) { setOAuthAccessToken(new AccessToken(conf.getOAuthAccessToken(), conf.getOAuthAccessTokenSecret())); } } @Override public boolean equals(final Object o) { if (this == o) return true; if (!(o instanceof OAuthSupport)) return false; final OAuthAuthorization that = (OAuthAuthorization) o; if (consumerKey != null ? !consumerKey.equals(that.consumerKey) : that.consumerKey != null) return false; if (consumerSecret != null ? !consumerSecret.equals(that.consumerSecret) : that.consumerSecret != null) return false; if (oauthToken != null ? !oauthToken.equals(that.oauthToken) : that.oauthToken != null) return false; return true; } public List<HttpParameter> generateOAuthSignatureHttpParams(final String method, final String sign_url) { final long timestamp = System.currentTimeMillis() / 1000; final long nonce = timestamp + RAND.nextInt(); final List<HttpParameter> oauthHeaderParams = new ArrayList<HttpParameter>(5); oauthHeaderParams.add(new HttpParameter("oauth_consumer_key", consumerKey)); oauthHeaderParams.add(OAUTH_SIGNATURE_METHOD); oauthHeaderParams.add(new HttpParameter("oauth_timestamp", timestamp)); oauthHeaderParams.add(new HttpParameter("oauth_nonce", nonce)); oauthHeaderParams.add(new HttpParameter("oauth_version", "1.0")); if (oauthToken != null) { oauthHeaderParams.add(new HttpParameter("oauth_token", oauthToken.getToken())); } final List<HttpParameter> signatureBaseParams = new ArrayList<HttpParameter>(oauthHeaderParams.size()); signatureBaseParams.addAll(oauthHeaderParams); parseGetParameters(sign_url, signatureBaseParams); final StringBuffer base = new StringBuffer(method).append("&") .append(HttpParameter.encode(constructRequestURL(sign_url))).append("&"); base.append(HttpParameter.encode(normalizeRequestParameters(signatureBaseParams))); final String oauthBaseString = base.toString(); final String signature = generateSignature(oauthBaseString, oauthToken); oauthHeaderParams.add(new HttpParameter("oauth_signature", signature)); return oauthHeaderParams; } // implementation for OAuthSupport interface // implementations for Authorization @Override public String getAuthorizationHeader(final HttpRequest req) { return generateAuthorizationHeader(req.getMethod().name(), req.getSignURL(), req.getParameters(), oauthToken); } /** * {@inheritDoc} */ @Override public AccessToken getOAuthAccessToken() throws TwitterException { ensureTokenIsAvailable(); if (oauthToken instanceof AccessToken) return (AccessToken) oauthToken; oauthToken = new AccessToken(http.post(conf.getOAuthAccessTokenURL(), conf.getSigningOAuthAccessTokenURL(), this)); return (AccessToken) oauthToken; } /** * {@inheritDoc} */ @Override public AccessToken getOAuthAccessToken(final RequestToken requestToken) throws TwitterException { oauthToken = requestToken; return getOAuthAccessToken(); } /** * {@inheritDoc} */ @Override public AccessToken getOAuthAccessToken(final RequestToken requestToken, final String oauthVerifier) throws TwitterException { oauthToken = requestToken; return getOAuthAccessToken(oauthVerifier); } /** * {@inheritDoc} */ @Override public AccessToken getOAuthAccessToken(final String oauthVerifier) throws TwitterException { ensureTokenIsAvailable(); final String url = conf.getOAuthAccessTokenURL(); if (0 == url.indexOf("http://")) { // SSL is required // @see https://dev.twitter.com/docs/oauth/xauth // url = "https://" + url.substring(7); } final String sign_url = conf.getSigningOAuthAccessTokenURL(); if (0 == sign_url.indexOf("http://")) { // SSL is required // @see https://dev.twitter.com/docs/oauth/xauth // sign_url = "https://" + sign_url.substring(7); } oauthToken = new AccessToken(http.post(url, sign_url, new HttpParameter[]{new HttpParameter("oauth_verifier", oauthVerifier)}, this)); return (AccessToken) oauthToken; } /** * {@inheritDoc} */ @Override public AccessToken getOAuthAccessToken(final String screenName, final String password) throws TwitterException { try { final String url = conf.getOAuthAccessTokenURL(); if (0 == url.indexOf("http://")) { // SSL is required // @see https://dev.twitter.com/docs/oauth/xauth // url = "https://" + url.substring(7); } final String sign_url = conf.getSigningOAuthAccessTokenURL(); if (0 == sign_url.indexOf("http://")) { // SSL is required // @see https://dev.twitter.com/docs/oauth/xauth // sign_url = "https://" + sign_url.substring(7); } oauthToken = new AccessToken(http.post(url, sign_url, new HttpParameter[]{ new HttpParameter("x_auth_username", screenName), new HttpParameter("x_auth_password", password), new HttpParameter("x_auth_mode", "client_auth")}, this)); return (AccessToken) oauthToken; } catch (final TwitterException te) { throw new TwitterException("The screen name / password combination seems to be invalid.", te, te.getStatusCode()); } } /** * {@inheritDoc} */ @Override public RequestToken getOAuthRequestToken() throws TwitterException { return getOAuthRequestToken(null, null); } /** * {@inheritDoc} */ @Override public RequestToken getOAuthRequestToken(final String callbackURL) throws TwitterException { return getOAuthRequestToken(callbackURL, null); } /** * {@inheritDoc} */ @Override public RequestToken getOAuthRequestToken(final String callbackURL, final String xAuthAccessType) throws TwitterException { if (oauthToken instanceof AccessToken) throw new IllegalStateException("Access token already available."); final List<HttpParameter> params = new ArrayList<HttpParameter>(); if (callbackURL != null) { params.add(new HttpParameter("oauth_callback", callbackURL)); } if (xAuthAccessType != null) { params.add(new HttpParameter("x_auth_access_type", xAuthAccessType)); } final String url = conf.getOAuthRequestTokenURL(); if (0 == url.indexOf("http://")) { // SSL is required // @see https://dev.twitter.com/docs/oauth/xauth // url = "https://" + url.substring(7); } final String sign_url = conf.getSigningOAuthRequestTokenURL(); if (0 == sign_url.indexOf("http://")) { // SSL is required // @see https://dev.twitter.com/docs/oauth/xauth // sign_url = "https://" + sign_url.substring(7); } oauthToken = new RequestToken(conf, http.post(url, sign_url, params.toArray(new HttpParameter[params.size()]), this), this); return (RequestToken) oauthToken; } @Override public int hashCode() { int result = consumerKey != null ? consumerKey.hashCode() : 0; result = 31 * result + (consumerSecret != null ? consumerSecret.hashCode() : 0); result = 31 * result + (oauthToken != null ? oauthToken.hashCode() : 0); return result; } /** * #{inheritDoc} */ @Override public boolean isEnabled() { return oauthToken != null && oauthToken instanceof AccessToken; } /** * {@inheritDoc} */ @Override public void setOAuthAccessToken(final AccessToken accessToken) { oauthToken = accessToken; } @Override public void setOAuthConsumer(final String consumerKey, final String consumerSecret) { this.consumerKey = consumerKey != null ? consumerKey : ""; this.consumerSecret = consumerSecret != null ? consumerSecret : ""; } /** * Sets the OAuth realm * * @param realm OAuth realm * @since Twitter 2.1.4 */ public void setOAuthRealm(final String realm) { this.realm = realm; } @Override public String toString() { return "OAuthAuthorization{" + "consumerKey='" + consumerKey + '\'' + ", consumerSecret='******************************************\'" + ", oauthToken=" + oauthToken + '}'; } private void ensureTokenIsAvailable() { if (null == oauthToken) throw new IllegalStateException("No Token available."); } /* package */ private void parseGetParameters(final String url, final List<HttpParameter> signatureBaseParams) { final int queryStart = url.indexOf("?"); if (-1 != queryStart) { final String[] queryStrs = InternalStringUtil.split(url.substring(queryStart + 1), "&"); try { for (final String query : queryStrs) { final String[] split = InternalStringUtil.split(query, "="); if (split.length == 2) { signatureBaseParams.add(new HttpParameter(URLDecoder.decode(split[0], "UTF-8"), URLDecoder .decode(split[1], "UTF-8"))); } else { signatureBaseParams.add(new HttpParameter(URLDecoder.decode(split[0], "UTF-8"), "")); } } } catch (final UnsupportedEncodingException ignore) { } } } /** * @return generated authorization header * @see <a href="http://oauth.net/core/1.0a/#rfc.section.5.4.1">OAuth Core - * 5.4.1. Authorization Header</a> */ /* package */String generateAuthorizationHeader(final String method, final String sign_url, final HttpParameter[] params, final OAuthToken token) { final long timestamp = System.currentTimeMillis() / 1000; final long nonce = timestamp + RAND.nextInt(); return generateAuthorizationHeader(method, sign_url, params, String.valueOf(nonce), String.valueOf(timestamp), token); } /* package */String generateAuthorizationHeader(final String method, final String sign_url, HttpParameter[] params, final String nonce, final String timestamp, final OAuthToken otoken) { if (null == params) { params = new HttpParameter[0]; } final List<HttpParameter> oauthHeaderParams = new ArrayList<HttpParameter>(5); oauthHeaderParams.add(new HttpParameter("oauth_consumer_key", consumerKey)); oauthHeaderParams.add(OAUTH_SIGNATURE_METHOD); oauthHeaderParams.add(new HttpParameter("oauth_timestamp", timestamp)); oauthHeaderParams.add(new HttpParameter("oauth_nonce", nonce)); oauthHeaderParams.add(new HttpParameter("oauth_version", "1.0")); if (otoken != null) { oauthHeaderParams.add(new HttpParameter("oauth_token", otoken.getToken())); } final List<HttpParameter> signatureBaseParams = new ArrayList<HttpParameter>(oauthHeaderParams.size() + params.length); signatureBaseParams.addAll(oauthHeaderParams); if (!HttpParameter.containsFile(params)) { signatureBaseParams.addAll(toParamList(params)); } parseGetParameters(sign_url, signatureBaseParams); final StringBuffer base = new StringBuffer(method).append("&") .append(HttpParameter.encode(constructRequestURL(sign_url))).append("&"); base.append(HttpParameter.encode(normalizeRequestParameters(signatureBaseParams))); final String oauthBaseString = base.toString(); logger.debug("OAuth base string: ", oauthBaseString); final String signature = generateSignature(oauthBaseString, otoken); logger.debug("OAuth signature: ", signature); oauthHeaderParams.add(new HttpParameter("oauth_signature", signature)); // http://oauth.net/core/1.0/#rfc.section.9.1.1 if (realm != null) { oauthHeaderParams.add(new HttpParameter("realm", realm)); } return "OAuth " + encodeParameters(oauthHeaderParams, ",", true); } String generateSignature(final String data) { return generateSignature(data, null); } /** * Computes RFC 2104-compliant HMAC signature. * * @param data the data to be signed * @param token the token * @return signature * @see <a href="http://oauth.net/core/1.0a/#rfc.section.9.2.1">OAuth Core - * 9.2.1. Generating Signature</a> */ /* package */String generateSignature(final String data, final OAuthToken token) { byte[] byteHMAC = null; try { final Mac mac = Mac.getInstance(HMAC_SHA1); SecretKeySpec spec; if (null == token) { final String oauthSignature = HttpParameter.encode(consumerSecret) + "&"; spec = new SecretKeySpec(oauthSignature.getBytes(), HMAC_SHA1); } else { spec = token.getSecretKeySpec(); if (null == spec) { final String oauthSignature = HttpParameter.encode(consumerSecret) + "&" + HttpParameter.encode(token.getTokenSecret()); spec = new SecretKeySpec(oauthSignature.getBytes(), HMAC_SHA1); token.setSecretKeySpec(spec); } } mac.init(spec); byteHMAC = mac.doFinal(data.getBytes()); } catch (final InvalidKeyException ike) { logger.error("Failed initialize \"Message Authentication Code\" (MAC)", ike); throw new AssertionError(ike); } catch (final NoSuchAlgorithmException nsae) { logger.error("Failed to get HmacSHA1 \"Message Authentication Code\" (MAC)", nsae); throw new AssertionError(nsae); } return BASE64Encoder.encode(byteHMAC); } /** * The Signature Base String includes the request absolute URL, tying the * signature to a specific endpoint. The URL used in the Signature Base * String MUST include the scheme, authority, and path, and MUST exclude the * query and fragment as defined by [RFC3986] section 3.<br> * If the absolute request URL is not available to the Service Provider (it * is always available to the Consumer), it can be constructed by combining * the scheme being used, the HTTP Host header, and the relative HTTP * request URL. If the Host header is not available, the Service Provider * SHOULD use the host name communicated to the Consumer in the * documentation or other means.<br> * The Service Provider SHOULD document the form of URL used in the * Signature Base String to avoid ambiguity due to URL normalization. Unless * specified, URL scheme and authority MUST be lowercase and include the * port number; http default port 80 and https default port 443 MUST be * excluded.<br> * <br> * For example, the request:<br> * HTTP://Example.com:80/resource?id=123<br> * Is included in the Signature Base String as:<br> * http://example.com/resource * * @param url the url to be normalized * @return the Signature Base String * @see <a href="http://oauth.net/core/1.0#rfc.section.9.1.2">OAuth Core - * 9.1.2. Construct Request URL</a> */ public static String constructRequestURL(String url) { final int index = url.indexOf("?"); if (-1 != index) { url = url.substring(0, index); } final int slashIndex = url.indexOf("/", 8); String baseURL = url.substring(0, slashIndex).toLowerCase(Locale.US); final int colonIndex = baseURL.indexOf(":", 8); if (-1 != colonIndex) { // url contains port number if (baseURL.startsWith("http://") && baseURL.endsWith(":80")) { // http default port 80 MUST be excluded baseURL = baseURL.substring(0, colonIndex); } else if (baseURL.startsWith("https://") && baseURL.endsWith(":443")) { // http default port 443 MUST be excluded baseURL = baseURL.substring(0, colonIndex); } } url = baseURL + url.substring(slashIndex); return url; } /** * @param httpParams parameters to be encoded and concatenated * @return encoded string * @see <a href="http://wiki.oauth.net/TestCases">OAuth / TestCases</a> * @see <a * href="http://groups.google.com/group/oauth/browse_thread/thread/a8398d0521f4ae3d/9d79b698ab217df2?hl=en&lnk=gst&q=space+encoding#9d79b698ab217df2">Space * encoding - OAuth | Google Groups</a> */ public static String encodeParameters(final List<HttpParameter> httpParams) { return encodeParameters(httpParams, "&", false); } public static String encodeParameters(final List<HttpParameter> httpParams, final String splitter, final boolean quot) { final StringBuffer buf = new StringBuffer(); for (final HttpParameter param : httpParams) { if (!param.isFile()) { if (buf.length() != 0) { if (quot) { buf.append("\""); } buf.append(splitter); } buf.append(HttpParameter.encode(param.getName())).append("="); if (quot) { buf.append("\""); } buf.append(HttpParameter.encode(param.getValue())); } } if (buf.length() != 0) { if (quot) { buf.append("\""); } } return buf.toString(); } public static String normalizeAuthorizationHeaders(final List<HttpParameter> params) { Collections.sort(params); return encodeParameters(params); } /** * The request parameters are collected, sorted and concatenated into a * normalized string:<br> * • Parameters in the OAuth HTTP Authorization header excluding the realm * parameter.<br> * • Parameters in the HTTP POST request body (with a content-type of * application/x-www-form-urlencoded).<br> * • HTTP GET parameters added to the URLs in the query part (as defined by * [RFC3986] section 3).<br> * <br> * The oauth_signature parameter MUST be excluded.<br> * The parameters are normalized into a single string as follows:<br> * 1. Parameters are sorted by name, using lexicographical byte value * ordering. If two or more parameters share the same name, they are sorted * by their value. For example:<br> * 2. a=1, c=hi%20there, f=25, f=50, f=a, z=p, z=t<br> * 3. <br> * 4. Parameters are concatenated in their sorted order into a single * string. For each parameter, the name is separated from the corresponding * value by an ‘=’ character (ASCII code 61), even if the value is empty. * Each name-value pair is separated by an ‘&’ character (ASCII code 38). * For example:<br> * 5. a=1&c=hi%20there&f=25&f=50&f=a&z=p&z=t<br> * 6. <br> * * @param params parameters to be normalized and concatenated * @return normalized and concatenated parameters * @see <a href="http://oauth.net/core/1.0#rfc.section.9.1.1">OAuth Core - * 9.1.1. Normalize Request Parameters</a> */ public static String normalizeRequestParameters(final HttpParameter[] params) { return normalizeRequestParameters(toParamList(params)); } public static String normalizeRequestParameters(final List<HttpParameter> params) { Collections.sort(params); return encodeParameters(params); } public static List<HttpParameter> toParamList(final HttpParameter[] params) { final List<HttpParameter> paramList = new ArrayList<HttpParameter>(params.length); paramList.addAll(Arrays.asList(params)); return paramList; } }