IllegalArgumentException When Accessing Google Sheets API - java

I'm attempting to read/write to Google Sheet (owned by me) via Google Sheets API v4. To get Credential object I'm using the following code:
private static Credential getCredentials(final NetHttpTransport HTTP_TRANSPORT) throws IOException {
// Load client secrets.
InputStream in = ResourceX.class.getResourceAsStream(CLIENT_SECRET_DIR);
GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(in));
System.out.println(clientSecrets.toPrettyString());
// When I print this ^^ it loads contents of secret key exactly as expected
// Build flow and trigger user authorization request.
Builder builder = new GoogleAuthorizationCodeFlow.Builder(HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, SCOPES); // error occurs on this line
builder.setDataStoreFactory(new FileDataStoreFactory(new java.io.File(CREDENTIALS_FOLDER)));
GoogleAuthorizationCodeFlow flow = builder.setAccessType("offline").build();
return new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()).authorize("user");
}
I understand that IllegalArgumentException means that somehow the argument clientSecrets is illegal to be passed as parameter however when I print out the contents of json file (see print statement in code above) the contents seems to match the expected contents (I cannot print the contents on SO for security reasons but nothing was changed in the file since I downloaded it from Google).
I found a similar question here but there are no answers addressing the core question.
What possibly have I missed that may cause this exception?
Stacktrace:
Exception in thread "main" java.lang.IllegalArgumentException
at com.google.api.client.repackaged.com.google.common.base.Preconditions.checkArgument(Preconditions.java:111)
at com.google.api.client.util.Preconditions.checkArgument(Preconditions.java:37)
at com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets.getDetails(GoogleClientSecrets.java:82)
at com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow$Builder.<init>(GoogleAuthorizationCodeFlow.java:195)
at global_projects.GoogleSheetsAPI.getCredentials(GoogleSheetsAPI.java:100)
at global_projects.GoogleSheetsAPI.updateSheetByOauth2(GoogleSheetsAPI.java:116)
at amz.Main.main(Main.java:78)
Update:
I think I found the problem. clientSecrets expects a json file from OAuth 2.0 client ID not a Service account key/jsonfile (as described in this link). However, since I am accessing my own spreadsheet is there a way to access it using a service account key/json file or must I use OAuth which would require me to provide consent to edit my own spreadsheet?

There are 2 ways of accessing your Google sheets file using the Google Client API.
Create service account credentials for which you must give edit access in your particular sheet to the email which Google assigned for the account.
OR
Create Oauth2 client id credentials to use with generating a credential object. This method will prompt you, by way of return URL, to provide authorisation in browser. Once you approve, you can query for a refresh token that enables you to refresh your access token without needing the browser flow approval again.
You are using method (1) and you're getting the IllegalArgumenException error because you have not given access to the email assigned to the service account.

Related

Drive API:Insufficient permissions when changing owner

