summaryrefslogtreecommitdiffstats
path: root/provider_src/com/android/email/mail/internet/OAuthAuthenticator.java
blob: c3e255b5cce4f8dee52f875c55e164fda259c8b3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
package com.android.email.mail.internet;

import android.content.Context;
import android.text.format.DateUtils;

import com.android.email.activity.setup.AccountSettingsUtils;
import com.android.emailcommon.Logging;
import com.android.emailcommon.VendorPolicyLoader.OAuthProvider;
import com.android.emailcommon.mail.AuthenticationFailedException;
import com.android.emailcommon.mail.MessagingException;
import com.android.mail.utils.LogUtils;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

public class OAuthAuthenticator {
    private static final String TAG = Logging.LOG_TAG;

    public static final String OAUTH_REQUEST_CODE = "code";
    public static final String OAUTH_REQUEST_REFRESH_TOKEN = "refresh_token";
    public static final String OAUTH_REQUEST_CLIENT_ID = "client_id";
    public static final String OAUTH_REQUEST_CLIENT_SECRET = "client_secret";
    public static final String OAUTH_REQUEST_REDIRECT_URI = "redirect_uri";
    public static final String OAUTH_REQUEST_GRANT_TYPE = "grant_type";

    public static final String JSON_ACCESS_TOKEN = "access_token";
    public static final String JSON_REFRESH_TOKEN = "refresh_token";
    public static final String JSON_EXPIRES_IN = "expires_in";


    private static final long CONNECTION_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS;
    private static final long COMMAND_TIMEOUT = 30 * DateUtils.SECOND_IN_MILLIS;

    final HttpClient mClient;

    public static class AuthenticationResult {
        public AuthenticationResult(final String accessToken, final String refreshToken,
                final int expiresInSeconds) {
            mAccessToken = accessToken;
            mRefreshToken = refreshToken;
            mExpiresInSeconds = expiresInSeconds;
        }

        @Override
        public String toString() {
            return "result access " + (mAccessToken==null?"null":"[REDACTED]") +
                    " refresh " + (mRefreshToken==null?"null":"[REDACTED]") +
                    " expiresInSeconds " + mExpiresInSeconds;
        }

        public final String mAccessToken;
        public final String mRefreshToken;
        public final int mExpiresInSeconds;
    }

    public OAuthAuthenticator() {
        final HttpParams params = new BasicHttpParams();
        HttpConnectionParams.setConnectionTimeout(params, (int)(CONNECTION_TIMEOUT));
        HttpConnectionParams.setSoTimeout(params, (int)(COMMAND_TIMEOUT));
        HttpConnectionParams.setSocketBufferSize(params, 8192);
        mClient = new DefaultHttpClient(params);
    }

