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