I have made a program in Java that generates a spreadsheet filled with statistics for VEX teams. My whole goal is to essentially have a program be able to generate a spreadsheet and change that sheet's ownership to a specific email. The Sheets functionality works completely, being able to create and modify spreadsheet values easily(using the Sheets API). The problem is when I try to change the ownership of the file using the Drive API, I get an "Insufficient Permission" message when running Drive.permissions().create.
When using the APIs explorer and testing it out myself, I was able to transfer ownership of spreadsheets, but I cannot do it within Java.
Here is how I create my GoogleCredential:
private void setCredential() throws GeneralSecurityException, IOException {
//Create new transport
HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
//Build authenticated credential
GoogleCredential credential = new GoogleCredential.Builder()
.setTransport(httpTransport)
.setJsonFactory(jsonFactory)
.setClientSecrets(Constants.GOOGLE_CLIENT_ID,
Constants.GOOGLE_CLIENT_SECRET)
.build();
credential.setAccessToken(this.accessToken)
.setRefreshToken(Constants.GOOGLE_REFRESH_TOKEN);
//Set class field
this.credential = credential;
}
I noticed that I do not have any actual scopes here(which is weird, since this credential still works when the Sheets API does its work), in which I have looked around on how to input a proper scope here but found nothing. I don't know if this is why my program errors, or if it is for another reason.
This is how I call the method to transfer ownership:
private void transferOwnership(Drive driveService) throws IOException {
//Print message
System.out.printf("Transferring ownership to %s", this.usrEmail);
//Build request body
Permission body = new Permission()
.setRole("owner")
.setType("user")
.setEmailAddress(this.usrEmail);
//Execute Drive request
Permission permission = driveService.permissions().create(this.spreadsheetId, body)
.setFileId(this.spreadsheetId)
.setEmailMessage("Test - Replace with something")
.setSendNotificationEmail(true)
.setSupportsTeamDrives(true)
.setTransferOwnership(true)
.setUseDomainAdminAccess(false)
.setFields("emailAddress")
.execute();
//Print message
System.out.printf("Ownership successfully transferred to %s", this.usrEmail);
}
These are my questions:
Am I building my credential wrong? If so, how should I properly build it?
Do I need to make two different credentials?(one for Sheets API, one for Drive API)
How does my credential work fine with the Sheets API even though a scope isn't defined, but it does not work with the Drive API
EDIT: The way I use the APIs explorer to change ownership
EDIT 2: This is how I get my access token. As for the refresh token, I use the Oauth Playground from google.
public void setAccessToken() throws IOException, GeneralSecurityException{
//Create a token response using refresh token and oauth credentials
this.token_response = new GoogleRefreshTokenRequest(GoogleNetHttpTransport.newTrustedTransport(), JacksonFactory.getDefaultInstance(),
Constants.GOOGLE_REFRESH_TOKEN, Constants.GOOGLE_CLIENT_ID, Constants.GOOGLE_CLIENT_SECRET)
.execute();
//Set the access token
this.accessToken = token_response.getAccessToken();
}

(Android/Java) Google Sheets v4 isUserRecoverableError status: NEED_PERMISSION

I am currently trying to simply delete a row off my Google Spreadsheet using the Google Sheets v4 API.
Here is the code I am using:
private void deleteRow()
{
List<Request> requests = new ArrayList<>();
DeleteDimensionRequest deleteDimensionRequest = new DeleteDimensionRequest();
DimensionRange dimensionRange = new DimensionRange();
dimensionRange.setStartIndex(14);
dimensionRange.setEndIndex(15);
deleteDimensionRequest.setRange(dimensionRange);
requests.add(new Request()
.setDeleteDimension(deleteDimensionRequest)
);
BatchUpdateSpreadsheetRequest batchUpdateRequest = new BatchUpdateSpreadsheetRequest()
.setRequests(requests);
try
{
mService.spreadsheets().batchUpdate("Spreadsheet_ID", batchUpdateRequest).execute();
}
catch(IOException e)
{
e.printStackTrace();
}
}
The error this function gives me is:
08-14 15:47:10.818 26956-27285/com.xxx.xxxxx.xxxxxxxx
W/GoogleAuthUtil: isUserRecoverableError status: NEED_PERMISSION
In my other class file, I've already indicated the scopes of permissions including drive and spreadsheets.
Here is the picture of the error:
In the java quickstart...
public static Credential authorize() throws IOException {
// Load client secrets.
InputStream in =
SheetsQuickstart.class.getResourceAsStream("/client_secret.json");
GoogleClientSecrets clientSecrets =
GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(in));
// Build flow and trigger user authorization request.
GoogleAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow.Builder(
HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, SCOPES)
.setDataStoreFactory(DATA_STORE_FACTORY)
.setAccessType("offline")
.build();
Credential credential = new AuthorizationCodeInstalledApp(
flow, new LocalServerReceiver()).authorize("user");
System.out.println(
"Credentials saved to " + DATA_STORE_DIR.getAbsolutePath());
return credential;
}
Is this the sort of oath credential in addition to the one provided by the android quickstart that I need to include?
Based from the given JSON response, encountered error is due to insufficient authentication scope. You can try to check required OAuth 2.0 scope information for the Google Sheets API as given in Authorizing requests with OAuth 2.0.
Please note that requests to the Google Sheets API for non-public user data must be authorized by an authenticated user. Likewise, if an application needs to create spreadsheets, or otherwise manipulate their metadata, then the application must also request a Google Drive API scope.
Additionally, the solution given in this SO post - 403 Forbidden error when accessing Google Drive API downloadURL regarding error code 403 might also help.
The problem is likely that you are loaded stored credentials with the insufficient scopes, and you need to delete your stored credentials and authorize access again.
You code appears to be adapted from the Java quickstart, which requests a readonly scope. Trying to use that scope for a write operation would fail with this error.
Since there is not one single question relating to managing sheets with an android application, here is the way to request an additional OATH 2.0 from scratch, which took me a few hours to understand:
Strategy to get the right additional authorization...
https://developers.google.com/identity/protocols/OAuth2InstalledApp#libraries
Diagram:
All of these requests can be handled with some imports such as HttpUrlConnection... within AsyncTask.
The list of scopes...
https://developers.google.com/sheets/guides/authorizing

