under what situations (other than someone revoking permission of changing their gmail password) would a user need to re-authorization an app's access to their gmail data? My app stores and uses a refresh token to maintain a current access token, but my users are occasionally still asked to authorize from scratch, and sometimes this then fails and only works again when they reset their browser settings.
Causes for a refresh token to expire.
user revokes access.
User changes password and your using a gmail scope
refresh token has not been used in six months.
You can have a max of 50 outstanding refresh tokens for a user. So if the user is requesting a new refresh token often make sure to store the newest as the once you hit 51 the oldest one available will be expired.
That should be about it unless you consider the bug of fall 2015 which caused a bunch of refresh tokens to expire due to daylight savings time. (That was a fun day)
I'm not storing anything explicitly, it's all being done by Google's Authorization Broker. Here's the code:
UserCredential credential;
using (FileStream credentialsfile = new FileStream(Application.StartupPath + #"/credentials.json", FileMode.Open, FileAccess.Read))
{
string credPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
credPath = Path.Combine(credPath, ".credentials/SJGmailAPI.json");
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(GoogleClientSecrets.Load(credentialsfile).Secrets, Scopes, senderemailaddress, CancellationToken.None, new FileDataStore(credPath, true)).Result;
}
bool rc = AuthTest.TestAuthorisation(credential,senderemailaddress,ApplicationName);
So is Google's own code storing the wrong refresh token then?
Related
I followed this tutorial EXACTLY, however
#app.route('/test')
def test_api_request():
if 'credentials' not in flask.session:
return flask.redirect('authorize')
# Load credentials from the session.
credentials = google.oauth2.credentials.Credentials(
**flask.session['credentials'])
drive = googleapiclient.discovery.build(
API_SERVICE_NAME, API_VERSION, credentials=credentials)
files = drive.files().list().execute()
# Save credentials back to session in case access token was refreshed.
# ACTION ITEM: In a production app, you likely want to save these
# credentials in a persistent database instead.
flask.session['credentials'] = credentials_to_dict(credentials)
return flask.jsonify(**files)
However in this part:
credentials = google.oauth2.credentials.Credentials(
**flask.session['credentials'])
The refresh token expires after an hour, with this error:
The credentials do not contain the necessary fields need to refresh the access token. You must specify refresh_token, token_uri, client_id, and client_secret.
But clearly in the flask session the dict object is there:
{'client_id': '<COMMENTED_OUT>.apps.googleusercontent.com',
'client_secret': '<COMMENTED_OUT>',
'refresh_token': None,
'scopes': ['https://spreadsheets.google.com/feeds',
'https://www.googleapis.com/auth/drive',
'https://mail.google.com/'],
'token': '<COMMENTED_OUT>',
'token_uri': 'https://oauth2.googleapis.com/token'}
I believe the google tutorial auto-refreshes the token
Two questions
1) Do i need to manually "refresh" the refresh token? The comment in the tutorial says "Save credentials back to session in case access token was refreshed".. which implies that it's refreshed automatically
2) Is this because the app is still in unverified status?
Looking at the dict, the refresh token is missing:
'refresh_token': None,
You need this token in order to refresh your access token after it expires. The refresh token is only provided in the JSON response if the user saw a consent screen (the one that lists the scopes being requested). If the user has previously approved access, and the scopes haven't changed, the OAuth flow will skip that screen if the user is sent back into the flow, and therefore not return the refresh token.
What likely happened is that during your testing you approved access once, but didn't store the refresh token correctly. Further attempts to approve access didn't return the refresh token, hence your inability to refresh the access token.
To ensure a refresh token is always returned, set the URL parameter prompt=consent in the authorization URL:
authorization_url, state = flow.authorization_url(
access_type='offline',
include_granted_scopes='true'
prompt='consent')
(It's documented in the "HTTP/REST" tab here).
Alternatively, visit and revoke access to your application. The next time you go through the OAuth flow you should see the consent screen again, and get a new refresh token.
We are creating an application with Ionic framework as front-end and Ruby on Rails as back-end. We are able to link Gmail account in our app. Account linking is working fine, we get serverAuthCode from front-end and then using that we get refresh token and we are able to fetch emails with that refresh token at first attempt. But within seconds, it get expired or revoked. Getting the following issue:
Signet::AuthorizationError (Authorization failed. Server message:
{
"error" : "invalid_grant",
"error_description" : "Token has been expired or revoked."
})
It seems like, refresh token itself is expiring in seconds. Does anyone have any idea about how to fix it?
Update:
Existing code looks like this:
class User
def authentication(linked_account)
client = Signet::OAuth2::Client.new(
authorization_uri: 'https://accounts.google.com/o/oauth2/auth',
token_credential_uri: Rails.application.secrets.token_credential_uri,
client_id: Rails.application.secrets.google_client_id,
client_secret: Rails.application.secrets.google_client_secret,
scope: 'https://www.googleapis.com/auth/gmail.readonly, https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/userinfo.profile',
redirect_uri: Rails.application.secrets.redirect_uri,
refresh_token: linked_account[:refresh_token]
)
client.update!(access_token: linked_account.token, expires_at: linked_account.expires_at)
return AccessToken.new(linked_account.token) unless client.expired?
auth.fetch_access_token!
end
def get_email(linked_account)
auth = authentication(linked_account)
gmail = Google::Apis::GmailV1::GmailService.new
gmail.client_options.application_name = User::APPLICATION_NAME
gmail.authorization = AccessToken.new(linked_account.token)
query = "(is:inbox OR is:sent)"
gmail.list_user_messages(linked_account[:uid], q: "#{query}")
## Getting error over here ^^
end
end // class end
class AccessToken
attr_reader :token
def initialize(token)
#token = token
end
def apply!(headers)
headers['Authorization'] = "Bearer #{#token}"
end
end
Reference link: https://github.com/google/google-api-ruby-client/issues/296
From what I can guess the issue seems to be on these two lines. The way token expiry is being checked and the new token is being generated. It would be great if there is minimal reproducible code.
return AccessToken.new(linked_account.token) unless client.expired?
auth.fetch_access_token!
Here is how I get my access token:
def self.access_token(refresh_token)
Cache.fetch(refresh_token, expires_in: 60.minutes) do
url = GoogleService::TOKEN_CREDENTIAL_URI
# p.s. TOKEN_CREDENTIAL_URI = 'https://www.googleapis.com/oauth2/v4/token'
_, response = Request.post(
url,
payload: {
"client_id": GoogleService::CLIENT_ID,
"client_secret": GoogleService::CLIENT_SECRET,
"refresh_token": refresh_token,
"grant_type": "refresh_token"
}
)
response['access_token']
end
end
And then use this access token for any purpose. Let me know how it goes and also if you are able to create a reproducible version of the API. That will be great.
Have you tried refreshing the access token with the refresh token? You can catch the error and retry.
Something like this:
begin
gmail.list_user_messages(linked_account[:uid], q: "#{query}")
rescue Google::Apis::AuthorizationError => exception
client.refresh!
retry
end
Not enough code is posted, but what is posted looks wrong.
linked_account is not defined
Nowhere is it shown that linked_account.token is ever updated (or set, for that matter). It needs to be updated when the refresh_token is used to get a new access token.
auth appears to be undefined in the line auth.fetch_access_token!
GmailService#authorization= takes a Signet::OAuth2::Client not an AccessToken.
Probably what is happening is that you have a valid access token in linked_account.token until you call client.update!, which fetches a new access token and invalidates the old one. But since you never update linked_account, future calls fail until you go through the code path that resets it.
You only need to call client.update! if the access token has expired, and if it has expired and you get a new one, you need to store that new one in linked_account.token.
The thought that the refresh token will never expire is actually a misunderstanding. The actual scene is that the server issues a short-lived access token and a long lived refresh token. So in reality what happens is that the access token can be regained using the long lived refresh tokens but yes, you will have to request a new refresh token (as it expires too !). For example; you may treat refresh tokens as if they never expire. However on sign-in check for a new one, in case the user revokes the refresh token, in this scenario, Google will provide a new refresh token on sign-in so just update the refresh token.
Now the condition can be that the user revokes access to your application. In this case, the refresh token will expire (or I should actually say that it would become an unauthorized one). So if that is the scenario in your case, you will have to think on avoiding the revoking of access for the application.
For better understanding of it, you may refer to this document and even OAuth 2.0 documentation.
There are several reasons why a refresh token would stop working.
It gets to old refresh tokens expire after six months if not used.
A user can reauthecate your application and get a new refresh token both refresh tokens will work you can have a max of fifty outstanding refresh tokens then the first will stop working.
the user can revoke your access.
Wakey daylight savings time bug of 2015. (we wont talk about that)
Gmail and reset password.
This is mostly like due to a password reset. OAuth grants with the gmail scopes are revoked when a user changes their password.
See Automatic OAuth 2.0 token revocation upon password change
In general, users can revoke grants at any time. You should be able to handle that case gracefully and alert the user that they need to reauthorize if they wish to continue using the functionality provided.
You have been doing a lot of testing i would guess are you saving the newest refresh token? If not then you may be using old refresh tokens and the will stop working. (number 2)
In my case, only youtube upload api raise
Unauthorized (Google::Apis::AuthorizationError)
and other api, like list videos api work well
it's because i use new google account and have not up video
i manually up video in youtube web, youtube require me create "channel"
and I try youtube up api again, it work
I guess it's because youtube has channel to up
I was forcibly trying to expire the Google API OAuth Access Token for the Contacts API.
Here is the general outline of my test:
Get the code:
auth_uri = flow.step1_get_authorize_url()
redirect(auth_uri) code here
Exchange it for the access token:
code = request.GET.get('code')
credentials = flow.step2_exchange(code)
store_for_reuse(key, credentials)
store_for_refresh(key, credentials)
Get a new access token:
credentials_original = get_for_reuse(key)
credentials_for_refresh = get_for_refresh(key)
credentials_for_refresh.refresh(http)
Now, I can verify that the two credentials object have different access token values
credentials_original.access_token != credentials_for_refresh.acesss_token
Surprisingly, I can still make successful API calls with the original access token. Shouldn't the original one expire when the new access token is issued?
Getting a new token does not expire existing tokens. Tokens will be valid for their designated lifespan unless explicitly revoked.
I am new to dot net MVC and I am working on a application which required Box integration, in which user authorizes our application using oauth2 authentication flow.
I get the access token and refresh token and save them to my database for further offline access so that user do not have to authenticate the application again.
Now, I am using Box Windows SDK v2 to get list of files and folders of a user.
Here is the code block by which I am able to get the rot folder of box.
var config = new BoxConfig(clientId, clientSecret, new Uri("http://localhost:49671/CloudBox/Callback/"));
//Pls note, here accessToken and refreshToken are fetched from database
OAuthSession session = new OAuthSession(accessToken, refreshToken, 3600, "bearer");
BoxClient client = new BoxClient(config, session);
BoxFolder boxFolder = client.FoldersManager.GetInformationAsync("0").Result;
Everything is fine upto this point.
Now when the access token expires (as it is valid only for 3600 seconds), and I try to get the root folder again, Box SDK refreshes access token and refresh token automatically without telling me. and provides me the root folder object.
At this moment I got the root folder, but I am not aware that Box SDK has updated the access token and refresh tokens. Still I have old access token and refresh token in my database. They are not updated. And I am lost. Now I am left with those old invalidated access token and refresh token.
Pls help. How do I know that Box SDK has updated access token and refresh token ? so that I can update them in my database for future use.
It would be nice, if you can provide a working sample MVC application which stores the access token and refresh token. You can provide the code blocks which needs to be placed in controller. I hope, I will be able to integrate them.
Thanks in advance.
You could use the events triggered by Box.V2 after refreshing the token:
SessionAuthenticated : Fires when a new set of auth token and refresh token pair has been fetched
SessionInvalidated: Fires when the authenticaiton session is invalidated
Please help me to understand the concept of expiry time for sessiontoken.
Below is the way I am setting the session token after receiving the token from STS.
var principal = validationfunction();//returns claimsprincipal
if (principal != null)
{
var token = new SessionSecurityToken(principal.ClaimsPrincipal)
{
IsReferenceMode = false
};
//this makes sure that the identity and claims are written to the cookie.
FederatedAuthentication.WSFederationAuthenticationModule.SetPrincipalAndWriteSessionToken(token, true);
}
Please confirm if this is true or not:
if the token lifetime is 10 mins. if user is inactive for 10 mins and doesnt send any request to
website it the session token expires and its redirected to STS login page.
if user is active and keep refreshing the page/visits different page the sessiontoken lifetime gets
refreshed . it means everytime the user visits the page the token gets new expiry value. So user will not be redirected to login page every 10 mins.
if user requests a STS protected resource (web api) , the life time of token is treated absolute. Meaning regardless user is active or not, after 10 mins of token generated if the user requests web api , the token will be invalid and redirected to STS login page.
are the above concepts correct?
You need to set the token lifetime yourself. The default is IIRC - 10h. When the token has expired and you are accessing a protected resource, the application will emit a 401. If you have the WsFed modue - this will result in a roundtrip to the STS
Session security tokens are absolute expiration by default
You wouldn't use a cookie to secure a Web API - a redirect does not make sense for APIs (nor does cookie authentication).