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