Webservice credentials - OpenID/Android AccountManager? - android

I'm building a webservice and would like to use the user's google account credentials.
The service runs on GAE and will have a web client and an Android native client.
This is my first attempt of something like this and I've been reading about OpenID and the Android AccountManager library.
I'm still not sure what are my options in terms of storing the users in my Datastore. What Identifier should I use ? Is it possible to use OpenID on a native Android application ?
Any help and/or pointers would be appreciated. Thanks.

We had a similar requirements on the last project: GAE backend with GWT frontend and Android/iPhone clients. Also, we did not want to store user credentials.
So we choose to use OpenID, which is unfortunately a Web standard and does not play well with mobile devices, but is doable.
On the GAE side we simply enabled federated login which gave us OpenID.
On mobile devices, when user needs to login we present to them a list op OpenID authenticators (Google, Yahoo, etc..). Then we open a native browser (not embedded browser) and direct user to chosen OpenID authentication site. The upside is that user's browser usually already has username/pass remembered, so this step just requires user to press one button.
This is all pretty straightforward. Now here is the tricky part:
After user confirms login, OpenID redirects back to our GAE return url (you need to provide this url when request is made). On this url we create a custom URL, for example:
yourappname://usrname#XXXYYYZZZ
where XXXYYYZZZZ is auth token. We get this token from the return page where it's stored as an ACSID cookie: we used some JSP to read this cookie and wrap it into above custom URL.
Then we register our Android and iPhone apps to handle the yourappname:// URLs, so that when user cliskc this link, our app is invoked and the link is passed to it. We extract user name and token from this link and we use it in REST requests to the GAE backend.
If you have any more questions I'd gladly update this post.
Update:
The user session cookie on production AppEngine is named ACSID, while on development AppEngine server it's named dev_appserver_login.

