1package com.android.email.mail.internet;
2
3import android.content.Context;
4import android.text.format.DateUtils;
5
6import com.android.email.activity.setup.AccountSettingsUtils;
7import com.android.emailcommon.Logging;
8import com.android.emailcommon.VendorPolicyLoader.OAuthProvider;
9import com.android.emailcommon.mail.AuthenticationFailedException;
10import com.android.emailcommon.mail.MessagingException;
11import com.android.mail.utils.LogUtils;
12
13import org.apache.http.HttpResponse;
14import org.apache.http.HttpStatus;
15import org.apache.http.client.HttpClient;
16import org.apache.http.client.entity.UrlEncodedFormEntity;
17import org.apache.http.client.methods.HttpPost;
18import org.apache.http.impl.client.DefaultHttpClient;
19import org.apache.http.message.BasicNameValuePair;
20import org.apache.http.params.BasicHttpParams;
21import org.apache.http.params.HttpConnectionParams;
22import org.apache.http.params.HttpParams;
23import org.json.JSONException;
24import org.json.JSONObject;
25
26import java.io.BufferedReader;
27import java.io.IOException;
28import java.io.InputStreamReader;
29import java.io.UnsupportedEncodingException;
30import java.util.ArrayList;
31import java.util.List;
32
33public 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