Refresh LinkedIn token with omniauth before expiration - ruby-on-rails

I have a Rails (3.2.11) application that allows users to post updates to their LinkedIn profiles. I'm currently using the omniauth-linkedin gem to capture the initial user authentication and the linkedin gem to post the updates. The problem I'm having is that LinkedIn access tokens expire after 60 days, but according to their documentation a token can be refreshed prior to expiration without a user having to reauthorize the application.
I've looked at the LinkedIn Tips and Tricks, Authentication Overview, and tons of posts on StackOverflow - this, this, and this being just a couple of examples - and I still can't find any answers.
After a user authorizes the app (via omniauth-linkedin), I save the access_token and secret returned to me from LinkedIn. I need to figure out how I can use the still-valid access_token to refresh it and extend the expiration date another 60 days.
I've tried using the authenticate endpoint from LinkedIn (where tokens.access_token is the currently valid token):
url = "https//www.linkedin.com/uas/oauth/authenticate?oauth_token=" + tokens.access_token
result = RestClient.post(url, {oauth_callback: "http://localhost:3000/users/auth/linkedin/callback"})
but I get an undefined method 'request_uri' for #<URI::Generic:0x1b144d20> Exception.
I've tried using the OAuth::Consumer client (where tokens.access_token and tokens.token_secret are the currently valid tokens):
configuration = { site: 'https://api.linkedin.com', authorize_path: '/uas/oauth/authenticate',
request_token_path: '/uas/oauth/requestToken', access_token_path: '/uas/oauth/accessToken' }
consumer = OAuth::Consumer.new(ENV['LINKEDIN_APP_ID'], ENV['LINKEDIN_SECRET'], configuration)
access_token = OAuth::AccessToken.new(consumer, tokens.access_token, tokens.token_secret)
but this just gives me back the same access_token and secret.
In the end, I'd love to be able to leverage the existing omniauth-linkedin gem functionality to handle this refresh, any idea if this is possible? Thanks!

In your second approach (using the OAuth::Consumer client and passing in your existing access token and secret) should refresh the token for you. As the documentation states, as long as the current user is logged into LinkedIn.com and the current access token hasn't expired yet, the token will be refreshed.
That doesn't mean necessarily that you'll get a new token. You may get the same one as you had before. The key difference is that the lifespan of the token should 60 days. You can verify this by check the value of oauth_expires_in parameter. It should be set to 5184000.
This blog post goes into detail about refreshing the token: https://developer.linkedin.com/blog/tips-and-tricks-refreshing-access-token

Related

Google OAuth2 refresh expires & does not renew

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.

Google::Apis::AuthorizationError (Unauthorized)

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

How to use Omniauth Asana with Rails API only app

I have a Rails 5 API only app and an Angular JS Frontend app and would like to integrate with Asana API. I'm using the ruby-asana, omniauth and omniauth-asana gems.
I start the request using Asana's JS library like so:
var client = Asana.Client.create({
clientId: 172706773623703,
clientSecret: '<client_secret>',
redirectUri: '<redirect_url>'
});
client.useOauth({
flowType: Asana.auth.PopFlow
});
And the above does redirect me to Asana where I can login. On the redirectUri I'm giving a backend route (Rails 5 API only) which should handle the remaining on the authentication (using the JS only I get only a temporary token that cannot be self renewed meaning the user will have to authenticate every time the token expires. This is if I understood the documentation correctly).
So, on the controller I've created to handle the route, I have the following (from an example on Asana's documentation):
require 'omniauth-asana'
use OmniAuth::Strategies::Asana, <secret>, <secret>
creds = request.env["omniauth.auth"]["credentials"].tap { |h| h.delete('expires') }
strategy = request.env["omniauth.strategy"]
access_token = OAuth2::AccessToken.from_hash(strategy.client, creds).refresh!
$client = Asana::Client.new do |c|
c.authentication :oauth2, access_token
end
Now, the above doesn't work because 1) there's no request.env as this is an API only app, so I've followed the instruction on Omniauth and have added the following to my config/application.rb:
config.session_store :cookie_store, key: '_interslice_session'
config.middleware.use ActionDispatch::Cookies # Required for all session management
config.middleware.use ActionDispatch::Session::CookieStore, config.session_options
Now, in the request.headers I have _interslice_session which has some numbers. How can I create a Asana client with the above?
Any ideas?
OK, I think I see what you're attempting to do here; I think the best way forward is to start with how OAuth's Authorization Code Grant happens in general, then move into specifics for OmniAuth.
You send the user to a URL that Asana owns; that is, your goal is to get the user to visit a particular url. For Asana, this is https://app.asana.com/-/oauth_authorize. (Note that we respond with an error if you don't sent a correct client_id param, but feel free to check that link if you want). Do not send the client_secret during this request - it is intended to never be involved in client-side code, as this is insecure.
If they agree to give access, Asana sends back a redirect request to the user's browser with a short-lived code. That then means that your server will be called from the user's browser with this code as a parameter, so has to handle a new incoming request from the browser to whatever you specified as your redirect URI. Also, this location must be accessible by all users of your integration wherever they are.
You send this code from your server as a POST request to https://app.asana.com/-/oauth_token with your client_secret to Asana for a refresh token. This is where your application actually asks for credentials; the token given in the previous phases simply acknowledges that for a short time, the user has given your app permission to ask for these credentials, and your client_secret assures Asana that, for this server-side request, your app really is yours (it's like your application's password).
We send back an access_token which represents (approximately) a client-user credential pair that is valid for an hour.
You use these credentials to access our API on behalf of this user. We also send back a refresh_token which is long-lived, and used to get new short-lived access_tokens after they expire in a very similar way.
OK, so how this works with OmniAuth if I grok it correctly is that it expects to handle almost all of it. I'll be working through our omniauth example in our ruby-asana client library here: https://github.com/Asana/ruby-asana/blob/master/examples/omniauth_integration.rb
You set up OmniAuth with your client id and client secret
use OmniAuth::Strategies::Asana, <client_id>, <client_secret>
A request comes in, and you don't have credentials for it.
get '/' do
if $client
...
else
'sign in to asana'
end
end
The user clicks the sign in link, which (code omitted) sends them to the sign_in endpoint. This endpoint issues a redirect to /auth/asana
The browser requests /auth/asana from our server. If you look at that example, it's not implemented in our code. That's because the /auth/:provider is magically handled by OmniAuth.
This is where all the magic happens. OmniAuth handles the entire login flow above: send browser to our oauth_authorize above, then receive the callback and sticks the relevant params in the environment such that it knows "we just got the short lived code". By the time these lines get hit:
creds = request.env["omniauth.auth"]["credentials"].tap { |h| h.delete('expires') }
strategy = request.env["omniauth.strategy"]
you are inside a callback that OmniAuth has intercepted, gotten the needed creds, and set the creds in the environment. You shouldn't have to handle the oauth callback and token exchange manually.
Now, with the code you provided, I notice a few things right off:
You are causing the popup cycle to happen client side. It may be (and I strongly suspect) that this won't work with OmniAuth - it expects to handle the whole OAuth flow.
Based on the code snippet you provided, you aren't serving this out of a request-response cycle in the controller, rather, it appears that this is in the controller body and not out of an instance method. It may be a typo, but this needs to be in a method that is called back to outside of Rails (that is, a route must point to this a controller method that Asana can use to handle the browser request).
You shouldn't have to look at request.headers, I think - I'm not sure what the issues might be with request.env, but I suspect they may be unrelated to the API-only nature of your app. Are you sure that this is because it's API-only? After adding in the middleware, did you double-check that you can't access request.env? My hunch would be that persistent data in request.env will still be there, only it would require on the middleware being added in to do this. The instructions on OmniAuth simply say that you need to have a session store for your API - which makes sense to me, because APIs don't necessarily need to store state across requests, OmniAuth is telling you to put a session store back in.
I know this is a lot of info, but hopefully it helps you get on the right track. Cheers!

