Gmail API suddenly stopped working with [Error: unauthorized_client] - google-api

Where I work we use Google Apps for Work. For the last 9 months we've been using the Gmail API (~2,000 requests per day) to pull in new emails for our support email accounts.
This is how we originally set it up:
Go to https://console.developers.google.com/project/
Click on the project (or create a new one)
Click on API's & Auth
Click on Credentials
Click on Create new Client ID
Click on Service account
Download a JWT (json) for the account.
Follow the node.js quickstart guide with an installed/native type token for the same account, and authorize it through the console. The JWT tokens did not work unless we did this step, once for each account.
We did this for each of our individual support email accounts to avoid having to turn on domain wide delegation for any of them in the admin console. We were then able to authenticate with the tokens using the officially supported npm library googleapis, similar to this:
var google = require('googleapis');
var jwtClient = new google.auth.JWT(
token.client_email,
null,
token.private_key,
['https://www.googleapis.com/auth/gmail.readonly'],
'supportemail#mycompany.com'
);
jwtClient.authorize(function(err, tokens) {
if (err) {
return cb(err);
}
var gmail = google.gmail('v1');
var requestOptions = {
auth: jwtClient,
userId: 'me',
id: messageId,
format: 'raw'
};
gmail.users.messages.get(requestOptions, function(err, response) {
if (err) {
return cb(err);
}
// do stuff with the response
});
});
Like I said, we used this for a long time and never had any issues. Yesterday around 10am MST every one of the accounts stopped being able to authenticate at the same time, with jwtClient.authorize() suddenly returning the error [Error: unauthorized_client].
I tried doing the same thing with a new token on a new service account (the web interface to get the token has changed quite a bit in the last 9 months), and it returns the same error.
The version of googleapis that we were using was 0.9.7, but we can't get JWT authentication to work on the newest version either.
We opened a ticket with the Google APIs support team, but the support person we spoke with had never read the Gmail API specs before and was ultimately unable to help us, so he redirected us here in order to get in touch with the API engineering support team.
We have noticed that authentication works if we enable the scope for domain wide delegation in the admin console, but we would prefer not to do that. We don't need to impersonate the accounts and would prefer to use an individual JWT for each account.

It turns out that the auth flow we were using was never supported, and probably was broken due to a bugfix on Google's part.
In the question comments #Brandon Jewett-Hall and #Steve Bazyl recommended that we use the installed app auth flow instead, as it allows for indefinite refreshing of access tokens and is supported.
More information about the different auth flows can be found in the Google API docs.

Related

Auth0 and Google API, access token expired, howto write files to Google Drive

