1

I'm using google-api-client-android2-1.10.3-beta.jar.

I have some piece of code, which works half year ago. Just that recently, when I try to run them again, they no longer work. I'm getting the following exception.

com.google.api.client.googleapis.json.GoogleJsonResponseException: 403 Forbidden
{
  "code": 403,
  "errors": [
    {
      "domain": "global",
      "location": "Authorization",
      "locationType": "header",
      "message": "The authentication method used is not allowed.",
      "reason": "authenticationMethod"
    }
  ],
  "message": "The authentication method used is not allowed."
}

The exception is being thrown during

// request = service.files().list().setQ("title contains 'jstock-" + Utils.getJStockUUID().substring(0, 19) + "' and trashed = false");
FileList files = request.execute();

My piece of code, basically performing request

https://www.googleapis.com/drive/v2/files?q=title+contains+%27jstock-fe78440e-e0fe-4efb-%27+and+trashed+%3d+false, by using AuthToken from com.google.api.client.googleapis.extensions.android2.auth.GoogleAccountManager

I try the request in oAuth 2.0 playground. It works fine. However, I'm not sure why my code which works half year ago, doesn't work any more.

Here is the code which is used to get AuthToken

package com.jstock.cloud;

import java.io.IOException;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;

import com.google.api.client.googleapis.extensions.android2.auth.GoogleAccountManager;
import com.jstock.engine.Subject;
import com.jstock.gui.JStockApplication;
import com.jstock.gui.Preferences;
import com.jstock.gui.R;

// The code is picked from http://code.google.com/p/google-api-java-client/source/browse/tasks-android-sample/src/main/java/com/google/api/services/samples/tasks/android/TasksSample.java?repo=samples
public class LoginManager extends Subject<LoginManager, String> {
    public LoginManager(Activity activity) {
        // TODO : Need to revise API key.
        ClientCredentials.errorIfNotSpecified();

        // Noted, the passed in activity, should override the following method.
        // protected void onActivityResult(int requestCode, int resultCode, Intent data)
        // In the overriden method, it should call LoginManager's onActivityResult.
        this.activity = activity;

        dialog = new ProgressDialog(activity);
        dialog.setMessage(activity.getString(R.string.login));

        // TODO : #12 Decide usage of SharedPreferences over JStockOptions        
        authToken = Preferences.getAuthTOken();
        accountManager = new GoogleAccountManager(JStockApplication.instance());
        accountName = Preferences.getAccountName();
    }

    public void gotAccount() {
        Account account = accountManager.getAccountByName(accountName);
        if (account == null) {
            chooseAccount();
            return;
        }
        if (authToken != null) {
            onAuthToken(authToken);
            return;
        }
        dialog.show();
        accountManager.getAccountManager().getAuthToken(account, AUTH_TOKEN_TYPE, true, new AccountManagerCallback<Bundle>() {
            public void run(AccountManagerFuture<Bundle> future) {
                try {
                    Bundle bundle = future.getResult();
                    if (bundle.containsKey(AccountManager.KEY_INTENT)) {
                        Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT);
                        intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK);
                        activity.startActivityForResult(intent, REQUEST_AUTHENTICATE);
                        return;
                    } else if (bundle.containsKey(AccountManager.KEY_AUTHTOKEN)) {
                        final String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
                        setAuthToken(authToken);
                        onAuthToken(authToken);
                        return;
                    }
                } catch (Exception e) {
                    Log.e(TAG, e.getMessage(), e);
                }
                onAuthToken(null);
            }
        }, null);
    }

    public void chooseAccount() {
        dialog.show();
        accountManager.getAccountManager().getAuthTokenByFeatures(GoogleAccountManager.ACCOUNT_TYPE,
            AUTH_TOKEN_TYPE,
            null,
            activity,
            null,
            null,
            new AccountManagerCallback<Bundle>() {
                public void run(AccountManagerFuture<Bundle> future) {
                    Bundle bundle;
                    try {
                        bundle = future.getResult();
                        setAccountName(bundle.getString(AccountManager.KEY_ACCOUNT_NAME));
                        final String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
                        setAuthToken(authToken);
                        onAuthToken(authToken);
                        return;
                    } catch (OperationCanceledException e) {
                        // user canceled
                    } catch (AuthenticatorException e) {
                        Log.e(TAG, e.getMessage(), e);
                    } catch (IOException e) {
                        Log.e(TAG, e.getMessage(), e);
                    }
                    onAuthToken(null);
                }
            },
            null);
    }

    private void setAccountName(String accountName) {
        // TODO : #12 Decide usage of SharedPreferences over JStockOptions
        Preferences.setAccountName(accountName);
        this.accountName = accountName;
    }

    private void setAuthToken(String authToken) {
        // TODO : #12 Decide usage of SharedPreferences over JStockOptions
        Preferences.setAuthToken(authToken);
        this.authToken = authToken;
    }

    // To be consumed by this.activity.
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
        case REQUEST_AUTHENTICATE:
            if (resultCode == Activity.RESULT_OK) {
                gotAccount();
            } else {
                chooseAccount();
            }
            break;
        }
    }

    private void onAuthToken(final String authToken) {
        // Need to run on AsyncTask. This method is currently executed by main
        // thread, as we are using null in the last parameter of 
        // getAuthTokenByFeatures and getAuthToken. I try not to notify through
        // main thread, as I believe callers will most probably perform non-UI
        // task.
        new AsyncTask<Void, Void, Void>() {

            @Override
            protected void onPreExecute() {
                dialog.dismiss();
            } 

            @Override
            protected Void doInBackground(Void... arg0) {
                // authToken possible null, which indicates failure.
                LoginManager.this.notify(LoginManager.this, authToken);
                return null;
            }

            @Override
            protected void onPostExecute(Void result) {
            }            
        }.execute();
    }

    public void invalidateAuthToken() {
        // TODO : #12 Decide usage of SharedPreferences over JStockOptions        
        accountManager.invalidateAuthToken(authToken);
        authToken = null;
        Preferences.removeAuthToken();
    }

    private static final String AUTH_TOKEN_TYPE = "oauth2:https://www.googleapis.com/auth/drive";
    private static final int REQUEST_AUTHENTICATE = 0;
    private final GoogleAccountManager accountManager;
    private final Activity activity;
    private String accountName;
    private final ProgressDialog dialog;
    private String authToken;
    private static final String TAG = "LoginManager";
}

