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