simi / omniauth-facebook

Facebook OAuth2 Strategy for OmniAuth
https://simi.github.io/omniauth-facebook/
1.26k stars 404 forks source link

Support Facebook's new offline_access paradigm #23

Closed jonhyman closed 10 years ago

jonhyman commented 12 years ago

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:

https://graph.facebook.com/oauth/access_token?             
client_id=APP_ID&
client_secret=APP_SECRET&
grant_type=fb_exchange_token&
fb_exchange_token=EXISTING_ACCESS_TOKEN
mkdynamic commented 12 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.

ryansobol commented 12 years ago

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.

mkdynamic commented 12 years ago

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.

ryansobol commented 12 years ago

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.

ryansobol commented 12 years ago

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.

jonhyman commented 12 years ago

@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.

ryansobol commented 12 years ago

@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?

iainbeeston commented 12 years ago

I've deprecated offline access and I'm using the enhanced auth dialog, and I'm getting 60 day tokens with new facebook users

mkdynamic commented 12 years ago

I'm increasingly coming to the view that renewing access tokens is not a concern of omniauth-facebook. What do you think?

iainbeeston commented 12 years ago

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

ryansobol commented 12 years ago

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

khoan commented 12 years ago

watching...

jonhyman commented 12 years ago

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
mrchess commented 12 years ago

As of May 1st 2012 offline_access is deprecated. src: https://developers.facebook.com/roadmap/offline-access-removal/

mkdynamic commented 12 years ago

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.

markopavlovic commented 12 years ago

Well, this could be a killer for some web apps tho :/

mkdynamic commented 12 years ago

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.

rwz commented 12 years ago

@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.

iainbeeston commented 12 years ago

@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.

mkdynamic commented 12 years ago

@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.

iainbeeston commented 12 years ago

@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!)

iainbeeston commented 12 years ago

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? :-\

markopavlovic commented 12 years ago

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?

tekin commented 12 years ago

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...

iainbeeston commented 12 years ago

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...

ryansobol commented 12 years ago

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:

  1. Your app needs to interact with Facebook's Graph API (e.g. create a wall post, upload a photo, etc.)
  2. AND your user's token has expired
mkdynamic commented 12 years ago

@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/.

jonhyman commented 12 years ago

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.

tekin commented 12 years ago

@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....

iainbeeston commented 12 years ago

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.

markopavlovic commented 12 years ago

I don't get it, but actually when I request an old offline_accsess I get back a 60days token :S

nandayadav commented 12 years ago

@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.

tekin commented 12 years ago

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.

joshco commented 12 years ago

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

nandayadav commented 12 years ago

@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

jonhyman commented 12 years ago

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.

joshco commented 12 years ago

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?

joshco commented 12 years ago

also, is there a way to force it to use server side requests? how do I make my requests signed?

phuongnd08 commented 11 years ago

I'm using omniauth-facebook right now and keep getting 2-hours expired token using the server side flow. Any thoughts?

phuongnd08 commented 11 years ago

To be clear I'm still using gem 'omniauth-facebook', '1.4.0' as I encountered stop-bug with later version.

corwinstephen commented 11 years ago

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.

joshco commented 11 years ago

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.

phuongnd08 commented 11 years ago

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.

dopa commented 11 years ago

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
mkdynamic commented 10 years ago

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.