Rails 4 / Koala: how to refresh long access token?

Using Rails 4 and Koala gem version 2.
I have my personal Facebook page (not profile).
When I am publishing a new post in my website I want to automatically publish it on my facebook page as well.
I am using Koala gem but I have some problem with access tokens.
Visiting https://developers.facebook.com/tools/explorer/ I get a short-live token. This token expires after only 2 hours.
My goal is to obtain a long-live token.
I have created a new Facebook app and I have obtained a client-id and client-secret.
Visiting this https://graph.facebook.com/oauth/access_token?client_id=MY-CLIENT-ID&client_secret=MY-CLIENT-SECRET&grant_type=fb_exchange_token&fb_exchange_token=MY-SHORT-LIVE-TOKEN I get long-live token.
Now I can use my long-live token to publish on my page:
user = Koala::Facebook::API.new long_live_access_token
page_access_token = user.get_connections('me', 'accounts').first['access_token']
page = Koala::Facebook::API.new page_access_token
page.put_connections("me", "feed", message: "I am posting on my page!ok!")
It seems to work, but my problems is that the long-live token expires in 60 days so I need to refresh it.
I have found no solution to refresh a long-live token. It is clear that I can't repeat the entire procedure every 60 days!
Ideas?
From the official docs:
These tokens (long-lived) will be refreshed once per day when the
person using your app makes a request to Facebook's servers. If no
requests are made, the token will expire after about 60 days and the
person will have to go through the login flow again to get a new
token.
The token that never expires is a page token.

Google API Client: Token has been revoked issue

I'm using the google-api-client gem in my Rails project. I have omniauth and devise working, and I have users authenticate through Google.
I thought I had this going very well, until recently. I've noticed my app will throw an error when it fetches the Google Calendar API after one hour. The expiration is one hour from authentication time, and from then I get this error:
Signet::AuthorizationError (Authorization failed. Server message:
{
"error" : "invalid_grant",
"error_description" : "Token has been revoked."
}):
This is separate from invalid refresh tokens, as I do have the refresh token stored in the database. It is sending the refresh token request, which spurs that error above, with this code:
client = Google::APIClient.new(
:application_name => APP_NAME,
:application_version => APP_VERSION,
)
client.authorization.client_id = CLIENT_ID
client.authorization.client_secret = CLIENT_SECRET
client.authorization.refresh_token = user.auth_refresh_token
token_result = client.authorization.fetch_access_token!
I have been very careful as to not sign in and out of my Google accounts, so I cannot figure out why Google would send back this message. If I refresh the page after 55 minutes, all is okay. If I refresh the page after 1 hour, it complains about the access token being revoked.
Has anyone had this issue before? If so, what did you do to fix it? Was it something you had to change in Google's Developer Console?
I ended up figuring out the issue, so I thought I'd share what fixed it.
In config/initializers/devise.rb, I have:
scope: 'userinfo.profile, userinfo.email, calendar, https://www.googleapis.com/auth/gmail.readonly', prompt: 'select_account consent' }
What did it for me was the prompt: 'select_account consent' part. Asking the user for consent at each login seems to keep the refresh token up to date. When the user logs in via Google I check if there was a refresh token in the response, and if there was I save that to the database. If not, I keep their current refresh token in the database.
In all honesty, I really don't get why it was necessary for me to do this but for other users who've shared their code examples it was fine. Perhaps there was a change in the Google's OAuth2 or maybe there's a discrepancy in my method of handling the authorization.

Resources