I spent about a week to find a suitable and modern looking way for this - without web browser and by using android account manager.
If you would like to use Google account and AccountManager to identify the user you can:
Get his token to Google Contacts (auth token type is "cp") through AccountManager on background thread:
public String getUserToken(Activity activity)
{
AccountManager accountManager = AccountManager.get(activity);
AccountManagerFuture<Bundle> amf = accountManager.getAuthTokenByFeatures("com.google", "cp", null, activity, Bundle.EMPTY, Bundle.EMPTY, null, null );
Bundle bundle = null;
try {
bundle = amf.getResult();
String name = (String) bundle.get(AccountManager.KEY_ACCOUNT_NAME);
String type = (String) bundle.get(AccountManager.KEY_ACCOUNT_TYPE);
String token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
return token;
} catch (OperationCanceledException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (AuthenticatorException e) {
e.printStackTrace();
}
return null;
}
Pass received UserToken to the server over secured channel.
Validate the token at the server by google using gdata library (Google Data API library):
public String getUserId(String token)
{
ContactsService contactsService = new ContactsService("Taxi");
contactsService.setUserToken(token);
IFeed feed = null;
try {
feed = contactsService.getFeed(new URL("https://www.google.com/m8/feeds/contacts/default/full?max-results=10000"), ContactFeed.class);
} catch (IOException e) {
e.printStackTrace();
} catch (ServiceException e) {
e.printStackTrace();
} catch (NullPointerException e) {
e.printStackTrace();
}
if (feed == null)
return null;
String externalId = feed.getId();
IPerson person = feed.getAuthors().get(0);
String email = person.getEmail();
String name = person.getName();
String nameLang = person.getNameLang();
return externalId;
}
Google token can expire (usually after an hour), so if you failed to validate the token at the server, you must send response back to client, invalidate the token and get a new one. Use account manager to invalidate the token:
public void invalidateUserToken(Context context, String token)
{
AccountManager accountManager = AccountManager.get(context);
accountManager.invalidateAuthToken("com.google", token);
}

I think this blog post does exactly what you want. It worked for me. Both of the solutions posted here are viable and clever, but I think this does it exactly how the asker was asking.
Essentially, you're just getting an authToken using the "ah" scope, and passing it to the right webpage to get the ACSID cookie that will let you access any AppEngine page that uses UserService for authentication.

http://developer.android.com/search.html#q=AccountManager&t=0
http://developer.android.com/resources/samples/SampleSyncAdapter/index.html
at the bottom of this page you will find all needed code
best regards

Related

Oauth2 refresh token renewal in Android

I'm developing an Android app which is using Oauth2 tokens to get authorization in order to access secured resources. I'm using a third party platform as the authentication server (using OpenId Connect). Basically my problem is that I want to deal with an expired refresh token.
Current scenario
I've got a NetUtils class which acts like a singleton and manages all my requests using a secured rest template. That rest template injects the required Authorization header for each request using a request wrapper. The NetUtils class deals whith tokens and timeouts, saving them in user preferences and refreshing them when it's needed.
However, the problem comes when the refresh token itself expires. As I'm using the Authorization code flow, I need to open a WebView and redirect the user to the login page, but I notice it when the NetUtils class determinates the refresh token has expired. Ideally, the app would launch a WebView, the user would login again and the stored request would be executed. Here it is my code to refresh the access token:
private AccessToken refreshToken(String idClient, String clientSecret, AccessToken accessToken) {
MultiValueMap<String, String> clientAuthenticationForm = new LinkedMultiValueMap<>();
clientAuthenticationForm.add("grant_type", "refresh_token");
clientAuthenticationForm.add("refresh_token", accessToken.getRefreshToken());
clientAuthenticationForm.add("client_id", idClient);
clientAuthenticationForm.add("client_secret", clientSecret);
try {
long lastClientRefresh = mPrefs.getLong(Preferences.LAST_LOGIN_TIME, Long.MIN_VALUE);
boolean refreshTokenExpired = lastClientRefresh
+ TimeUnit.SECONDS.toMillis(accessToken.getRefreshExpiresIn()) < System
.currentTimeMillis();
if (!refreshTokenExpired) {
return regularRestTemplate
.postForEntity(tokenUrl(), clientAuthenticationForm, AccessToken.class)
.getBody();
}else{
//How to cope with this?
return null;
}
} catch (Exception ex) {
Log.e(TAG, ex.getMessage(), ex);
throw ex;
}
}
Other choice
Other choice would be to make the refresh token long lived and refresh it each time the app starts, for example. I have to mention that client_id and client_secret are currently being hardcoded in the app (although client credential grants are not meant to be enabled in production, so there's still the need to provide a username and password to retrieve a token).
What would be the best practice here?
I think I can't suggest you how to code in Java, but I also had some troubles with refresh_token while creating application in PHP so maybe my thoughts will help you with something.
At first I was looking for refresh_token which never expires (like in Google API) so I can even hardcode it and use whenever I want to create a new access_token. Anyway it's really hard to do in oAuth2. So I have found a interesting look on this problem here:
Why do access tokens expire?
It showed me a bit other way to work with refresh_token. I have set on my oAuth service that it generates and returns a new refresh_token everytime I use refresh_token to obtain a new access_token. That part helped me most:
https://bshaffer.github.io/oauth2-server-php-docs/grant-types/refresh-token/
And there we got something like:
$server = new OAuth2\Server($storage, array(
'always_issue_new_refresh_token' => true, // this part
'refresh_token_lifetime' => 2419200,
));
In this case I have a long live refresh_token which I can store somewhere and when I need it I will use it to get a new access_token, but response will also provide me a new refresh_token which I can store again and use it later for obtaining a new access_token.
So in your case I think the best way is to keep generating refresh_token everytime you ask for access_token with refresh_token. And if user will not use your APP for longer time, I think he should authorize himself again.

Store Google+ friends list on backend server

I've integrated Google+ sign-in in my Android application. Now I wan't to get the user's friend list and store it on the server. I can't get the friends list on the client app and send it to the server since the data can be easily tampered. So I thought about generating an access token using the following code and send it to the server, which the server will then use to query the Google+ API and get the user's friends.
String accountName = Plus.AccountApi.getAccountName(mGoogleApiClient);
Account account = new Account(accountName, GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE);
String scope = "oauth2:" + Constants.SCOPE_PLUS_LOGIN + " " + Constants.SCOPE_EMAIL;
try {
String accessToken = GoogleAuthUtil.getToken(getApplicationContext(), account, scope);
} catch (IOException | GoogleAuthException e) {
e.printStackTrace();
}
But is sending the access token to the server directly using https safe enough? Cause if the token is compromised then any third party can use it to steal the user's personal information.
Or is there any better way to fetch and store the signed in user's friend list on the server?
1) Always transfer access_tokens over HTTPS.
2) Don't build any way to get access_tokens from your server. Make sure the server only supports sending access_tokens to it.
3) access_tokens expire after one hour so the window for abuse is limited.

Getting Google one-time authorization code

I am having trouble getting a one-time authorization code from Google. I am attempting to get the authorization code from an Android client so that I can send it to my Rails backend (web client).
In my Google Cloud Developer Console I have an application with two Client IDs:
Client ID for web application (for my rails backend)
Client ID for Android application (for my android client). The SHA1 used is from ~/.android/debug.keystore
Suppose the Web Application Client ID is 12345.apps.googleusercontent.com
Suppose the Android Client ID is 67890.apps.googleusercontent.com
This is some of my code:
private final static String WEB_CLIENT_ID = "12345.apps.googleusercontent.com";
private final static String GOOGLE_CALENDAR_API_SCOPE = "audience:server:client_id:" + WEB_CLIENT_ID;
private void getAndUseAuthToken(final String email) {
AsyncTask task = new AsyncTask<String, Void, String>() {
#Override
protected String doInBackground(String... emails) {
try {
return GoogleAuthUtil.getToken(AddExternalCalendarsActivity.this, emails[0], GOOGLE_CALENDAR_API_SCOPE);
} catch (UserRecoverableAuthException e) {
startActivityForResult(e.getIntent(), IntentConstants.REQUEST_GOOGLE_AUTHORIZATION);
} catch (IOException e) {
e.printStackTrace();
} catch (GoogleAuthException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
#Override
protected void onPostExecute(String authToken) {
if (authToken != null) {
saveTokenAndGetCalendars(email, authToken);
}
}
};
String[] emails = new String[1];
emails[0] = email;
task.execute(emails);
}
Some additional notes
I am hitting the GoogleAuthException and receiving "Unknown" as the message detail
I'm unable to add additional members in the permissions of the Google Cloud Console for this project - when adding a new member a popup appears with "Server Error. Whoops! Our bad.". I have sent feedback to Google twice.
I'm referring to this documentation. Notice the quote below. By "fixed", are they saying that I do not need to prepend audience:server:client_id in my GOOGLE_CALENDAR_API_SCOPE variable? I've tried both with and without and still getting the same GoogleAuthException.
In this situation, the Android app can call the
GoogleAuthUtil.getToken() method on behalf of any of the Google
accounts on the device, and with a scope argument value of
audience:server:client_id:9414861317621.apps.googleusercontent.com.
The prefix audience:server:client_id: is fixed, and the rest of the
scope string is the client ID of the web component.
If I use this scope, I can authenticate with google from device. However, the documentation I've read suggests that I need to use the server web client id (which is in the same google console project as the android client id) in the scope in order for the server to hit the google api on behalf of the user who authorized it on the android client:
private final static String GOOGLE_CALENDAR_API_SCOPE = "oauth2:https://www.googleapis.com/auth/calendar";
UPDATE 1
I originally added in answer:
The reason for my first problem - I am hitting the GoogleAuthException and receiving "Unknown" as the message detail - was a mistake I made when adding the android client id in the cloud console. The SHA1 was correct but I did not type the package name correctly. I used com.company.app when my android package is actually com.company.android.app.
The code in the original question works fine. Just make sure you have all the necessary clients in your Google Cloud Console project.
But another problem still exists. When I send the one-time authorization token returned from GoogleAuthUtil.getToken() to the Rails backend, and then try to exchange it for an access_token and refresh_token, I get the follow:
Signet::AuthorizationError:
Authorization failed. Server message:
{
"error" : "invalid_grant"
}
This google documentation and several SO posts suggests that I need to set access_type=offline. But I think that is when you are requesting the one-time authorization code and offline access from a Web Server. I'm trying to request the one-time authorization code from an Android client and send it to the web server.
Is this possible with GoogleAuthUtil.getToken()?
UPDATE 2
Google Plus login must be in the scope even if you're only trying to access the calendar:
private final static String GOOGLE_CALENDAR_API_SCOPE = "oauth2:server:client_id:" + WEB_CLIENT_ID + ":api_scope:https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth/calendar";
This SO post was helpful. Also, Google's Cross Client identity documentation does state:
[Note: This policy in being rolled out gradually. For the moment, when access tokens are involved, it only applies when the requested scopes include https://www.googleapis.com/auth/plus.login.]
I'll summarize in an answer if the token exchange works on Rails backend.
Two things solved this for me:
Make sure the Android and Web Client IDs are setup in correctly in the same Google Cloud Console project.
Use the correct scope. Plus login is required even if you're only accessing the calendar api:
// the id of the web server that is exchanging the auth code for access and refresh tokens
private final static String WEB_CLIENT_ID = "12345.apps.googleusercontent.com";
private final static String GOOGLE_CALENDAR_API_SCOPE = "oauth2:server:client_id:" + WEB_CLIENT_ID + ":api_scope:https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth/calendar";
Here's what I've done:
final String authToken = GoogleAuthUtil.getTokenWithNotification (this.context, account.name, "oauth2:" + AuthUtils.profileScope, new Bundle (),
Contract.authority, new Bundle ());
But you plain getToken should work the same for a foreground activity.
Take this token, send it to the server in an a way you can use it like an HTTP header (over HTTPS). As long as the server scope is a subset of the scope used to acquire the token, you shouldn't have a problem. The server uses the server id and the android client uses the android client id.
You should set your scope to either:
oauth2:https://www.googleapis.com/auth/calendar
or
oauth2:https://www.googleapis.com/auth/calendar.readonly
See https://developers.google.com/google-apps/calendar/auth
Edit: OK I see what you are trying to do. The scope value is a space separated list so you would likely need to append audience:server:client_id: to your scope like:
"oauth2:https://www.googleapis.com/auth/calendar audience:server:client_id:12345-your-web-component-client-id"

Obtaining a basic google auth-token from AccountManager

I want to obtain a Google Authtoken from the AccountManager that I can send to my Webservice (not hosted on App Engine) to authenticate the User (I just need the email address and eventually his name, if no permission is required for this).
What do I have to use for the "authTokenType" Paramter of the "getAuthToken" method?
And which google Api do I have to use to get the Users Email?
This is doable using OpenID Connect, however it's sort of experimental, so details could change in the future. If you get an OAuth token for the 'https://www.googleapis.com/auth/userinfo.email' or 'https://www.googleapis.com/auth/userinfo.profile' scope you can use it to get user info from https://www.googleapis.com/oauth2/v1/userinfo (including email). Of course the user needs to authorize this.
You should theoretically be able to get the token from AcccountManager using the "oauth2:https://www.googleapis.com/auth/userinfo.profile" as the token type, but that doesn't appear to work on my device (Galaxy Nexus with stock 4.0.4). Since getting a token via the AccountManager doesn't work (at least for now), the only reliable way is to use a WebView and get one via the browser as described here: https://developers.google.com/accounts/docs/MobileApps
There is a demo web app here that does this: https://oauthssodemo.appspot.com
(late) Update: Google Play Services has been released and it is the preferred way to get an OAuth token. It should be available on all devices with Android 2.2 and later. Getting a profile token does work with it, in fact they use it in the demo app
I have had problems with this as well, since I was not able to find anything like a reference. Perhaps this can help you (code copied from an Android example on using the account manager):
Somewhere in an event handler of your Android app, issue a request for an auth token to get the user's email address in Android:
_accountMgr = AccountManager.get(this);
Account [] accounts = _accountMgr.getAccounts();
Account account = accounts[0]; // For me this is Google, still need to figure out how to get it by name.
_accountMgr.getAuthToken(account, AUTH_TOKEN_TYPE, false, new GetAuthTokenCallback(), null);
In the callback, extract the access token:
private class GetAuthTokenCallback implements AccountManagerCallback<Bundle> {
public void run(AccountManagerFuture<Bundle> result) {
Bundle bundle;
try {
bundle = result.getResult();
final String access_token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
// store token somewhere you can supply it to your web server.
} catch (Exception e) {
// do something here.
}
}
}
Make some request to your web server, supplying the access token.
On the web server, validate the access token and obtain the email address:
curl -d 'access_token=<this is the token the app sent you>' https://www.googleapis.com/oauth2/v1/tokeninfo
You should get something like this:
{
"issued_to": "<something>.apps.googleusercontent.com",
"audience": "<something>.apps.googleusercontent.com",
"scope": "https://www.googleapis.com/auth/userinfo.email",
"expires_in": 3562,
"email": "<users email address>",
"verified_email": true,
"access_type": "online"
}
or if something went wrong:
{
"error": "invalid_token",
"error_description": "Bad Request"
}
You can get the User's name with the Google+ People API. (It will not provide the user's email address).
If this is OK, you can use "Know who you are on Google" as the authTokenType.
There is a sample application provided by Google that demonstrates how to use the AndroidAccountManager in conjunction with the Google+ APIs.
Link: http://code.google.com/p/google-plus-java-starter/source/browse/#hg%2Fandroid

Android AccountManager, Oauth, getAccessToken() does nothing except ask for password

After spending roughly two days on this, I'm getting a little rattled. Although by now, having chosen OAuth2 over OpenID, I'm pretty well versed on the difference between Authenticate and Authorize.
I want my android app to provide several ways to authenticate users, one of them is google accounts, and later also facebook and twitter accounts. I'm trying to use the AccountManager class to get an OAuth access token to (for now) just verify the user's email address. The goal is that if the user already has a google account saved on the android device, they can authorize my app once, MAYBE even without typing a password, and never have to login again from their android.
I decided to use Google's own AccountManager as it promised to handle much of this natively in the Android framework, without even opening a browser window. I am using the library / build target for google APIs version 7 (Android 2.1), the first level that supports AccountManager.
I have tried this two different ways, one using AccountManager.getAuthTokenByFeatures() where you do not specify an Account object, and the other using getAuthToken() where you do specify such an object.
In each case, the call completes (as I expect it to) and the application displays an authorization dialog asking if I want to authorize the app. So far, so good. If I refuse, the program throws the exception that I expect. If I accept, a "Google sign-in" dialog appears asking me for the password to the account. Note that I already entered the password when I added the account to the device. If I type the password in the dialog, there is an "Authorizing" wait screen and then the same dialog re-appears. Oddly enough, the "Authorizing" wait screen seems to take a little bit longer if I type the CORRECT password. So it appears that I cannot get to the code path where I successfully obtain the token.
As tempting as it is to vent about google not being clear about what AUTH_TOKEN_TYPE is or the fact that the userinfo.email URL is undocumented, I would really just like to learn what I am doing wrong here and move past this.
Here is my code, I will monitor this and of course be happy to answer any questions. Right now I am going to work on getting a capture of the network traffic, to see if that provides any further insight into what is going wrong.
Here are images showing the auth screen (ok) and the password dialog (less ok)
http://imageshack.us/g/707/device20120609020618.png/
public void loginToGoogle() {
System.out.println("Starting");
AccountManagerFuture<Bundle> bundleFuture =
AccountManager.get(_activity).getAuthTokenByFeatures(
"com.google",
"https://www.googleapis.com/auth/userinfo.email",
null,
_activity,
null,
null,
new AccountManagerCallback<Bundle>() {
public void run(AccountManagerFuture<Bundle> future) {
Bundle bundle;
try {
bundle = future.getResult();
for (String s : bundle.keySet()) {
System.out.println("Found key: "+ s);
}
System.out.println(bundle.getString(AccountManager.KEY_ACCOUNT_NAME));
System.out.println(bundle.getString(AccountManager.KEY_AUTHTOKEN));
//Use Token
} catch (OperationCanceledException e) {
Log.e("e", e.getMessage(), e);
System.out.println("User appears to have denied auth request");
} catch (AuthenticatorException e) {
Log.e("e", e.getMessage(), e);
} catch (IOException e) {
Log.e("e", e.getMessage(), e);
}
}
},
null);
System.out.println("Done with AccountManager call");
}
Your issue most likely lies in the Auth Token Type you pass into the getAuthTokenByFeatures. Since you're using oauth2, you need to add it to beginning of your auth type:
String AUTH_TOKEN_TYPE = "oauth2:https://www.googleapis.com/auth/userinfo.email"
However, I'm not positive that the Auth Token Type you are trying to use is valid. I've only dealt with calendars and tasks myself. Hope this works out, if not, just let me know and I'll find you the right Auth type

Categories

Resources