package com.zegoggles.smssync.auth;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import javax.net.ssl.HttpsURLConnection;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import static com.zegoggles.smssync.App.TAG;
/**
* https://developers.google.com/identity/protocols/OAuth2UserAgent
*/
public class OAuth2Client {
private static final String AUTH_URL = "https://accounts.google.com/o/oauth2/auth";
public static final String TOKEN_URL = "https://www.googleapis.com/oauth2/v3/token";
/**
* For installed applications, use a value of code, indicating that the Google OAuth 2.0 endpoint should return an authorization code.
*/
private static final String RESPONSE_TYPE = "response_type";
/**
* Identifies the client that is making the request.
* The value passed in this parameter must exactly match the value shown in the Google Developers Console.
*/
private static final String CLIENT_ID = "client_id";
/**
* Determines where the response is sent.
* The value of this parameter must exactly match one of the values that appear in the
* Credentials page in the Google Developers Console (including the http or https scheme, case, and trailing slash).
* You may choose between <code>urn:ietf:wg:oauth:2.0:oob</code>,
* <code>urn:ietf:wg:oauth:2.0:oob:auto</code>, or an <code>http://localhost</code> port.
* For more details, see <a href="https://developers.google.com/identity/protocols/OAuth2InstalledApp#choosingredirecturi">Choosing a redirect URI</a>.
*/
private static final String REDIRECT_URI = "redirect_uri";
/**
* Space-delimited set of scope strings.
*
* Identifies the Google API access that your application is requesting.
* The values passed in this parameter inform the consent screen that is shown to the user. There may be an inverse
* relationship between the number of permissions requested and the likelihood
* of obtaining user consent.
*/
private static final String SCOPE = "scope";
/**
* Provides any state information that might be useful to your application upon receipt
* of the response. The Google Authorization Server roundtrips this parameter, so your application receives
* the same value it sent. Possible uses include redirecting the user to the
* correct resource in your site, nonces, and cross-site-request-forgery mitigations.
*/
private static final String STATE = "state";
/**
* When your application knows which user it is trying to authenticate, it can
* provide this parameter as a hint to the Authentication Server.
* Passing this hint will either pre-fill the email box on the sign-in form or select the proper
* multi-login session, thereby simplifying the login flow.
*/
private static final String LOGIN_HINT = "login_hint";
/**
* If this is provided with the value true, and the authorization request is granted, the authorization will include
* any previous authorizations granted to this user/application combination
* for other scopes; see Incremental Authorization.
*/
private static final String INCLUDE_GRANTED_SCOPES = "include_granted_scopes";
// Scopes as defined in http://code.google.com/apis/accounts/docs/OAuth.html#prepScope
private static final String GMAIL_SCOPE = "https://mail.google.com/";
private static final String CONTACTS_SCOPE = "https://www.google.com/m8/feeds/";
private static final String DEFAULT_SCOPE = GMAIL_SCOPE + " " + CONTACTS_SCOPE;
private static final String REDIRECT_OOB = "urn:ietf:wg:oauth:2.0:oob";
private static final String CONTACTS_URL = "https://www.google.com/m8/feeds/contacts/default/thin?max-results=1";
/**
* As defined in the OAuth 2.0 specification, this field must contain a value of authorization_code.
*/
private static final String GRANT_TYPE = "grant_type";
private static final String AUTHORIZATION_CODE = "authorization_code";
/**
* The authorization code returned from the initial request.
*/
private static final String CODE = "code";
private static final String REFRESH_TOKEN = "refresh_token";
private static final String ERROR = "error";
private final String clientId;
public OAuth2Client(String clientId) {
if (TextUtils.isEmpty(clientId)) {
throw new IllegalArgumentException("empty client id");
}
this.clientId = clientId;
}
public Uri requestUrl() {
return Uri.parse(AUTH_URL)
.buildUpon()
.appendQueryParameter(SCOPE, DEFAULT_SCOPE)
.appendQueryParameter(CLIENT_ID, clientId)
.appendQueryParameter(RESPONSE_TYPE, "code")
.appendQueryParameter(REDIRECT_URI, REDIRECT_OOB).build();
}
public OAuth2Token getToken(String code) throws IOException {
HttpsURLConnection connection = postTokenEndpoint(getAccessTokenPostData(code));
final int responseCode = connection.getResponseCode();
if (responseCode == HttpsURLConnection.HTTP_OK) {
OAuth2Token token = parseResponse(connection.getInputStream());
String username = getUsernameFromContacts(token);
Log.d(TAG, "got token " + token.getTokenForLogging()+ ", username="+username);
return new OAuth2Token(token.accessToken, token.tokenType, token.refreshToken, token.expiresIn, username);
} else {
Log.e(TAG, "error: " + responseCode);
throw new IOException("Invalid response from server:" + responseCode);
}
}
public OAuth2Token refreshToken(String refreshToken) throws IOException {
HttpsURLConnection connection = postTokenEndpoint(getRefreshTokenPostData(refreshToken));
final int responseCode = connection.getResponseCode();
if (responseCode == HttpsURLConnection.HTTP_OK) {
return parseResponse(connection.getInputStream());
} else {
Log.e(TAG, "error: " + responseCode);
throw new IOException("Invalid response from server:" + responseCode);
}
}
private OAuth2Token parseResponse(InputStream inputStream) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[8192];
int n;
while ((n = inputStream.read(buffer)) != -1) {
bos.write(buffer, 0, n);
}
inputStream.close();
return OAuth2Token.fromJSON(bos.toString("UTF-8"));
}
private HttpsURLConnection postTokenEndpoint(String payload) throws IOException {
HttpsURLConnection connection = (HttpsURLConnection) new URL(TOKEN_URL).openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
final OutputStream os = connection.getOutputStream();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
writer.write(payload);
writer.flush();
writer.close();
os.close();
return connection;
}
private String getAccessTokenPostData(String code) {
final Uri uri = Uri.parse(TOKEN_URL)
.buildUpon()
.appendQueryParameter(GRANT_TYPE, AUTHORIZATION_CODE)
.appendQueryParameter(REDIRECT_URI, REDIRECT_OOB)
.appendQueryParameter(CLIENT_ID, clientId)
.appendQueryParameter(CODE, code)
.build();
return uri.getEncodedQuery();
}
private String getRefreshTokenPostData(String refreshToken) {
final Uri uri = Uri.parse(TOKEN_URL)
.buildUpon()
.appendQueryParameter(GRANT_TYPE, REFRESH_TOKEN)
.appendQueryParameter(REFRESH_TOKEN, refreshToken)
.appendQueryParameter(CLIENT_ID, clientId)
.build();
return uri.getEncodedQuery();
}
// Retrieves the google email account address using the contacts API
private String getUsernameFromContacts(OAuth2Token token) {
try {
HttpsURLConnection connection = (HttpsURLConnection) new URL(CONTACTS_URL).openConnection();
connection.addRequestProperty("Authorization", "Bearer "+token.accessToken);
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
final InputStream inputStream = connection.getInputStream();
String email = extractEmail(inputStream);
inputStream.close();
return email;
} else {
Log.w(TAG, String.format("unexpected server response: %d (%s)",
connection.getResponseCode(), connection.getResponseMessage()));
return null;
}
} catch (SAXException e) {
Log.e(TAG, ERROR, e);
return null;
} catch (IOException e) {
Log.e(TAG, ERROR, e);
return null;
} catch (ParserConfigurationException e) {
Log.e(TAG, ERROR, e);
return null;
}
}
private String extractEmail(InputStream inputStream) throws ParserConfigurationException, SAXException, IOException {
final XMLReader xmlReader = SAXParserFactory.newInstance().newSAXParser().getXMLReader();
final FeedHandler feedHandler = new FeedHandler();
xmlReader.setContentHandler(feedHandler);
xmlReader.parse(new InputSource(inputStream));
return feedHandler.getEmail();
}
public String getClientId() {
return clientId;
}
private static class FeedHandler extends DefaultHandler {
private static final String EMAIL = "email";
private static final String AUTHOR = "author";
private final StringBuilder email = new StringBuilder();
private boolean inEmail;
private boolean inAuthor;
@Override
public void startElement(String uri, String localName, String qName, Attributes atts) {
inEmail = EMAIL.equals(qName);
if (AUTHOR.equals(qName)) {
inAuthor = true;
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if (inAuthor && AUTHOR.equals(qName)) {
inAuthor = false;
}
}
@Override
public void characters(char[] c, int start, int length) {
if (inAuthor && inEmail) {
email.append(c, start, length);
}
}
@Override
public void error(SAXParseException e) throws SAXException {
Log.e(TAG, "error during parsing", e);
}
@Override public void warning(SAXParseException e) throws SAXException {
Log.w(TAG, "error during parsing", e);
}
public String getEmail() {
return email.toString().trim();
}
}
}