Here is the code which is used to perform API request

package com.jstock.cloud;

import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import android.util.Log;

import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.services.GoogleKeyInitializer;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.client.extensions.android2.AndroidHttp;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.Drive.Files;
import com.google.api.services.drive.model.File;
import com.google.api.services.drive.model.FileList;
import com.jstock.gui.Utils;

public class CloudFile {
    public final java.io.File file;
    public final long checksum;
    public final long date;
    public final int version;
    private CloudFile(java.io.File file, long checksum, long date, int version) {
        this.file = file;
        this.checksum = checksum;
        this.date = date;
        this.version = version;
    }

    public static CloudFile newInstance(java.io.File file, long checksum, long date, int version) {
        return new CloudFile(file, checksum, date, version);
    }

    public static CloudFile loadFromGoogleDrive(String authToken) {
        final HttpTransport transport = AndroidHttp.newCompatibleTransport();
        final JsonFactory jsonFactory = new GsonFactory();
        GoogleCredential credential = new GoogleCredential();
        Log.i("CHEOK", "authToken = " + authToken);
        credential.setAccessToken(authToken);
        Drive service = new Drive.Builder(transport, jsonFactory, credential)
            .setApplicationName(Utils.getApplicationName())
            .setJsonHttpRequestInitializer(new GoogleKeyInitializer(ClientCredentials.KEY))
            .build();
        List<File> files = retrieveAllJStockFiles(service);

        long checksum = 0;
        long date = 0;
        int version = 0;  
        File file = null;

        for (File _file : files) {
            file = _file;
            // Use title, not filename.
            final String title = file.getTitle();
            final String downloadUrl = file.getDownloadUrl();
            if (title == null || downloadUrl == null) {
                // Do we really need to perform null checking?
                continue;
            }

            // Retrieve checksum, date and version information from filename.
            final Matcher matcher = googleDocTitlePattern.matcher(title);
            String _checksum = null;
            String _date = null;
            String _version = null;
            if (matcher.find()){
                if (matcher.groupCount() == 3) {
                    _checksum = matcher.group(1);
                    _date = matcher.group(2);
                    _version = matcher.group(3);
                }
            }
            if (_checksum == null || _date == null || _version == null) {
                continue;
            }

            try {
                checksum = Long.parseLong(_checksum);
                date = Long.parseLong(_date);
                version = Integer.parseInt(_version);
            } catch (NumberFormatException ex) {
                Log.e(TAG, "", ex);
                continue;
            } 
        }   // for

        if (file == null) {
            return null;
        }

        final java.io.File temp = Utils.createTempFileOnExternalCacheDir(Utils.getJStockUUID(), ".zip");
        if (temp == null) {
            return null;
        }       
        // Delete temp file when program exits.
        temp.deleteOnExit();
        // Delete temp file when program exits.

        Map<String, String> headers = new LinkedHashMap<String, String>();
        headers.put("Authorization", "OAuth " + authToken);
        java.io.File downloadedFile = Utils.downloadAsTempFile(file.getDownloadUrl(), headers, temp);
        if (downloadedFile == null) {
            return null;
        }
        return CloudFile.newInstance(downloadedFile, checksum, date, version);
    }

