1e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonpackage com.android.email.mail.internet; 2e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 3e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport android.content.Context; 4e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport android.text.format.DateUtils; 5e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 6e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport com.android.email.activity.setup.AccountSettingsUtils; 7e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport com.android.emailcommon.Logging; 8e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport com.android.emailcommon.VendorPolicyLoader.OAuthProvider; 9e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport com.android.emailcommon.mail.AuthenticationFailedException; 10e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport com.android.emailcommon.mail.MessagingException; 11e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport com.android.mail.utils.LogUtils; 12e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 13e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport org.apache.http.HttpResponse; 14e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport org.apache.http.HttpStatus; 15e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport org.apache.http.client.HttpClient; 16e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport org.apache.http.client.entity.UrlEncodedFormEntity; 17e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport org.apache.http.client.methods.HttpPost; 18e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport org.apache.http.impl.client.DefaultHttpClient; 19e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport org.apache.http.message.BasicNameValuePair; 20e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport org.apache.http.params.BasicHttpParams; 21e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport org.apache.http.params.HttpConnectionParams; 22e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport org.apache.http.params.HttpParams; 23e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport org.json.JSONException; 24e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport org.json.JSONObject; 25e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 26e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport java.io.BufferedReader; 27e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport java.io.IOException; 28e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport java.io.InputStreamReader; 29e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport java.io.UnsupportedEncodingException; 30e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport java.util.ArrayList; 31e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonimport java.util.List; 32e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 33e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdonpublic class OAuthAuthenticator { 34e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon private static final String TAG = Logging.LOG_TAG; 35e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 36e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon public static final String OAUTH_REQUEST_CODE = "code"; 37e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon public static final String OAUTH_REQUEST_REFRESH_TOKEN = "refresh_token"; 38e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon public static final String OAUTH_REQUEST_CLIENT_ID = "client_id"; 39e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon public static final String OAUTH_REQUEST_CLIENT_SECRET = "client_secret"; 40e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon public static final String OAUTH_REQUEST_REDIRECT_URI = "redirect_uri"; 41e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon public static final String OAUTH_REQUEST_GRANT_TYPE = "grant_type"; 42e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 43e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon public static final String JSON_ACCESS_TOKEN = "access_token"; 44e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon public static final String JSON_REFRESH_TOKEN = "refresh_token"; 45e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon public static final String JSON_EXPIRES_IN = "expires_in"; 46e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 47e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 48e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon private static final long CONNECTION_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS; 49e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon private static final long COMMAND_TIMEOUT = 30 * DateUtils.SECOND_IN_MILLIS; 50e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 51e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon final HttpClient mClient; 52e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 53e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon public static class AuthenticationResult { 54e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon public AuthenticationResult(final String accessToken, final String refreshToken, 55e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon final int expiresInSeconds) { 56e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon mAccessToken = accessToken; 57e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon mRefreshToken = refreshToken; 58e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon mExpiresInSeconds = expiresInSeconds; 59e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } 60e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 61e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon @Override 62e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon public String toString() { 63e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon return "result access " + (mAccessToken==null?"null":"[REDACTED]") + 64e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon " refresh " + (mRefreshToken==null?"null":"[REDACTED]") + 65e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon " expiresInSeconds " + mExpiresInSeconds; 66e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } 67e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 68e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon public final String mAccessToken; 69e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon public final String mRefreshToken; 70e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon public final int mExpiresInSeconds; 71e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } 72e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 73e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon public OAuthAuthenticator() { 74e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon final HttpParams params = new BasicHttpParams(); 75e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon HttpConnectionParams.setConnectionTimeout(params, (int)(CONNECTION_TIMEOUT)); 76e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon HttpConnectionParams.setSoTimeout(params, (int)(COMMAND_TIMEOUT)); 77e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon HttpConnectionParams.setSocketBufferSize(params, 8192); 78e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon mClient = new DefaultHttpClient(params); 79e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } 80e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 81e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon public AuthenticationResult requestAccess(final Context context, final String providerId, 82e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon final String code) throws MessagingException, IOException { 83e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon final OAuthProvider provider = AccountSettingsUtils.findOAuthProvider(context, providerId); 84e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon if (provider == null) { 85e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon LogUtils.e(TAG, "invalid provider %s", providerId); 86e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon // This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed 87e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon // exception, this will at least give the user a heads up to set up their account again. 88e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon throw new AuthenticationFailedException("Invalid provider" + providerId); 89e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } 90e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 91e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon final HttpPost post = new HttpPost(provider.tokenEndpoint); 92e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon post.setHeader("Content-Type", "application/x-www-form-urlencoded"); 93e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon final List<BasicNameValuePair> nvp = new ArrayList<BasicNameValuePair>(); 94e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CODE, code)); 95e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_ID, provider.clientId)); 96e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_SECRET, provider.clientSecret)); 97e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon nvp.add(new BasicNameValuePair(OAUTH_REQUEST_REDIRECT_URI, provider.redirectUri)); 98e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon nvp.add(new BasicNameValuePair(OAUTH_REQUEST_GRANT_TYPE, "authorization_code")); 99e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon try { 100e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon post.setEntity(new UrlEncodedFormEntity(nvp)); 101e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } catch (UnsupportedEncodingException e) { 102e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon LogUtils.e(TAG, e, "unsupported encoding"); 103e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon // This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed 104e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon // exception, this will at least give the user a heads up to set up their account again. 105e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon throw new AuthenticationFailedException("Unsupported encoding", e); 106e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } 107e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 108e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon return doRequest(post); 109e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } 110e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 111e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon public AuthenticationResult requestRefresh(final Context context, final String providerId, 112e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon final String refreshToken) throws MessagingException, IOException { 113e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon final OAuthProvider provider = AccountSettingsUtils.findOAuthProvider(context, providerId); 114e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon if (provider == null) { 115e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon LogUtils.e(TAG, "invalid provider %s", providerId); 116e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon // This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed 117e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon // exception, this will at least give the user a heads up to set up their account again. 118e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon throw new AuthenticationFailedException("Invalid provider" + providerId); 119e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } 120e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon final HttpPost post = new HttpPost(provider.refreshEndpoint); 121e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon post.setHeader("Content-Type", "application/x-www-form-urlencoded"); 122e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon final List<BasicNameValuePair> nvp = new ArrayList<BasicNameValuePair>(); 123e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon nvp.add(new BasicNameValuePair(OAUTH_REQUEST_REFRESH_TOKEN, refreshToken)); 124e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_ID, provider.clientId)); 125e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_SECRET, provider.clientSecret)); 126e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon nvp.add(new BasicNameValuePair(OAUTH_REQUEST_GRANT_TYPE, "refresh_token")); 127e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon try { 128e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon post.setEntity(new UrlEncodedFormEntity(nvp)); 129e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } catch (UnsupportedEncodingException e) { 130e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon LogUtils.e(TAG, e, "unsupported encoding"); 131e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon // This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed 132e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon // exception, this will at least give the user a heads up to set up their account again. 133e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon throw new AuthenticationFailedException("Unsuported encoding", e); 134e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } 135e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 136e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon return doRequest(post); 137e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } 138e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 139e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon private AuthenticationResult doRequest(HttpPost post) throws MessagingException, 140e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon IOException { 141e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon final HttpResponse response; 142e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon response = mClient.execute(post); 143e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon final int status = response.getStatusLine().getStatusCode(); 144e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon if (status == HttpStatus.SC_OK) { 145e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon return parseResponse(response); 146e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } else if (status == HttpStatus.SC_FORBIDDEN || status == HttpStatus.SC_UNAUTHORIZED || 147e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon status == HttpStatus.SC_BAD_REQUEST) { 148e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon LogUtils.e(TAG, "HTTP Authentication error getting oauth tokens %d", status); 149e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon // This is fatal, and we probably should clear our tokens after this. 150e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon throw new AuthenticationFailedException("Auth error getting auth token"); 151e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } else { 152e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon LogUtils.e(TAG, "HTTP Error %d getting oauth tokens", status); 153e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon // This is probably a transient error, we can try again later. 154e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon throw new MessagingException("HTTPError " + status + " getting oauth token"); 155e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } 156e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } 157e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 158e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon private AuthenticationResult parseResponse(HttpResponse response) throws IOException, 159e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon MessagingException { 160e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon final BufferedReader reader = new BufferedReader(new InputStreamReader( 161e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon response.getEntity().getContent(), "UTF-8")); 162e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon final StringBuilder builder = new StringBuilder(); 163e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon for (String line = null; (line = reader.readLine()) != null;) { 164e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon builder.append(line).append("\n"); 165e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } 166e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon try { 167e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon final JSONObject jsonResult = new JSONObject(builder.toString()); 168e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon final String accessToken = jsonResult.getString(JSON_ACCESS_TOKEN); 169e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon final String expiresIn = jsonResult.getString(JSON_EXPIRES_IN); 170e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon final String refreshToken; 171e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon if (jsonResult.has(JSON_REFRESH_TOKEN)) { 172e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon refreshToken = jsonResult.getString(JSON_REFRESH_TOKEN); 173e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } else { 174e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon refreshToken = null; 175e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } 176e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon try { 177e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon int expiresInSeconds = Integer.valueOf(expiresIn); 178e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon return new AuthenticationResult(accessToken, refreshToken, expiresInSeconds); 179e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } catch (NumberFormatException e) { 180e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon LogUtils.e(TAG, e, "Invalid expiration %s", expiresIn); 181e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon // This indicates a server error, we can try again later. 182e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon throw new MessagingException("Invalid number format", e); 183e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } 184e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } catch (JSONException e) { 185e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon LogUtils.e(TAG, e, "Invalid JSON"); 186e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon // This indicates a server error, we can try again later. 187e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon throw new MessagingException("Invalid JSON", e); 188e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } 189e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon } 190e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon} 191e8eb6e659b5914eb7deab451c583e906010d0457Martin Hibdon 192