I've written an online converter and integrated Auth0 to my website. What I'm trying to achieve is to auto-upload the converted file to the Google Drive of a logged in user. I set up Google oauth in Auth0 and everything seemed to work fine.
The problem is, Google's access_token expires after 60min and I don't have a refresh_token. Therefore, the user needs to log in via the Google Login-page again. That is not, what I want, because the user is in fact logged in way longer than just 60min on my site, but Google refuses API-calls (because the Google token expired).
I know I can request a refresh_token by setting access_type=offline but this will add the permission Have offline access. I don't want that, I just want to upload data to the user's Drive, if he clicked the convert button on my page. I don't want to ask the users for permissions I don't need. If I (as a user) would link my Google account on a similar page and the tool asks for offline access I wouldn't approve, to be honest - the permission sounds like the tool creator can do whatever he wants with your account whenever he wants... There are many tools out there that have write access to a user's Drive without asking for offline access and with one single login until the user revokes the permission. How is that done?
Is there a way to make Google API calls without asking for offline access and without forcing the user to approve the app (that is already approved by him) again and again every 60min?
Thanks in advance,
phlo
Is there a way to make Google API calls without asking for offline access and without forcing the user to approve the app (that is already approved by him) again and again every 60min?
Yes there are ways, but it depends on the specifics of your use case. For example, is your code in Java/php/etc running on a server, or is it JavaScript running in the browser?
Running your auth in the browser is probably the simplest solution as the Google library (https://developers.google.com/api-client-library/javascript/features/authentication) does all the work for you.
By asking for offline access you are requesting a refresh token. Google is going to tell the user that you are requesting offline access. You can request something without telling the user what they are authorizing.
No there is no way to request a refresh token without displaying that message. Nor is there a way for you to change the message it's a standard Google thing.
I found the solution!
Prerequirements
Enable Use Auth0 instead of the IdP to do Single Sign On in your client's Dashboard
Create a new Angular-route to handle the silent login callback (e.g. /sso)
Add
$rootScope.$on("$locationChangeStart", function() {
if ($location.path().indexOf("sso") == -1) {
authService.relogin(); //this is your own service
}
});
to your run-function and set the callbackURL in angularAuth0Provider.init() to your new Angular-route (<YOUR_DOMAIN>/sso). Add this URL to your accepted callbacks in the Auth0 dashboard - this won't end in an infinite loop, because the locationChangeStart-event won't call authService.relogin() for this route
Add $window.close(); to the controller of the Angular-route (/sso) to auto-close the popup
Authenticate the user via Auth0 and save the timestamp and the Auth0-access_token somewhere
On reload:
Check, if the Auth0-token is still valid in authService.relogin(). If not, the user has to login again either way. If the token is valid and the Google token is about to expire (check this with the saved timestamp to prevent unnecessary API calls) check for SSO-data and login silently, if present
/* ... */
if (validToken && googleExpired) {
angularAuth0.getSSOData(function (err, data) {
var lastUsedConnection = data.lastUsedConnection;
var connectionName = (_.isUndefined(lastUsedConnection) ? undefined : lastUsedConnection.name);
var isGoogle = (_.isUndefined(connectionName) ? false : connectionName == "google-oauth2");
if (!err && data.sso && isGoogle) {
authManager.authenticate();
localStorage.setItem("last-relogin", new Date().getTime());
angularAuth0.signin({
popup: true,
connection: data.lastUsedConnection.name
});
}
});
}
Now you will find a fresh Google access_token for this user (without asking for offline access)
Resources:
https://auth0.com/docs/quickstart/spa/angularjs/03-session-handling
https://auth0.com/docs/quickstart/spa/angularjs/11-sso

Running Google Picker with offline access oAuth token

What I am doing:
I am integrating Google Picker on my page. This will allow users to select files from their Google Drive to be used in the web app. In the app, people in a group share a common google drive (i.e. they all can select files from account example#email.com) which was created by group admin by his email address. When the admin signs-up for the account we do OAuth and get access_token with refresh_token against our app on google (with offline access enabled). I plan to use the access_token and refresh-token of the admin, on other group user's account when they try to use picker to select files.
What I have done:
I have integrated the Google Picker successfully in my app using the basic code provided in docs. Then to achieve what I wanted, I removed following code from the example code:
gapi.load('auth', {'callback': onAuthApiLoad});
and
function onAuthApiLoad() {
window.gapi.auth.authorize(
{
'client_id': clientId,
'scope': scope,
'immediate': false
},
handleAuthResult);
}
and
function handleAuthResult(authResult) {
if (authResult && !authResult.error) {
oauthToken = authResult.access_token;
createPicker();
}
}
and instead of .setOAuthToken(oauthToken) I pass refreshed access_token directly as string (I get that from my server with an ajax call).
.setOAuthToken("<access_token>")
But every time I call picker.setVisible(true); I get a screen in an iframe saying In order to select an item from your online storage, please sign in.
Problem:
Try to add sign in listener. Listeners provide a way to automatically respond to changes in the current user's Sign-In session. For example, after your startup method initializes the Google Sign-In auth2 object, you can set up listeners to respond to events like auth2.isSignedIn state changes, or changes in auth2.currentUser.
Validating the token might be a possibility before using the token each time but that might add a lot of extra overhead for a rare use-case each time we load the picker and when calling the API endpoints with a token after the re-authentication issue, there was no key about the token being invalid. You can validate a token by making a web service request to an endpoint on the Google Authorization Server and performing a string match on the results of that web service request.

Firebase 3.x - Token / Session Expiration

Does anyone know how long would it take for the token to expire? There no option now to set the token validity on the console.
Since May 2016 Firebase Authentication login sessions don't expire anymore. Instead they use a combination of long-lived account tokens and short-lived, auto-refreshed access/ID tokens to get the best of both worlds.
If you want to end a user's session, you can call signOut().
Its does expire. After one hour logged in the token id expire. If you try to verify sdk returns a error "Error: Firebase ID token has expired. Get a fresh token from your client app and try again. See https://firebase.google.com/docs/auth/server/verify-id-tokens for details on how to retrieve an ID token."
Is There such a way to change expiration time to Firebase token, not custom token.
Anybody that know how this really works.
For anyone that is still confused, it is all explained in great detail here
If your app includes a custom backend server, ID tokens can and should
be used to communicate securely with it. Instead of sending requests
with a user’s raw uid which can be easily spoofed by a malicious
client, send the user's ID token which can be verified via a Firebase
Admin SDK (or even a third-party JWT library if Firebase does not have
an Admin SDK in your language of choice). To facilitate this, the
modern client SDKs provide convenient methods for retrieving ID tokens
for the currently logged-in user. The Admin SDK ensures the ID token
is valid and returns the decoded token, which includes the uid of the
user it belongs to as well as any custom claims added to it.
If the above answer is still confusing to you,
This is what i did:
firebase.auth().onAuthStateChanged(async user => {
if (user) {
const lastSignInTime = new Date(user.metadata.lastSignInTime);
const lastSignInTimeTimeStamp = Math.round(lastSignInTime.getTime() / 1000);
const yesterdayTimeStamp = Math.round(new Date().getTime() / 1000) - (24 * 3600);
if(lastSignInTimeTimeStamp < yesterdayTimeStamp){
await firebase.auth().signOut()
this.setState({
loggedIn: false
});
return false;
}
this.setState({
loggedIn: true,
user
});
}
})

How do I authorise an app (web or installed) without user intervention?

Let's say that I have a web app ("mydriveapp") that needs to access Drive files in a background service. It will either own the files it is accessing, or be run in a Google Account with which the owner has shared the documents.
I understand that my app needs a refresh token, but I don't want to write the code to obtain that since I'll only ever do it once.
NB. This is NOT using a Service Account. The app will be run under a conventional Google account. Service Account is a valid approach in some situations. However the technique of using Oauth Playground to simulate the app can save a bunch of redundant effort, and applies to any APIs for which sharing to a Service Account is unsupported.
NB June 2022. It seems that Google have updated their verification requirements which adds additional steps (or negates the approach - depending on your point of view).
See recent comments for more detail
This can be done with the Oauth2 Playground at https://developers.google.com/oauthplayground
Steps:-
Create the Google Account (eg. my.drive.app#gmail.com) - Or skip this step if you are using an existing account.
Use the API console to register the mydriveapp (https://console.developers.google.com/apis/credentials/oauthclient?project=mydriveapp or just https://console.developers.google.com/apis/)
Create a new set of credentials. Credentials/Create Credentials/OAuth Client Id then select Web application
Include https://developers.google.com/oauthplayground as a valid redirect URI
Note the client ID (web app) and Client Secret
Login as my.drive.app#gmail.com
Go to Oauth2 playground
In Settings (gear icon), set
OAuth flow: Server-side
Access type: Offline
Use your own OAuth credentials: TICK
Client Id and Client Secret: from step 5
Click Step 1 and choose Drive API v3 https://www.googleapis.com/auth/drive (having said that, this technique also works for any of the Google APIs listed)
Click Authorize APIs. You will be prompted to choose your Google account and confirm access
Click Step 2 and "Exchange authorization code for tokens"
Copy the returned Refresh token and paste it into your app, source code or in to some form of storage from where your app can retrieve it.
Your app can now run unattended, and use the Refresh Token as described https://developers.google.com/accounts/docs/OAuth2WebServer#offline to obtain an Access Token.
NB. Be aware that the refresh token can be expired by Google which will mean that you need to repeat steps 5 onwards to get a new refresh token. The symptom of this will be a Invalid Grant returned when you try to use the refresh token.
NB2. This technique works well if you want a web app which access your own (and only your own) Drive account, without bothering to write the authorization code which would only ever be run once. Just skip step 1, and replace "my.drive.app" with your own email address in step 6. make sure you are aware of the security implications if the Refresh Token gets stolen.
See Woody's comment below where he links to this Google video https://www.youtube.com/watch?v=hfWe1gPCnzc
.
.
.
Here is a quick JavaScript routine that shows how to use the Refresh Token from the OAuth Playground to list some Drive files. You can simply copy-paste it into Chrome dev console, or run it with node. Of course provide your own credentials (the ones below are all fake).
function get_access_token_using_saved_refresh_token() {
// from the oauth playground
const refresh_token = "1/0PvMAoF9GaJFqbNsLZQg-f9NXEljQclmRP4Gwfdo_0";
// from the API console
const client_id = "559798723558-amtjh114mvtpiqis80lkl3kdo4gfm5k.apps.googleusercontent.com";
// from the API console
const client_secret = "WnGC6KJ91H40mg6H9r1eF9L";
// from https://developers.google.com/identity/protocols/OAuth2WebServer#offline
const refresh_url = "https://www.googleapis.com/oauth2/v4/token";
const post_body = `grant_type=refresh_token&client_id=${encodeURIComponent(client_id)}&client_secret=${encodeURIComponent(client_secret)}&refresh_token=${encodeURIComponent(refresh_token)}`;
let refresh_request = {
body: post_body,
method: "POST",
headers: new Headers({
'Content-Type': 'application/x-www-form-urlencoded'
})
}
// post to the refresh endpoint, parse the json response and use the access token to call files.list
fetch(refresh_url, refresh_request).then( response => {
return(response.json());
}).then( response_json => {
console.log(response_json);
files_list(response_json.access_token);
});
}
// a quick and dirty function to list some Drive files using the newly acquired access token
function files_list (access_token) {
const drive_url = "https://www.googleapis.com/drive/v3/files";
let drive_request = {
method: "GET",
headers: new Headers({
Authorization: "Bearer "+access_token
})
}
fetch(drive_url, drive_request).then( response => {
return(response.json());
}).then( list => {
console.log("Found a file called "+list.files[0].name);
});
}
get_access_token_using_saved_refresh_token();
Warning May 2022 - this answer may not be valid any longer - see David Stein's comment
Let me add an alternative route to pinoyyid's excellent answer (which didn't work for me - popping redirect errors).
Instead of using the OAuthPlayground you can also use the HTTP REST API directly. So the difference to pinoyyid's answer is that we'll do things locally. Follow steps 1-3 from pinoyyid's answer. I'll quote them:
Create the Google Account (eg. my.drive.app#gmail.com) - Or skip this step if you are using an existing account.
Use the API console to register the mydriveapp (https://console.developers.google.com/apis/credentials/oauthclient?project=mydriveapp or just https://console.developers.google.com/apis/)
Create a new set of credentials (NB OAuth Client ID not Service Account Key and then choose "Web Application" from the selection)
Now, instead of the playground, add the following to your credentials:
Authorized JavaScript Sources: http://localhost (I don't know if this is required but just do it.)
Authorized Redirect URIs: http://localhost:8080
Screenshot (in German):
Make sure to actually save your changes via the blue button below!
Now you'll probably want to use a GUI to build your HTTP requests. I used Insomnia but you can go with Postman or plain cURL. I recommend Insomnia for it allows you to go through the consent screens easily.
Build a new GET request with the following parameters:
URL: https://accounts.google.com/o/oauth2/v2/auth
Query Param: redirect_uri=http://localhost:8080
Query Param: prompt=consent
Query Param: response_type=code
Query Param: client_id=<your client id from OAuth credentials>
Query Param: scope=<your chosen scopes, e.g. https://www.googleapis.com/auth/drive.file>
Query Param: access_type=offline
If your tool of choice doesn't handle URL encoding automagically make sure to get it right yourself.
Before you fire your request set up a webserver to listen on http://localhost:8080. If you have node and npm installed run npm i express, then create an index.js:
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('ok');
console.log(req)
});
app.listen(8080, function () {
console.log('Listening on port 8080!');
});
And run the server via node index.js. I recommend to either not log the whole req object or to run node index.js | less for the full output will be huge.
There are very simple solutions for other languages, too. E.g. use PHP's built in web server on 8080 php -S localhost:8080.
Now fire your request (in Insomnia) and you should be prompted with the login:
Log in with your email and password and confirm the consent screen (should contain your chosen scopes).
Go back to your terminal and check the output. If you logged the whole thing scroll down (e.g. pgdown in less) until you see a line with code=4/....
Copy that code; it is your authorization code that you'll want to exchange for an access and refresh token. Don't copy too much - if there's an ampersand & do not copy it or anything after. & delimits query parameters. We just want the code.
Now set up a HTTP POST request pointing to https://www.googleapis.com/oauth2/v4/token as form URL encoded. In Insomnia you can just click that - in other tools you might have to set the header yourself to Content-Type: application/x-www-form-urlencoded.
Add the following parameters:
code=<the authorization code from the last step>
client_id=<your client ID again>
client_secret=<your client secret from the OAuth credentials>
redirect_uri=http://localhost:8080
grant_type=authorization_code
Again, make sure that the encoding is correct.
Fire your request and check the output from your server. In the response you should see a JSON object:
{
"access_token": "xxxx",
"expires_in": 3600,
"refresh_token": "1/xxxx",
"scope": "https://www.googleapis.com/auth/drive.file",
"token_type": "Bearer"
}
You can use the access_token right away but it'll only be valid for one hour. Note the refresh token. This is the one you can always* exchange for a new access token.
* You will have to repeat the procedure if the user changes his password, revokes access, is inactive for 6 months etc.
Happy OAuthing!

How to reset google oauth 2.0 authorization?

I'm using Google APIs Client Library for JavaScript (Beta) to authorize user google account on web application (for youtube manipulations). Everything works fine, but i have no idea how to "logout" user from my application, i.e. reset access tokens.
For example, following code checks user authorization and if not, shows popup window for user to log into account and permit web-application access to user data:
gapi.auth.authorize({client_id: CLIENT_ID, scope: SCOPES, immediate: false}, handleAuth);
But client library doesn't have methods to reset authorization.
There is workaround to redirect user to "accounts.google.com/logout", but this
approach is not that i need: thus we logging user off from google account not only from my application, but also anywhere.
Google faq and client library description neither helpful.
Try revoking an access token, that should revoke the actual grant so auto-approvals will stop working. I assume this will solve your issue.
https://developers.google.com/accounts/docs/OAuth2WebServer#tokenrevoke
Its very simple. Just revoke the access.
void RevokeAcess()
{
try{
HttpClient client = new DefaultHttpClient();
HttpPost post = new HttpPost("https://accounts.google.com/o/oauth2/revoke?token="+ACCESS_TOKEN);
org.apache.http.HttpResponse response = client.execute(post);
}
catch(IOException e)
{
}
}
But it should be in asyncTask
It depends what you mean by resetting authorization. I could think of a three ways of doing this:
Remove authorization on the server
Go to myaccount.google.com/permissions, find your app and remove it. The next time you try to sign in you have to complete full authorization flow with account chooser and consent screen.
Sign out on the client
gapi.auth2.getAuthInstance().signOut();
In this way Google authorization server still remembers your app and the authorization token remains in browser storage.
Sign out and disconnect
gapi.auth2.getAuthInstance().signOut();
gapi.auth2.getAuthInstance().disconnect();
This is equivalent to (1) but on the client.
Simply use: gapi.auth.setToken(null);
Solution for dotnet, call below API and pass the access token, doc - https://developers.google.com/identity/protocols/oauth2/web-server#tokenrevoke
string url = "https://accounts.google.com/o/oauth2/revoke?token=" + profileToken.ProfileAccessToken;
RestClient client = new RestClient(url);
var req = new RestRequest(Method.POST);
IRestResponse resp = client.Execute(req);

Resources