package com.android.email.mail.internet; import android.content.Context; import android.text.format.DateUtils; import com.android.email.activity.setup.AccountSettingsUtils; import com.android.emailcommon.Logging; import com.android.emailcommon.VendorPolicyLoader.OAuthProvider; import com.android.emailcommon.mail.AuthenticationFailedException; import com.android.emailcommon.mail.MessagingException; import com.android.mail.utils.LogUtils; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicNameValuePair; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; public class OAuthAuthenticator { private static final String TAG = Logging.LOG_TAG; public static final String OAUTH_REQUEST_CODE = "code"; public static final String OAUTH_REQUEST_REFRESH_TOKEN = "refresh_token"; public static final String OAUTH_REQUEST_CLIENT_ID = "client_id"; public static final String OAUTH_REQUEST_CLIENT_SECRET = "client_secret"; public static final String OAUTH_REQUEST_REDIRECT_URI = "redirect_uri"; public static final String OAUTH_REQUEST_GRANT_TYPE = "grant_type"; public static final String JSON_ACCESS_TOKEN = "access_token"; public static final String JSON_REFRESH_TOKEN = "refresh_token"; public static final String JSON_EXPIRES_IN = "expires_in"; private static final long CONNECTION_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS; private static final long COMMAND_TIMEOUT = 30 * DateUtils.SECOND_IN_MILLIS; final HttpClient mClient; public static class AuthenticationResult { public AuthenticationResult(final String accessToken, final String refreshToken, final int expiresInSeconds) { mAccessToken = accessToken; mRefreshToken = refreshToken; mExpiresInSeconds = expiresInSeconds; } @Override public String toString() { return "result access " + (mAccessToken==null?"null":"[REDACTED]") + " refresh " + (mRefreshToken==null?"null":"[REDACTED]") + " expiresInSeconds " + mExpiresInSeconds; } public final String mAccessToken; public final String mRefreshToken; public final int mExpiresInSeconds; } public OAuthAuthenticator() { final HttpParams params = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(params, (int)(CONNECTION_TIMEOUT)); HttpConnectionParams.setSoTimeout(params, (int)(COMMAND_TIMEOUT)); HttpConnectionParams.setSocketBufferSize(params, 8192); mClient = new DefaultHttpClient(params); } public AuthenticationResult requestAccess(final Context context, final String providerId, final String code) throws MessagingException, IOException { final OAuthProvider provider = AccountSettingsUtils.findOAuthProvider(context, providerId); if (provider == null) { LogUtils.e(TAG, "invalid provider %s", providerId); // This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed // exception, this will at least give the user a heads up to set up their account again. throw new AuthenticationFailedException("Invalid provider" + providerId); } final HttpPost post = new HttpPost(provider.tokenEndpoint); post.setHeader("Content-Type", "application/x-www-form-urlencoded"); final List nvp = new ArrayList(); nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CODE, code)); nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_ID, provider.clientId)); nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_SECRET, provider.clientSecret)); nvp.add(new BasicNameValuePair(OAUTH_REQUEST_REDIRECT_URI, provider.redirectUri)); nvp.add(new BasicNameValuePair(OAUTH_REQUEST_GRANT_TYPE, "authorization_code")); try { post.setEntity(new UrlEncodedFormEntity(nvp)); } catch (UnsupportedEncodingException e) { LogUtils.e(TAG, e, "unsupported encoding"); // This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed // exception, this will at least give the user a heads up to set up their account again. throw new AuthenticationFailedException("Unsupported encoding", e); } return doRequest(post); } public AuthenticationResult requestRefresh(final Context context, final String providerId, final String refreshToken) throws MessagingException, IOException { final OAuthProvider provider = AccountSettingsUtils.findOAuthProvider(context, providerId); if (provider == null) { LogUtils.e(TAG, "invalid provider %s", providerId); // This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed // exception, this will at least give the user a heads up to set up their account again. throw new AuthenticationFailedException("Invalid provider" + providerId); } final HttpPost post = new HttpPost(provider.refreshEndpoint); post.setHeader("Content-Type", "application/x-www-form-urlencoded"); final List nvp = new ArrayList(); nvp.add(new BasicNameValuePair(OAUTH_REQUEST_REFRESH_TOKEN, refreshToken)); nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_ID, provider.clientId)); nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_SECRET, provider.clientSecret)); nvp.add(new BasicNameValuePair(OAUTH_REQUEST_GRANT_TYPE, "refresh_token")); try { post.setEntity(new UrlEncodedFormEntity(nvp)); } catch (UnsupportedEncodingException e) { LogUtils.e(TAG, e, "unsupported encoding"); // This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed // exception, this will at least give the user a heads up to set up their account again. throw new AuthenticationFailedException("Unsuported encoding", e); } return doRequest(post); } private AuthenticationResult doRequest(HttpPost post) throws MessagingException, IOException { final HttpResponse response; response = mClient.execute(post); final int status = response.getStatusLine().getStatusCode(); if (status == HttpStatus.SC_OK) { return parseResponse(response); } else if (status == HttpStatus.SC_FORBIDDEN || status == HttpStatus.SC_UNAUTHORIZED || status == HttpStatus.SC_BAD_REQUEST) { LogUtils.e(TAG, "HTTP Authentication error getting oauth tokens %d", status); // This is fatal, and we probably should clear our tokens after this. throw new AuthenticationFailedException("Auth error getting auth token"); } else { LogUtils.e(TAG, "HTTP Error %d getting oauth tokens", status); // This is probably a transient error, we can try again later. throw new MessagingException("HTTPError " + status + " getting oauth token"); } } private AuthenticationResult parseResponse(HttpResponse response) throws IOException, MessagingException { final BufferedReader reader = new BufferedReader(new InputStreamReader( response.getEntity().getContent(), "UTF-8")); final StringBuilder builder = new StringBuilder(); for (String line = null; (line = reader.readLine()) != null;) { builder.append(line).append("\n"); } try { final JSONObject jsonResult = new JSONObject(builder.toString()); final String accessToken = jsonResult.getString(JSON_ACCESS_TOKEN); final String expiresIn = jsonResult.getString(JSON_EXPIRES_IN); final String refreshToken; if (jsonResult.has(JSON_REFRESH_TOKEN)) { refreshToken = jsonResult.getString(JSON_REFRESH_TOKEN); } else { refreshToken = null; } try { int expiresInSeconds = Integer.valueOf(expiresIn); return new AuthenticationResult(accessToken, refreshToken, expiresInSeconds); } catch (NumberFormatException e) { LogUtils.e(TAG, e, "Invalid expiration %s", expiresIn); // This indicates a server error, we can try again later. throw new MessagingException("Invalid number format", e); } } catch (JSONException e) { LogUtils.e(TAG, e, "Invalid JSON"); // This indicates a server error, we can try again later. throw new MessagingException("Invalid JSON", e); } } }