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 {
AuthenticationResult(final String accessToken, final String refreshToken, final int expiresInSeconds)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
toString()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 
OAuthAuthenticator()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 
requestAccess(final Context context, final String providerId, final String code)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 
requestRefresh(final Context context, final String providerId, final String refreshToken)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 
doRequest(HttpPost post)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 
parseResponse(HttpResponse response)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