Upload videos to Youtube from my web server in Java

My goal is to upload videos that are uploaded to my web server to Youtube on my own channel, not the users' Youtube account (my web server is acting as a proxy).
I found the sample code for uploading video to Youtube here with the credential acquired this way. The problem that I have with this sample is that it writes to disk the credential, and it opens an http server. Since my web server can potentially have a lot of users uploading their videos concurrently, the credential file location has to be dynamic, and multiple binding to the same http port is not possible. Further more, after searching through other writing about uploading to Youtube, I think this approach is for users uploading to their Youtube account.
Could you share your experiences/code sample/solutions for my scenario? In short I am just trying to automate the process of me opening up Youtube dashboard, and uploading videos to a channel in my Youtube.
In general, starting at API V3, Google prefers OAuth2 over other mechanism, and uploading a video (or any other action that modifies user data) requires OAuth2.
Fortunately, there is a special kind of token called refresh token to the rescue. Refresh token does not expire like normal access token, and is used to generate normal access token when needed. So, I divided my application into 2 parts:
The 1st part is for generating refresh token, which is a Java desktop app, meant to be run by a user on a computer. See here for sample code from Google.
The 2nd part is is part of my web application, which uses a given refresh token to create a credential object.
Here is my implementation in Scala, which you can adapt to Java version easily:
For generating a refresh token, you should set the accessType to offline for the authorization flow. Note: if a token already exists on your system, it won't try to get new token, even if it does not have refresh token, so you also have to set approval prompt to force:
def authorize(dataStoreName: String, clientId: String, clientSecret: String): Credential = {
val builder = new GoogleAuthorizationCodeFlow.Builder(
HTTP_TRANSPORT,
JSON_FACTORY,
clientId,
clientSecret,
Seq(YouTubeScopes.YOUTUBE_UPLOAD)
)
val CREDENTIAL_DIRECTORY = s"${System.getProperty("user.home")}/.oauth-credentials"
val fileDataStoreFactory = new FileDataStoreFactory(new java.io.File(CREDENTIAL_DIRECTORY))
val dataStore: DataStore[StoredCredential] = fileDataStoreFactory.getDataStore(dataStoreName)
builder.setCredentialDataStore(dataStore).setAccessType("offline").setApprovalPrompt("force")
val flow = builder.build()
val localReceiver = new LocalServerReceiver.Builder().setPort(8000).build()
new AuthorizationCodeInstalledApp(flow, localReceiver).authorize("user")
}
val credential = authorize(dataStore, clientId, clientSecret)
val refreshToken = credential.getRefreshToken
For using the refresh token on the server, you can build a credential from a refresh token:
def getCredential = new GoogleCredential.Builder()
.setJsonFactory(JSON_FACTORY)
.setTransport(HTTP_TRANSPORT)
.setClientSecrets(clientId, clientSecret)
.build()
.setRefreshToken(refreshToken)
I have have bypassed the whole AuthorizationCodeInstalledApp authorize() method and created a new subclass which bypasses the jetty server implementation process.
The methods are as follows
getAuthorizationFromStorage : Get access token from stored credentials.
getAuthorizationFromGoogle : Get the authentication with the credentials from Google creates the url that will lead the user to the authentication page and creating a custom defined name-value pair in the state parameter. The value should be encoded with base64 encoder so we can receive the same code redirected from google after authentication.
saveAuthorizationFromGoogle : Save the credentials that we get from google.
Create the GoogleAuthorizationCodeFlow object from the credentialDatastorfrom the response received from the google after authentication.
Hit google to get the permanent refresh-token that can be used to get the accesstoken of the user any time .
Store the tokens like accesstoken and refreshtoken in the filename as userid
Checkout the code Implementation here