    public AuthenticationResult requestAccess(final Context context, final String providerId,
            final String code) throws MessagingException, IOException {
        final OAuthProvider provider = AccountSettingsUtils.findOAuthProvider(context, providerId);
        if (provider == null) {
            LogUtils.e(TAG, "invalid provider %s", providerId);
            // This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed
            // exception, this will at least give the user a heads up to set up their account again.
            throw new AuthenticationFailedException("Invalid provider" + providerId);
        }

        final HttpPost post = new HttpPost(provider.tokenEndpoint);
        post.setHeader("Content-Type", "application/x-www-form-urlencoded");
        final List<BasicNameValuePair> nvp = new ArrayList<BasicNameValuePair>();
        nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CODE, code));
        nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_ID, provider.clientId));
        nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_SECRET, provider.clientSecret));
        nvp.add(new BasicNameValuePair(OAUTH_REQUEST_REDIRECT_URI, provider.redirectUri));
        nvp.add(new BasicNameValuePair(OAUTH_REQUEST_GRANT_TYPE, "authorization_code"));
        try {
            post.setEntity(new UrlEncodedFormEntity(nvp));
        } catch (UnsupportedEncodingException e) {
            LogUtils.e(TAG, e, "unsupported encoding");
            // This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed
            // exception, this will at least give the user a heads up to set up their account again.
            throw new AuthenticationFailedException("Unsupported encoding", e);
        }

        return doRequest(post);
    }

    public AuthenticationResult requestRefresh(final Context context, final String providerId,
            final String refreshToken) throws MessagingException, IOException {
        final OAuthProvider provider = AccountSettingsUtils.findOAuthProvider(context, providerId);
        if (provider == null) {
            LogUtils.e(TAG, "invalid provider %s", providerId);
            // This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed
            // exception, this will at least give the user a heads up to set up their account again.
            throw new AuthenticationFailedException("Invalid provider" + providerId);
        }
        final HttpPost post = new HttpPost(provider.refreshEndpoint);
        post.setHeader("Content-Type", "application/x-www-form-urlencoded");
        final List<BasicNameValuePair> nvp = new ArrayList<BasicNameValuePair>();
        nvp.add(new BasicNameValuePair(OAUTH_REQUEST_REFRESH_TOKEN, refreshToken));
        nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_ID, provider.clientId));
        nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_SECRET, provider.clientSecret));
        nvp.add(new BasicNameValuePair(OAUTH_REQUEST_GRANT_TYPE, "refresh_token"));
        try {
            post.setEntity(new UrlEncodedFormEntity(nvp));
        } catch (UnsupportedEncodingException e) {
            LogUtils.e(TAG, e, "unsupported encoding");
            // This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed
            // exception, this will at least give the user a heads up to set up their account again.
            throw new AuthenticationFailedException("Unsuported encoding", e);
        }

        return doRequest(post);
    }

    private AuthenticationResult doRequest(HttpPost post) throws MessagingException,
            IOException {
        final HttpResponse response;
        response = mClient.execute(post);
        final int status = response.getStatusLine().getStatusCode();
        if (status == HttpStatus.SC_OK) {
            return parseResponse(response);
        } else if (status == HttpStatus.SC_FORBIDDEN || status == HttpStatus.SC_UNAUTHORIZED ||
                status == HttpStatus.SC_BAD_REQUEST) {
            LogUtils.e(TAG, "HTTP Authentication error getting oauth tokens %d", status);
            // This is fatal, and we probably should clear our tokens after this.
            throw new AuthenticationFailedException("Auth error getting auth token");
        } else {
            LogUtils.e(TAG, "HTTP Error %d getting oauth tokens", status);
            // This is probably a transient error, we can try again later.
            throw new MessagingException("HTTPError " + status + " getting oauth token");
        }
    }

    private AuthenticationResult parseResponse(HttpResponse response) throws IOException,
            MessagingException {
        final BufferedReader reader = new BufferedReader(new InputStreamReader(
                response.getEntity().getContent(), "UTF-8"));
        final StringBuilder builder = new StringBuilder();
        for (String line = null; (line = reader.readLine()) != null;) {
            builder.append(line).append("\n");
        }
        try {
            final JSONObject jsonResult = new JSONObject(builder.toString());
            final String accessToken = jsonResult.getString(JSON_ACCESS_TOKEN);
            final String expiresIn = jsonResult.getString(JSON_EXPIRES_IN);
            final String refreshToken;
            if (jsonResult.has(JSON_REFRESH_TOKEN)) {
                refreshToken = jsonResult.getString(JSON_REFRESH_TOKEN);
            } else {
                refreshToken = null;
            }
            try {
                int expiresInSeconds = Integer.valueOf(expiresIn);
                return new AuthenticationResult(accessToken, refreshToken, expiresInSeconds);
            } catch (NumberFormatException e) {
                LogUtils.e(TAG, e, "Invalid expiration %s", expiresIn);
                // This indicates a server error, we can try again later.
                throw new MessagingException("Invalid number format", e);
            }
        } catch (JSONException e) {
            LogUtils.e(TAG, e, "Invalid JSON");
            // This indicates a server error, we can try again later.
            throw new MessagingException("Invalid JSON", e);
        }
    }
}