    /**
     * Retrieve a list of File resources.
     *
     * @param service Drive API service instance.
     * @return List of File resources.
     */
    private static List<File> retrieveAllJStockFiles(Drive service) {
        List<File> result = new ArrayList<File>();
        Files.List request = null;
        try {
            // Not sure why. In oAuth 2 playground, I cannot use full JStock
            // UUID. Perhaps it places restriction on length of query.
            // https://www.googleapis.com/drive/v2/files?q=title+contains+%27jstock-fe78440e-e0fe-4efb-%27+and+trashed+%3d+false
            //request = service.files().list();
            request = service.files().list().setQ("title contains 'jstock-" + Utils.getJStockUUID().substring(0, 19) + "' and trashed = false");
        } catch (IOException e) {
            Log.e(TAG, "", e);
            return result;
        }

        do {
            try {
                FileList files = request.execute();

                result.addAll(files.getItems());
                request.setPageToken(files.getNextPageToken());
            } catch (IOException e) {
                Log.e(TAG, "", e);
                request.setPageToken(null);
            }
        } while (request.getPageToken() != null && request.getPageToken().length() > 0);

        return result;
    } 

    // http://stackoverflow.com/questions/1360113/is-java-regex-thread-safe
    private static final Pattern googleDocTitlePattern = Pattern.compile("jstock-" + Utils.getJStockUUID() +  "-checksum=([0-9]+)-date=([0-9]+)-version=([0-9]+)\\.zip", Pattern.CASE_INSENSITIVE);    
    private static final String TAG = "CloudFile";
}

Here is how we perform API call

// authToken is String, which obtained from LoginManager.
CloudFile.loadFromGoogleDrive(authToken); 

Any idea why the above exception is being thrown?

Cheok Yan Cheng
  • 47,586
  • 132
  • 466
  • 875
  • You must be using Google Play Services to make Drive API calls from an Android device. The security model of the Drive API requires it. Check the link below on how to get started with it. https://developers.google.com/drive/quickstart-android – Ali Afshar Jan 14 '13 at 21:36
  • After reading the article, seems like the `google-api-client-android2-1.10.3-beta.jar` I use is pretty outdated. I use the latest lib and performing step "Requires SHA-1 registered in console". Now, the thing works even without setJsonHttpRequestInitializer. Is this something introduced recently? – Cheok Yan Cheng Jan 16 '13 at 01:31

2 Answers2

0

Google Play Services are maybe a new approach to Google Drive authentication but it doesn't necessarily work better than the old approach.

According to this post How setup Google Drive Credentials within Android App? it works well only from SDK 11 (Honeycomb).

Not sure why is it, but I confirm it's true.

BTW, With the recent version of the drive SDK (google-api-services-drive-v2-rev97-1.16.0-rc.jar) it's necessary to initialize the drive object this way (setJsonHttpRequestInitializer doesn't exist anymore)

//request auth by AccountManager
AccountManager mgr = AccountManager.get(mContext);
String t = obtainToken(mgr.getAuthToken(getAccount(), "oauth2:"+DriveScopes.DRIVE, null, activity, callback, null));

if (t != null) {//refresh the token
    AccountManager.get(mContext).invalidateAuthToken("com.google", t);
    t = obtainToken(mgr.getAuthToken(getAccount(), "oauth2:"+DriveScopes.DRIVE, null, activity, callback, null));
}

//token is actually provided in the initializer
//credential = new GoogleCredential().setAccessToken(t);

final String token = t;

// prepare drive
if (drive == null) {
    drive = new Drive.Builder(new NetHttpTransport(), new JacksonFactory(), null)
    .setGoogleClientRequestInitializer(new GoogleClientRequestInitializer() {

        @Override
        public void initialize(AbstractGoogleClientRequest<?> request) throws IOException {
            DriveRequest driveRequest = (DriveRequest) request;
            driveRequest.setPrettyPrint(true);
            driveRequest.setKey(SIMPLE_API_ACCESS_KEY);
            driveRequest.setOauthToken(token);
        }
    })
    .build();
}
Community
  • 1
  • 1
Krzysztof
  • 328
  • 2
  • 9
0

I believe you are using an API Key to get the access token. Instead you need to pass your clientID/clientSecret when you request the token. That is if you are NOT using Google PlayServices.

To learn how follow the OAuth2.0 Authorization Code Flow and use the GoogleAuthorizationCodeFlow

stan0
  • 11,549
  • 6
  • 42
  • 59