GoogleCredential Missing Access Token

I am using the service account model and Google's Java API to retrieve and modify users.
I am able to successfully create a GoogleCredential object using code similar to Google's example:
GoogleCredential googleCredential = new GoogleCredentialBuilder()
.setTransport(httpTransport)
.setJsonFactory(jsonFactory)
.setServiceAccountId(SERVICE_ACCOUNT_EMAIL)
.setServiceAccountUser(SERVICE_ACCOUNT_USER)
.setServiceAccountPrivateKeyFromP12File(P12_PRIVATE_KEY_FILE)
.setServiceAccountScopes(Collections.singleton(GLOBAL_USER_AND_ALIAS_SCOPE)
.build();
I see no mention in any examples that I have to explicitly create an access token, so I have been assuming that the above code takes care of that. Is that true?
After that, I successfully create an instance of Directory, then try to retrieve a specific user:
User user = new User();
user = directory.users().get(uid).execute();
That fails, throwing a NullPointerException.
When I inspect the GoogleCredential object right before the call to get the user object, it appears that it does not contain an access token:
accessToken = null
refreshToken = null
What am I missing?
How does one get the access token using the service account model?
Thanks in advance.
Where are you getting your accessToken? Try
credential.refreshToken();
accessToken = credential.getAccessToken();
Also, you should consider running your credentials in the Oauth2 Playground. If it works in the playground, then it's likely something wrong with your implementation.
Andy is correct. The examples for the Google Java API leave out this critical step. At runtime, the Google code throws a NullPointerException with no other details that would identify where it is occurring. Stepping through the debugger in Eclipse made it clear that the token was null in GoogleCredential.

GAE backend / task queue - how to pass user credentials in parameter?

In our GAE application we process user`s documents in google drive and as this process takes sometimes more then 30seconds we are getting deadline exception as it is GAE frontend instance.
we would like to use backend instance. Problem is how to pass credentials (com.google.api.client.auth.oauth2.Credentials) to be able to initialize Google drive API.
How to pass user credentials into GAE task queue and then to backend instance to be able to use them later when task will be running ?
com.google.api.client.auth.oauth2.Credentials is not serializable ...
Is there any way ?
You'll need to keep the inputs to the credential so you can re-create it in the backend handler.
This is either an authorization code, or the access token and refresh token that you got in exchange for the authorization code. They're all strings so should serialize readily.
If all that sounds unfamiliar, I'd be curious to understand how you got the Credential in the first place. Links to useful documentation below:
Exchange the authorization code for a refresh and access token
Retrieve and Use OAuth 2.0 Credentials
Notably, sample code in that last link specifically includes a method that you're expected to implement to squirrel away the access/refresh tokens:
/**
* Retrieved stored credentials for the provided user ID.
*
* #param userId User's ID.
* #return Stored Credential if found, {#code null} otherwise.
*/
static Credential getStoredCredentials(String userId) {
// TODO: Implement this method to work with your database. Instantiate a new
// Credential instance with stored accessToken and refreshToken.
throw new UnsupportedOperationException();
}
I'd suggest using AppEngineCredentialStore to store the access and refresh tokens. Please take a look at calendar-appengine-sample as an example usage. Here's an example code snippet from Utils.java:
static GoogleAuthorizationCodeFlow newFlow() throws IOException {
return new GoogleAuthorizationCodeFlow.Builder(
HTTP_TRANSPORT,
JSON_FACTORY,
getClientCredential(),
Collections.singleton(CalendarScopes.CALENDAR))
.setCredentialStore(new AppEngineCredentialStore())
.setAccessType("offline")
.build();
}
NOTE: I'm an owner of the google-api-java-client project.

Categories