Closed jonhyman closed 10 years ago
Need to look into this... it's a pain since it looks like it is non-standard to the OAuth2 spec. At least we have a bit of time to figure it out.
Any thoughts you had on the best way to implement this would be appreciated.
I read the article and came to a different conclusion. The "Server-side OAuth Developers" section states the "resulting access_token will have the longer expiration time". And only client-side OAuth developers need to "extend the expiration time of an existing, valid access_token".
So what does "longer expiration time" mean? My guess is 60 days. I presume Facebook wants to deprecate the offline_access permission to synchronize the expiration time of both client-side and server-side OAuth requests. I have some time today to put my hypothesis to the test.
Thanks @jonhyman for starting the discussion.
Yea, the documentation seems pretty unclear. From testing the server-side flow a few days ago, I recall the expiration time being only a few hours.
Even 60 days is still going to be an issue, since we need a way to renew the access_token that isn't too fiddly.
Yea, the documentation seems pretty unclear. From testing the server-side flow a few days ago, I recall the expiration time being only a few hours.
Totally unclear. However, confirming my hypothesis, here are 60-day credentials which Facebook sent me today:
credentials:
expires_at: 1335901431
expires: true
token: AAAFPfXj...
Which is converted to a Time object like so:
>> Time.at(1335901431)
=> Tue May 01 12:43:51 -0700 2012
All that I needed to do was enable the "deprecate offline_access" setting from the "Advanced" tab of my Facebook app's developer page and click the "Save Changes" button.
https://developers.facebook.com/apps/:id/advanced # Replace :id with your app's ID.
Then I just ensured my Rails app requested the email,offline_access
permissions. If it requested just email
, it only received 2-hour credentials.
Even 60 days is still going to be an issue, since we need a way to renew the token that isn't too fiddly.
Renewing the token
is easier than you might think. Well, easy if you're using the omniauth-facebook
gem. :P
The deprecation article states that if "there is still a valid token for that user, the returned token from this second call may be the same or may have changed, but in either case the expiration time will have been reset." (Remember, Facebook "will only extend the expiration time once per day.")
From my own experiments, I can confirm that the resulting token
from calling /auth/facebook
on my Rails app was the same token
returned on previous calls when I either disabled the "deprecate offline_access" setting or requested only the email
permission. In those two cases, Facebook returned either non-expiring or 2-hour expiring credentials respectively, but the token
was exactly the same. Even if the token
had changed, it's not really a big deal. Only the provider
and uid
value-pair are required to uniquely identify a user.
Anyway, my point is I don't think much of the omniauth-facebook
internals will need to change because the offline_access deprecation. From my perspective, a valid token
is not a security requirement but a user-experience requirement. Other perspectives are certainly welcome.
UPDATE: After more experimentation today, I discovered that Facebook only issues 60-day credentials to users who have already given your application offline_access
permission. For all new auth requests, Facebook will issue 2-hour credentials even if you've enabled the "deprecate offline_access" setting and requested offline_access
permission.
@ryansobol, that is my experience as well. I'm only receiving 2 hour credentials. I think that they have to be renewed to get the longer validation time, but I haven't tested that yet. There is a Facebook bug report at https://developers.facebook.com/bugs/241373692605971?browse=search_4f532583134e85559528532 that multiple people have +1'd about the 2 hour time.
@jonhyman -- I think I may have figured out how to receive 60-day credentials for new and existing Facebook users. Enable both "Deprecate offline access" and "Enhanced Auth Dialog" settings (Settings -> Advanced). I've tested this with both the "email,offline_access" and "email" scopes.
Can anyone confirm?
I've deprecated offline access and I'm using the enhanced auth dialog, and I'm getting 60 day tokens with new facebook users
I'm increasingly coming to the view that renewing access tokens is not a concern of omniauth-facebook. What do you think?
IMO that depends on what the goal of omniauth-facebook is - providing a facebook strategy for omniauth or complete authentication support for facebook (beyond the oauth spec)
I've been using Koala and that does provide support for renewing access tokens
On 21/03/2012, at 4:28 AM, Mark Dodwell reply@reply.github.com wrote:
I'm increasingly coming to the view that renewing access tokens is not a concern of omniauth-facebook. What do you think?
Reply to this email directly or view it on GitHub: https://github.com/mkdynamic/omniauth-facebook/issues/23#issuecomment-4600773
I'm increasingly coming to the view that renewing access tokens is not a concern of omniauth-facebook. What do you think?
IMO that depends on the goal of each individual app. This is an open source project, right? So in theory, people can submit patches for the functionality that their app needs. :)
The current behavior of omniauth-facebook is right for the app I'm working on today. But perhaps in the future, the app's requirements will change and I'll need to adjust my implementation as appropriate. And if the adjustments are worth sharing upstream then I will. But, that's just me. :D
watching...
I decided to do this in my application since I had already abstracted Facebook from RestGraph into my own wrapper for other purposes. I've pasted the relevant sections. If you want the full adapter, let me know and I'll toss it up on a gist.
In my /auth/:provider/callback
controller and action, I have
FACEBOOK_PROVIDER = "facebook"
# omniauth.auth is not a HashWithIndifferentAccess, so it uses string keys
auth = env['omniauth.auth']
if auth['provider'] == FACEBOOK_PROVIDER
@oauth_token = auth['credentials']['token']
# Instantly renew the auth token so we get a longer expiration time
extension = Social::FacebookWrapper.extend_token(env['omniauth.strategy'].options.client_id,
env['omniauth.strategy'].options.client_secret,
@oauth_token)
end
Then my adapter is:
module Social
module FacebookAdapter
def initialize(access_token)
@client = RestGraph.new(:access_token => access_token)
end
# Extends an +access_token+ for an +app_id+, +app_secret+ pair. This uses the method specified at
# https://developers.facebook.com/roadmap/offline-access-removal/ to hit Facebook and renew the access token.
# This method returns {:access_token => new_access_token, :expires => Time object}. If the oauth token has
# already been granted offline_access, :expires will be nil.
def self.extend_token(app_id, app_secret, access_token)
begin
response = RestClient.post("https://graph.facebook.com/oauth/access_token?client_id=#{app_id}&client_secret=#{app_secret}&grant_type=fb_exchange_token&fb_exchange_token=#{access_token}", {})
# Facebook really sucks, they don't return the format as JSON but instead as plain-text in key=value&key=value
# regardless of the content-type and accept header sent to them. Therefore, parse that out into components.
components = parse_query_values(response)
# Convert components[:expires] into the proper Time object
if components[:expires]
# parse_query_values returns string values, so explicitly #to_i components[:expires]
# Per https://developers.facebook.com/docs/authentication/server-side/, expires is the number of seconds
# until the token expires
components[:expires] = (Time.now + components[:expires].to_i())
end
# If we have any problems renewing the token, say that the oauth token expires in 2 hours (Facebook's default)
rescue Exception => e
Rails.logger.error("Could not extend token. App id: #{app_id}: #{e}")
two_hours = (Time.now + 7200)
components = {:access_token => access_token, :expires => two_hours}
end
components
end
# Pass along any missing methods to the RestGraph client
def method_missing(method, *args, &block)
@client.__send__(method, *args, &block)
end
# Takes a +str+ in the form of key1=value1&key2=value2 and turns it into {:key1 => value, :key2 => value}
def self.parse_query_values(str)
str.split("&").reduce({}) do |result, kv|
key, value = kv.split("=")
result.merge!(key.to_sym => value)
end
end
end
end
As of May 1st 2012 offline_access is deprecated. src: https://developers.facebook.com/roadmap/offline-access-removal/
FYI. Removed mentions of offline_access from the docs and added info regarding the expiration time for access tokens: https://github.com/mkdynamic/omniauth-facebook#token-expiry
Any feedback/pull requests on that documentation to clarify would be appreciated.
With respect to token renewal, I am thinking adding an option (say) auto_renew_access_token
and then doing this automatically whenever omniauth-facebook obtains (or receives from a signed_request) an access token that is short-lived. Since this will involve an extra API call, I am thinking it makes sense for this to be false
by default. Will probably push a sketch of this up to a branch this weekend, be grateful for your guys thoughts.
Well, this could be a killer for some web apps tho :/
Scope this auto token exchange option: https://github.com/mkdynamic/omniauth-facebook/compare/auto_exchange_short_lived_tokens
It's definitely adding a fair bit of complexity, but it does work. Let me know what you guys think.
@mkdynamic i think it should be released ASAP. It's a perfect solution for people who use client-side auth and it does not break anything for all these folks who don't need it.
@mkdynamic I've been trying the auto exchange short lived token branch, but I can't get it working for me because I'm not receiving signed requests from facebook.
On lib/omniauth/strategies/facebook.rb:81 the access token is only refreshed if authorization_code_was_from_signed_request
is true, but for my app it's always false. Looking at the facebook documentation for signed requests I believe that it will be false for most omniauth-facebook apps during login, and therefore the token will never be exchanged. Having said that I'm sure there must be a good reason for the code to checking if it got the token from a signed request, so I wouldn't want to patch over it.
If you want me to experiment with any changes to get it working then I'd be glad to help.
@iainbeeston Thanks so much for checking this out.
The reason I did that (which could well be totally unhelpful and/or stupid) is that tokens obtained via server side flows should already long lived (https://github.com/mkdynamic/omniauth-facebook#token-expiry).
If you're getting a short lived token using the client-side flow, then you should get a signed request back from Facebook (at least that is my understanding). Providing you enable cookies with the FB JS SDK, the middleware should get the signed request from the cookie when you hit /auth/facebook/callback
. Even without cookies, you could manually pass it via a param...
Or are you getting short lived tokens some other way, where signed requests aren't available?...
Please feel free to fork and fiddle, would be super interested in any/all ideas/improvements.
@mkdynamic Unfortunately (and confusingly) I am using the server-side flow, and I have disabled offline access for my app, but I'm still getting 2 hour tokens. What's even stranger is that while I was debugging the facebook omniauth strategy with test facebook accounts, I was getting a mix of short and long lived tokens (maybe 80% vs 20%).
I'm at a loss to know why that might be happening - there's nothing notable about the auth hash from facebook (other than the 2 hour expires_at
value), the request wasn't signed (which is expected for the server-side flow) and the code
parameter is there as expected.
I'm tempted to monkey patch it for now to refresh the token regardless of whether the request was signed or not. The current code always checks if the token is due to expire within 2 hours, and this should always be true with the client-side flow and always false with the server-side flow (if the behaviour of facebook was as documented). Therefore removing the check that prevents renewing the token for unsigned requests (ie. the server-side flow) should only make a difference if you're using the server-side flow and facebook still issues a 2-hour token. So there shouldn't be any negative impact, right? (I'm going out on a limb a little here - I wish there were other people who could confirm this is happening to them too and I've not just missed some crucial config step!)
Right, I've been looking into this some more. It seems that in our production app we are always getting 60 day tokens from facebook. Maybe having our non-production apps sandboxed is changing the tokens that we're being issued? :-\
If you need the users autho for longer, do you consider makeing email notification about expire on facebook token and alert to refresh(log) to user?
I'm still trying to get my head around the implications of the impending removal of the offline_access permission, it's unbelievable how confusing it all is!
I get that client-side auth gives you a short-lived token that you can then exchange for a longer-lived one.
I get that server-side auth gives you a longer-lived token straight off the bat.
What I don't get is what you're expected to do once a longer-lived token has expired. From what I can gather, the only thing you can do is get the user to re-auth and that there is no way to extend a longer-lived token. Is this correct? If so, Facebook have once again sold us up the river...
Yep, that's my understanding too. I imagine we'll start seeing a lot of bimonthly reminder emails from websites that rely on the Facebook api...
What I don't get is what you're expected to do once a longer-lived token has expired. From what I can gather, the only thing you can do is get the user to re-auth and that there is no way to extend a longer-lived token. Is this correct?
Yep, that's correct. However, it really comes down to your app's requirements. If your app is like mine, and it only uses omniauth-facebook
for user authentication (i.e. user log-in), then there's no reason to worry about expired tokens.
I'm open to new perspectives, but as far as I understand, re-authorization is required when the following is true:
@ryansobol @iainbeeston @tekin That is my understanding too.
You can't extend a long lived token without asking the user for permission again, at least that is what it says here (under 'Scenario 3: Server-side OAuth Developers') http://developers.facebook.com/roadmap/offline-access-removal/.
I've been using the extending code in my server side and iOS client for about two months now. On my website, a user links his Facebook account to our service, he does not log in with Facebook. Because of this, we pop up reminders about expiring Facebook credentials and ask the user to "click here to renew." If we offered Facebook login, this wouldn't be an issue, but our service is for businesses and therefore really isn't built around the concept of having business owners log in with Facebook.
For our mobile app, we use Facebook's iOS SDK which extends the OAuth token when they open up the app.
@jonhyman That's pretty much how we're dealing with it.
We're also considering implementing an auto-renew feature where it's possible. For example, when a user's token is set to expire there could be a client-side check when they log in that uses the JS SDK to confirm that:
a) the user is logged in to Facebook b) it is the same user who originally linked the account c) the app authorisation is still valid and app permissions are the same.
If all of the above is true then redirecting them to Facebook for re-auth should simply pass through and back to the web app with a fresh token. To the user, it would be relatively unobtrusive - the page will appear to refresh and leave them back where they started. That's the idea anyway....
This is slightly OT (and I might have mentioned it before), but I've noticed a few times that facebook test accounts don't always get given long-lived tokens when logging in or renewing - sometimes I will only be given a 2 hour token. So far as I can tell this doesn't happen with real (production) facebook accounts. It'd be good to hear if anyone else has found this as well.
I don't get it, but actually when I request an old offline_accsess I get back a 60days token :S
@tekin So bottomline, user has to re-auth(regardless how seamless it might seem to them) every 2 months? I couldn't get much from their docs about expiration for long-lived tokens, but wish the life of tokens would be longer than 2 months.
Yep, that's the bottom line. Having said all that, Facebook have just extended the deprecation period until October so long-lived tokens will be around for a little bit longer.
so what's the best strategy to extend tokens? My app uses omniauth for login, but then uses koala for graph API queries. I use mostly server side queries. What I was thinking of doing is to make the login process 2 steps 1) login with omniauth 2) immediately extend the token. Koala has a function to do this, but I'm not sure how to do this with omniauth.
thoughts? Do I need to do this? Right now, my server side auth tokens are coming back as 2 hour, not 60 day
@joshco I am using same setup(omniauth for login and koala for api calls), but tokens we get are already have 2 months life time, not sure why you are getting short-lived tokens. Here's how FB app settings look http://cl.ly/image/2p3e0k2z281r
Make sure that you've gone into your Facebook app settings and enabled the offline_access deprecation. Even if you think you did it, check again since I think that Facebook switched some of these back when they moved the deprecation (I don't know for sure, but ours was set to disabled even though I had switched it 3 months ago).
Unless you set that, you won't get 2 month tokens.
I've updated to the auto_exchange branch. I think I'm actually using client side flow. My page has a login link with the client redirect. you can see my app at http://hero74.org
I'm seeing that my requests are all unsigned, so the code does not auto_exchange tokens. How do I make them signed?
also, is there a way to force it to use server side requests? how do I make my requests signed?
I'm using omniauth-facebook right now and keep getting 2-hours expired token using the server side flow. Any thoughts?
To be clear I'm still using gem 'omniauth-facebook', '1.4.0' as I encountered stop-bug with later version.
Have there been any developments on this issue in the last 6 months? Seems like an important feature that most people are going to want to take advantage of.
I was able to get this working back when I was having the problem. In my gemfile i have: gem 'omniauth-facebook', :git => 'git:// github.com/mkdynamic/omniauth-facebook.git', :ref => 'auto_exchange_short_lived_tokens'
I don't know if this has been incorporated into master and I'm afraid to try right now :)
On Mon, Jan 28, 2013 at 5:48 PM, corwinstephen notifications@github.comwrote:
Have there been any developments on this issue in the last 6 months? Seems like an important feature that most people are going to want to take advantage of.
— Reply to this email directly or view it on GitHubhttps://github.com/mkdynamic/omniauth-facebook/issues/23#issuecomment-12810013.
Don't know why but removing the app from my Facebook Account and the re-authorize gets me the long-lived token using the Server Side Flow.
Here's what I did, adapted from Railscasts #360 and the technique suggested by @joshco of getting the 60-day token with Koala immediately after getting the original token.
In my models/user.rb this code is working for me, and I can see the 60-day expiration in the database.
def self.from_omniauth(auth)
# immediately get 60 day auth token
oauth = Koala::Facebook::OAuth.new(ENV["FACEBOOK_APP_ID"], ENV["FACEBOOK_SECRET"])
new_access_info = oauth.exchange_access_token_info auth.credentials.token
new_access_token = new_access_info["access_token"]
new_access_expires_at = DateTime.now + new_access_info["expires"].to_i.seconds
where(auth.slice(:provider, :uid)).first_or_initialize.tap do |user|
user.provider = auth.provider
user.uid = auth.uid
user.name = auth.info.name
user.image = auth.info.image
user.email = auth.info.email
user.oauth_token = new_access_token #originally auth.credentials.token
user.oauth_expires_at = new_access_expires_at #originally Time.at(auth.credentials.expires_at)
user.save!
end
end
Closing this since no activity on this for 8 months. Supporting this seems complex, so unless anyone wants to submit a super convincing PR, it's unlikely to ever happen.
It looks like Facebook started deprecating offline_access as of Friday the 20th:
https://developers.facebook.com/docs/offline-access-deprecation/
OAuth tokens can now be exchanged via arguments passed to Facebook: