Skip to content

Commit

Permalink
Updated the authentication to use Google Play Services (better suppor…
Browse files Browse the repository at this point in the history
…ted, apparently), also updated Google API client libraries to 1.19.
  • Loading branch information
codeka committed Oct 17, 2014
1 parent 5af397b commit 513f9bb
Show file tree
Hide file tree
Showing 14 changed files with 73 additions and 156 deletions.
3 changes: 0 additions & 3 deletions client/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@
we need to enumerate the accounts so you can log on. -->
<uses-permission android:name="android.permission.GET_ACCOUNTS" />

<!-- Permission to request auth tokens from the AccountManager - again, so you can log on. -->
<uses-permission android:name="android.permission.USE_CREDENTIALS" />

<!-- Permission that allows us to access in-app billing -->
<uses-permission android:name="com.android.vending.BILLING" />

Expand Down
2 changes: 2 additions & 0 deletions client/src/au/com/codeka/warworlds/BaseActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public class BaseActivity extends ActionBarActivity {

private long mForegroundStartTimeMs;

public static final int AUTH_RECOVERY_REQUEST = 2397;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Expand Down
20 changes: 18 additions & 2 deletions client/src/au/com/codeka/warworlds/ServerGreeter.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import au.com.codeka.warworlds.model.RealmManager;

import com.google.android.gcm.GCMRegistrar;
import com.google.android.gms.auth.UserRecoverableAuthException;

/**
* This class is used to make sure we're said "Hello" to the server and that we've got our
Expand Down Expand Up @@ -127,7 +128,8 @@ private static void sayHello(final Activity activity, final int retries) {

PreferenceManager.setDefaultValues(activity, R.xml.global_options, false);

int memoryClass = ((ActivityManager) activity.getSystemService(BaseActivity.ACTIVITY_SERVICE)).getMemoryClass();
int memoryClass = ((ActivityManager) activity
.getSystemService(BaseActivity.ACTIVITY_SERVICE)).getMemoryClass();
if (memoryClass < 40) {
// on low memory devices, we want to make sure the background detail is always BLACK
// this is a bit of a hack, but should stop the worst of the memory issues (I hope!)
Expand Down Expand Up @@ -155,6 +157,7 @@ private static void sayHello(final Activity activity, final int retries) {
private boolean mWasEmpireReset;
private String mResetReason;
private String mToastMessage;
private Intent mIntent;

@Override
protected String doInBackground() {
Expand All @@ -172,11 +175,15 @@ public void run() {
Realm realm = RealmContext.i.getCurrentRealm();
if (!realm.getAuthenticator().isAuthenticated()) {
try {
log.info("Not authenticated, re-authenticating.");
realm.getAuthenticator().authenticate(activity, realm);
} catch (ApiException e) {
mErrorOccured = true;
// if it wasn't a network error, it probably means we need to re-auth.
mNeedsReAuthenticate = !e.networkError();
if (e.getCause() instanceof UserRecoverableAuthException) {
mIntent = ((UserRecoverableAuthException) e.getCause()).getIntent();
}
if (e.getServerErrorCode() > 0 && e.getServerErrorMessage() != null) {
mToastMessage = e.getServerErrorMessage();
}
Expand Down Expand Up @@ -269,6 +276,11 @@ public void run() {
"data connection.</p>";
mErrorOccured = true;
mNeedsReAuthenticate = false;
} else if (e.getCause() instanceof UserRecoverableAuthException) {
message = "<p class=\"error\">Authentication failed.</p>";
mErrorOccured = true;
mNeedsReAuthenticate = true;
mIntent = ((UserRecoverableAuthException) e.getCause()).getIntent();
} else if (e.getHttpStatusLine().getStatusCode() == 403) {
// if it's an authentication problem, we'll want to re-authenticate
message = "<p class=\"error\">Authentication failed.</p>";
Expand Down Expand Up @@ -318,7 +330,11 @@ protected void onComplete(String result) {
editor.remove("AccountName");
editor.commit();

mServerGreeting.mIntent = new Intent(activity, AccountsActivity.class);
if (mIntent != null) {
mServerGreeting.mIntent = mIntent;
} else {
mServerGreeting.mIntent = new Intent(activity, AccountsActivity.class);
}
mHelloComplete = true;
} else {
synchronized(mHelloWatchers) {
Expand Down
97 changes: 29 additions & 68 deletions client/src/au/com/codeka/warworlds/api/Authenticator.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
package au.com.codeka.warworlds.api;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

import org.apache.http.HttpEntity;
import org.eclipse.jdt.annotation.Nullable;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerFuture;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import au.com.codeka.common.Log;
import au.com.codeka.warworlds.App;
import au.com.codeka.warworlds.BaseActivity;
import au.com.codeka.warworlds.Util;
import au.com.codeka.warworlds.model.Realm;

import com.google.android.gms.auth.GoogleAuthException;
import com.google.android.gms.auth.GoogleAuthUtil;
import com.google.android.gms.auth.UserRecoverableAuthException;

public class Authenticator {
private static final Log log = new Log("Authenticator");
private String mAuthCookie;
Expand All @@ -44,10 +48,10 @@ public String getAuthCookie() {
* @return A cookie we can use in subsequent calls to the server.
* @throws ApiException
*/
public void authenticate(Activity activity, Realm realm) throws ApiException {
public boolean authenticate(@Nullable Activity activity, Realm realm) throws ApiException {
// make sure we don't try to authenticate WHILE WE'RE AUTHENTICATING...
if (mAuthenticating) {
return;
return true;
}
mAuthenticating = true;

Expand All @@ -58,76 +62,33 @@ public void authenticate(Activity activity, Realm realm) throws ApiException {
throw new ApiException("No account has been selected yet!");
}

AccountManager accountManager = AccountManager.get(activity == null ? App.i : activity);
Context context = App.i;
log.info("(re-)authenticating \"%s\" to realm %s...", accountName, realm.getDisplayName());
String cookie = null;

try {
Account[] accts = accountManager.getAccountsByType("com.google");
for (Account acct : accts) {
final Account account = acct;
if (account.name.equals(accountName)) {
log.info("Account found, fetching authentication token");

final String scope = "oauth2:email";
String authToken = getAuthToken(accountManager, account, activity, scope);
accountManager.invalidateAuthToken(account.type, authToken);
authToken = getAuthToken(accountManager, account, activity, scope);
cookie = getCookie(authToken, realm);
}
final String scope = "oauth2:email";
String authToken = GoogleAuthUtil.getToken(context, accountName, scope);
cookie = getCookie(authToken, realm);
log.info("Authentication successful.");
} catch (UserRecoverableAuthException e) {
// If it's a 'recoverable' exception, we need to start the given intent and then try
// again.
if (activity == null) {
throw new ApiException("Cannot retry, no activity given.", e);
}
Intent intent = e.getIntent();
activity.startActivityForResult(intent, BaseActivity.AUTH_RECOVERY_REQUEST);
log.warning("Got UserRecoverableAuthException, TODO");
} catch (GoogleAuthException e) {
throw new ApiException(e);
} catch (IOException e) {
throw new ApiException(e);
} finally {
mAuthenticating = false;
}
mAuthCookie = cookie;
}

private String getAuthToken(AccountManager accountManager, Account account, Activity activity, String scope) {
if (activity != null) {
log.info("Fetching auth token with activity");
AccountManagerFuture<Bundle>future = accountManager.getAuthToken(
account, scope, new Bundle(), activity, null, null);
return getAuthToken(future);
} else {
log.info("Fetching auth token withOUT activity");
return getAuthTokenNoActivity(accountManager, account);
}
}

@SuppressLint("NewApi") // getAuthToken for >= ICE_CREAM_SANDWICH
@SuppressWarnings("deprecation") // getAuthToken for < ICE_CREAM_SANDWICH
private String getAuthTokenNoActivity(AccountManager accountManager, Account account) {
// this version will notify the user of failures, but won't pop up the
// authentication page. Useful when running in the background.
AccountManagerFuture<Bundle> future;

int sdk = android.os.Build.VERSION.SDK_INT;
if (sdk < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
future = accountManager.getAuthToken(account, "ah", false,
null, null);
} else {
future = accountManager.getAuthToken(account, "ah", new Bundle(),
false, null, null);
}
return getAuthToken(future);
}

/**
* Gets the auth token from the given \c AccountmanagerFuture.
*/
private String getAuthToken(AccountManagerFuture<Bundle> future) {
try {
Bundle authTokenBundle = future.getResult();
if (authTokenBundle == null || authTokenBundle.get(AccountManager.KEY_AUTHTOKEN) == null) {
return null;
}

String authToken = authTokenBundle.get(AccountManager.KEY_AUTHTOKEN).toString();
return authToken;
} catch (Exception e) {
log.error("Error fetching auth token", e);
return null;
}
return true;
}

/**
Expand Down
Binary file modified control-field/out/control-field.jar
Binary file not shown.
Binary file modified planet-render/out/planet-render.jar
Binary file not shown.
10 changes: 6 additions & 4 deletions server/.classpath
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@
<classpathentry kind="lib" path="/home/dean/software/wwmmo/code/jetty/lib/jetty-webapp-9.0.1.v20130408.jar"/>
<classpathentry kind="lib" path="/home/dean/software/wwmmo/code/jetty/lib/jetty-xml-9.0.1.v20130408.jar"/>
<classpathentry kind="lib" path="/home/dean/software/wwmmo/code/jetty/lib/servlet-api-3.0.jar"/>
<classpathentry kind="lib" path="libs/google-api-client-1.18.0-rc.jar"/>
<classpathentry kind="lib" path="libs/google-http-client-1.18.0-rc.jar"/>
<classpathentry kind="lib" path="libs/google-http-client-gson-1.18.0-rc.jar"/>
<classpathentry kind="lib" path="libs/google-oauth-client-1.18.0-rc.jar"/>
<classpathentry kind="lib" path="libs/gson-2.1.jar"/>
<classpathentry kind="lib" path="libs/postgresql-9.3-1101.jdbc41.jar"/>
<classpathentry kind="lib" path="libs/proguard.jar"/>
<classpathentry kind="lib" path="libs/guava-18.0.jar"/>
<classpathentry kind="lib" path="libs/HikariCP-java6-2.1.1-SNAPSHOT.jar"/>
<classpathentry kind="lib" path="libs/google-api-client-1.19.0.jar"/>
<classpathentry kind="lib" path="libs/google-api-client-gson-1.19.0.jar"/>
<classpathentry kind="lib" path="libs/google-http-client-1.19.0.jar"/>
<classpathentry kind="lib" path="libs/google-http-client-gson-1.19.0.jar"/>
<classpathentry kind="lib" path="libs/google-oauth-client-1.19.0.jar"/>
<classpathentry kind="lib" path="libs/google-api-services-oauth2-v2-rev80-1.19.0.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>
Binary file not shown.
Binary file added server/libs/google-api-client-gson-1.19.0.jar
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,61 +1,27 @@
package au.com.codeka.warworlds.server.handlers;

import java.io.IOException;
import java.io.InputStreamReader;

import javax.servlet.http.Cookie;

import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.client.util.Key;
import com.google.common.collect.Lists;
import com.google.common.io.CharStreams;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

import au.com.codeka.common.Log;
import au.com.codeka.warworlds.server.RequestException;
import au.com.codeka.warworlds.server.RequestHandler;
import au.com.codeka.warworlds.server.ctrl.LoginController;

import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.services.oauth2.Oauth2;
import com.google.api.services.oauth2.model.Tokeninfo;

public class LoginHandler extends RequestHandler {
private final Log log = new Log("RequestHandler");

private static final String API_KEY = "AIzaSyANXsZc4CaLMXDBJDClO9uAnzuYysQJ0zw";
private static final String CLIENT_ID = "1021675369049-cb56ts1l657ghi3ml2cg07t7c8t3dta9.apps.googleusercontent.com";
private static final String CLIENT_SECRET = "6JQyk9rSsLHDJlenrsCWh4wv";

private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
private static final JsonFactory JSON_FACTORY = new GsonFactory();
private static final HttpRequestFactory REQUEST_FACTORY =
HTTP_TRANSPORT.createRequestFactory(new HttpRequestInitializer() {
@Override
public void initialize(HttpRequest request) {
request.setParser(new JsonObjectParser(JSON_FACTORY));
}
});

public static class PlusUrl extends GenericUrl {
public PlusUrl(String encodedUrl) {
super(encodedUrl);
}

@Key
private final String key = API_KEY;

public static PlusUrl me() {
return new PlusUrl("https://www.googleapis.com/plus/v1/people/me");
}
}

@Override
protected void get() throws RequestException {
Expand All @@ -70,47 +36,20 @@ protected void get() throws RequestException {
return;
}

String authToken = getRequest().getParameter("authToken");
GoogleCredential credential = new GoogleCredential();
Oauth2 oauth2= new Oauth2.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential)
.setApplicationName("wwmmo").build();

// make a quick request to Google's Authorization endpoint to make sure the token they've
// given us is actually valid (this'll also give us the actual email address)

JsonObject json;
try {
HttpRequest request = REQUEST_FACTORY.buildGetRequest(PlusUrl.me());
request.getHeaders().set("Authorization", Lists.newArrayList("OAuth " + authToken));
request.getHeaders().set("client_id", CLIENT_ID);
request.getHeaders().set("client_secret", CLIENT_SECRET);
Tokeninfo tokeninfo = oauth2.tokeninfo()
.setAccessToken(getRequest().getParameter("authToken")).execute();

HttpResponse response = request.execute();
String encoding = "utf-8";
if (response.getContentCharset() != null) {
encoding = response.getContentCharset().name();
}
InputStreamReader isr = new InputStreamReader(response.getContent(), encoding);
log.info("About me: %s", tokeninfo.toPrettyString());
String emailAddr = tokeninfo.getEmail();
String impersonateUser = getRequest().getParameter("impersonate");
String cookie = new LoginController().generateCookie(emailAddr, false, impersonateUser);

if (response.getStatusCode() != 200) {
String responseBody = CharStreams.toString(isr);
log.warning("%d %s\r\n%s", response.getStatusCode(),
response.getStatusMessage(), responseBody);
throw new RequestException(response.getStatusCode(), "Error fetching user details.");
}
json = new JsonParser().parse(isr).getAsJsonObject();
} catch (IOException e) {
throw new RequestException(e);
}

if (!json.has("emails")) {
throw new RequestException(500, "'emails' key expected.");
}

JsonArray emailsArray = json.get("emails").getAsJsonArray();
String emailAddr = emailsArray.get(0).getAsJsonObject().get("value").getAsString();
String impersonateUser = getRequest().getParameter("impersonate");
String cookie = new LoginController().generateCookie(emailAddr, false, impersonateUser);

getResponse().setContentType("text/plain");
try {
getResponse().setContentType("text/plain");
getResponse().getWriter().write(cookie);
} catch (IOException e) {
throw new RequestException(e);
Expand Down

0 comments on commit 513f9bb

Please sign in to comment.