spring-attic / spring-social

Allows you to connect your applications with SaaS providers such as Facebook and Twitter.
http://projects.spring.io/spring-social
Apache License 2.0
619 stars 351 forks source link

Support for new Facebook endpoint to extend access token lifetime #57

Open JohnWPhillips opened 12 years ago

JohnWPhillips commented 12 years ago

Facebook has begun supporting an OAuth2 extension to allow a server side request to exchange a short-lived Access Token for a Long-lived Access Token. I've added support for this into the spring-social OAuth2Operations interface and the spring-social-facebook FacebookOAuth2Template, please consider this for addition into a future spring-social release.

Since this is Facebook-specific an alternate implementation would be to subclass the OAuth2Operations interface in the spring-social-facebook package and have the user upcast the object from connectionFactory.getOAuthOperations(). This would be better because it would not add a Facebook specific API to the base class interface, however, it may be possible that what Facebook has done will somehow become part of a future OAuth standard and so it will need to be added to the base API at some point anyway.

Certainly interested in any insights SpringSource has into Facebook's deprecation of offline_access mode. Feel free to contact me at jphillips@tango.me for more info about my submitted changes.

habuma commented 12 years ago

See my comments on the other pull request you submitted.

Also, I already have a similar piece of code in place in a local branch that does not require any changes to Spring Social Core. But as I said in the other pull request, the endpoint can only be used for client-side extension of token expiration.

As for this someday becoming part of the OAuth standard, I have serious doubts. The OAuth 2 spec isn't final yet, but it also hasn't changed dramatically since around draft 14 and I believe it is inching closer to being final. Adding something like this would no doubt disrupt that specification process, further prolonging finalization of the spec.

Furthermore, the OAuth 2 specification already defines the notion of refresh tokens which solve this problem in a cleaner way than what Facebook has come up with. It's unfortunate that Facebook is not implementing the refresh token portion of the specification that Facebook itself is helping to write. I suspect that the odds are greater that Facebook will implement refresh tokens before the OAuth 2 spec will define Facebook's approach. But time will tell...until then, I'm not eager to add anything to Spring Social Core to directly support anything provider specific such as this.

JohnWPhillips commented 12 years ago

Thanks for the input, I appreciate it. My interpretation of what's going on is based on how mobile app development is going, and is based on the following 3 principles:

  1. Facebook wants to make it easy to do FB integration with a client-side mobile app
  2. Facebook wants to accommodate server-side API access in mobile apps
  3. Facebook wants to disallow unbounded refresh of access just because a mobile user has granted it once
  4. The developer should not keep his app secret client-side

How they're doing this is to have a client-side flow with short-lived access tokens which you can get just by using your app ID. I have been told that on the Facebook side they're doing client authentication before granting a short-lived access token, the request has to come from your iOS or Android app credentials that you have registered with your Facebook app. This means that a server cannot request via the short-lived access token flow.

To get a long-lived access token via the server side flow requires either a server-side Facebook OAuth2 dance (appropriate for browser based software, not mobile apps) or the extension of a short-lived token to a long-lived one. For mobile apps which wish both client and server side Facebook access, then, the flow is for the mobile client to do the dance and get the short-lived token, then pass it to the server for the server to extend and use. If the mobile app client gets uninstalled or is not running then after 60 days the server is no longer able to use the Facebook API.

The bad news is that this is all theory so far :) I've been looking at this issue for just a few days and am starting experimentation with real code. I did the spring-social pull request to try to seed the effort to add this new API, because from my perspective getting something added to spring-social is a long-lead-time item :) My statement about the OAuth2 standard addition is pure speculation.

Thanks for the prompt response. As I develop the TangoMe Facebook integration I'll keep you posted of anything I discover.

John

On 5/11/12 8:01 AM, "Craig Walls" <reply+i-4526254-d9935a38b9392b54175b3e1c8a03c3893ba0971a-1728217@reply.git hub.com> wrote:

See my comments on the other pull request you submitted.

Also, I already have a similar piece of code in place in a local branch that does not require any changes to Spring Social Core. But as I said in the other pull request, the endpoint can only be used for client-side extension of token expiration.

As for this someday becoming part of the OAuth standard, I have serious doubts. The OAuth 2 spec isn't final yet, but it also hasn't changed dramatically since around draft 14 and I believe it is inching closer to being final. Adding something like this would no doubt disrupt that specification process, further prolonging finalization of the spec.

Furthermore, the OAuth 2 specification already defines the notion of refresh tokens which solve this problem in a cleaner way than what Facebook has come up with. It's unfortunate that Facebook is not implementing the refresh token portion of the specification that Facebook itself is helping to write. I suspect that the odds are greater that Facebook will implement refresh tokens before the OAuth 2 spec will define Facebook's approach. But time will tell...until then, I'm not eager to add anything to Spring Social Core to directly support anything provider specific such as this.


Reply to this email directly or view it on GitHub: https://github.com/SpringSource/spring-social/pull/57#issuecomment-5652691

habuma commented 12 years ago

Regarding point #3, I see nothing wrong with "unbounded refresh", when it is, in fact, not unbounded. The user may at any time revoke access and shutdown the refresh.

My problem with doing a refresh at the browser-level (via the authorization flow) as FB requires it is that you may not realize that you need to refresh until you're deeper in your application code. A per-the-spec refresh token would allow the server-side code to put its work on hold, refresh the token, then carry on. But browser-refresh will require a lot of special handling to keep the application flow uninterrupted. My other problem with doing a refresh at the browser level is simply that it is not how it's defined in the specification and therefore I must have special-case code to cover FB's non-compliance with the spec.

Your thought on letting a client get the short-lived token and then allowing a server to refresh from that is a valid one and I had thought of that scenario. I'll consider it some more and decide how I want to proceed. It kind of demands that there be an example of that to showcase it, though...as well as some very clear documentation indicating that you shouldn't expect a long-lived token to be extended any further.

I wonder, however...FB's docs say two conflicting things: (1) You can only extend short-lived tokens, not long-lived tokens and (2) you can extend the life of a token every time the user visits your up, up to once per day. So, if you can only extend short-lived tokens, how can you continue to extend the life of a token once per day if the only token that will survive into tomorrow is the long-lived token?

And, what happens if you get a short-lived token, extend it to a long-lived token, and then the 60-days is almost up? The short-lived token is no longer any good and you can't refresh long-lived tokens. Therefore you're going to have to go through the authorization flow again anyway.

And, it's unclear on what FB means when they say "client-side". I interpret it as JavaScript code, but others interpret it as mobile (iOS/Android) code. It may even be that both are true. But if there's any truth to the JavaScript part of it, then how would a client extend the life of a short-lived token unless it either relies on the server to do it or if it carries the secret in the JS code.

habuma commented 12 years ago

This wasn't what I had planned to work on today, but...since it is a hot topic and will become more important as we approach July 5, I thought I'd give it another look.

I've done another sweep of tests and confirmed that the initial token granted to the server side is a long-lived 60-day token. I've also confirmed that calling the aforementioned endpoint with that token does nothing to extend the life of it. (Of course, I really should try again tomorrow to be certain that I'm not just hitting the once-per-day rule...but I have confirmed this in the past.)

As I mentioned before, no matter how you end up with a long-lived token (client or server), you're going to need to freshen it up before the 60 days is up and the only way to do that is to go through the authorization flow again (because you can only extend the life of short-lived tokens, not long-lived tokens). Fortunately, this doesn't bother the user because as long as the user has not revoked access, the authorization will still be good and Facebook will immediately redirect back to the app with a new authorization code to exchange for a fresh new access token.

Therefore, whether the token has expired or not, the only way to get a fresh long-lived token after the initial long-lived token (as I understand it) is to go through the authorization flow again.

So, one approach is for you to purposefully take the user through the authorization flow when they first come to your app on a given day and replace the existing connection with the new connection data. Or, you could wait until an ExpiredAuthorizationException is thrown and go through the process then. (Actually, it might be good to do this for any NotAuthorizedException.)

At the moment, there's no support for either case in Spring Social. But here's how it might work (none of this is confirmed...just some sketchings I made):

These descriptions are purposefully brief so as not to get lost in the details of what should happen. I'm certain that gotchas will be encountered that I haven't thought of.

One potential gotcha is dealing with the transactionality of the original request. Spring's transaction support should rollback anything that can be rolled back when a runtime exception is thrown, but what about non-transactional work that may have been done prior to the exception being thrown? Say, if a tweet was posted to Twitter and then an attempt to post to a person's Facebook wall is met with a NotAuthorizedException...how would that be handled? (Truth is, that's a problem right now and not only with Spring Social, but with any app that could perform non-transactional work before a transaction-killing exception is thrown.)

habuma commented 12 years ago

The aforementioned filter/interceptor could also check for how much life is left on a token and if it falls under some configurable threshold go ahead and do the authorization flow as described above. (As an option to the once-per-visit-per-day approach.)

JohnWPhillips commented 12 years ago

I believe the problem that Facebook may be trying to solve is that when a mobile user uninstalls an app it does not remove that vendor's server-side access grant to that user's data. The Facebook user can remove this access grant manually via the Facebook web page, but this is not obvious. However, with this new change any vendor whose app is uninstalled will eventually lose the ability to access the user's data.

Because of this new change a mobile application vendor who wishes to have client side and server side access to the Facebook API has the following options:

  1. Do OAuth2 dance client side and pass the token to the server
  2. Do OAuth2 dance server side and pass the token to the client
  3. Do OAuth2 dance both ways
  4. Proxy Facebook API requests from client/server to server/client

Of these, getting the token client side, passing it to the server, and extending it server-side seems like the easiest and most secure way to go. Facilitating this flow may be why this new endpoint has been added.

In the following link http://developers.facebook.com/roadmap/offline-access-removal/ Facebook says of its new endpoint "the endpoint can only be used to extend the short-lived user access_tokens. If you pass an access_token that had a long-lived expiration time, the endpoint will simply pass that same access_token back to you without altering or extending the expiration time." You can try getting a short-lived access token and extending it with the following test:

  1. Run something on http://localhost:8080
  2. Set your Facebook "Site URL" to http://localhost:8080 (assuming your app is still sandboxed/under development)
  3. Request Short-lived access token in your browser https://www.facebook.com/dialog/oauth?client_id=<your client id>&redirect_uri=http://localhost:8080&scope=user_about_me&response_type=to ken

The access token has a ~1-2 hour access period

  1. Extend to a long-lived access token https://graph.facebook.com/oauth/access_token&client_id=<your client id>&client_secret=<your client secret>&grant_type=fb_exchange_token&fb_exchange_token=<short lived token from step 3>

The token returned has a ~60 day expiration period

On 5/11/12 1:24 PM, "Craig Walls" <reply+i-4526254-d9935a38b9392b54175b3e1c8a03c3893ba0971a-1728217@reply.git hub.com> wrote:

The aforementioned filter/interceptor could also check for how much life is left on a token and if it falls under some configurable threshold go ahead and do the authorization flow as described above. (As an option to the once-per-visit-per-day approach.)


Reply to this email directly or view it on GitHub: https://github.com/SpringSource/spring-social/pull/57#issuecomment-5660104

habuma commented 12 years ago

Your explanation of how this impacts mobile makes sense, but I don't see how the same thing couldn't have been accomplished with a per-the-spec refresh token by keeping that refresh token server-side. Eventually the access token kept on the client will expire and the client will have to rely on the server-side to refresh the token. Conceptually, the endpoint Facebook is providing is no different than a refresh token endpoint would be except that you're using the not-yet-expired access token as the refresh token. Why not just implement the spec's refresh token and give guidance to mobile developers to handle the refresh on the server?

And yes, I've performed those same steps you described probably hundreds of times while investigating this. My question remains: What happens when that 60-day token you obtained in step 4 starts growing stale? You no longer have a valid short-lived token to extend it any further and you can't extend a long-lived token. So you have no choice but to do the authorization again.

JohnWPhillips commented 12 years ago

I'm not arguing for/against the Facebook changes, I'm just trying to understand and design to them. I'd like to ask you to consider adding this new API call to spring-social-facebook, as it's likely that I'll be using it in my Facebook integration implementation. Also, I imagined that you personally had walked through the new API using steps similar to the ones previously outlined, I more thought it was worth adding them to give context for anyone else reading this thread.

On 5/14/12 8:02 AM, "Craig Walls" <reply+i-4526254-d9935a38b9392b54175b3e1c8a03c3893ba0971a-1728217@reply.git hub.com> wrote:

Your explanation of how this impacts mobile makes sense, but I don't see how the same thing couldn't have been accomplished with a per-the-spec refresh token by keeping that refresh token server-side. Eventually the access token kept on the client will expire and the client will have to rely on the server-side to refresh the token. Conceptually, the endpoint Facebook is providing is no different than a refresh token endpoint would be except that you're using the not-yet-expired access token as the refresh token. Why not just implement the spec's refresh token and give guidance to mobile developers to handle the refresh on the server?

And yes, I've performed those same steps you described probably hundreds of times while investigating this. My question remains: What happens when that 60-day token you obtained in step 4 starts growing stale? You no longer have a valid short-lived token to extend it any further and you can't extend a long-lived token. So you have no choice but to do the authorization again.


Reply to this email directly or view it on GitHub: https://github.com/SpringSource/spring-social/pull/57#issuecomment-5692317

habuma commented 12 years ago

Completely understood. I did not mean to give the impression that I disagreed with your thoughts. I, too, am trying to get my head around this thing Facebook has done.

That said, I will certainly consider adding the API call. But I do not plan to add it in such a way that it requires any changes to Spring Social Core just to support a Facebook-only quirk. I will seek out a solution that keeps the changes confined to Spring Social Facebook.

Polve commented 11 years ago

I've the same need to extend a short lived access token, but I see this thread is old.

Is there any "standard" method in spring-social-facebook to do it?

Voxelot commented 10 years ago

I'm also looking for this feature, what is the latest news on this?

wongwill86 commented 10 years ago

Also looking for this or similar feature. Not sure if there is a better newer way to do this but it seems like I'm leaning towards @JohnWPhillips's

"Of these, getting the token client side, passing it to the server, and
extending it server-side seems like the easiest and most secure way to go.
 Facilitating this flow may be why this new endpoint has been added."

Also I'm not sure if there was a change to the FB api, but they don't advertise the OAuth2 method of grant_type='refresh_token' anymore. Calling the Facebook/Oauth2Template's refreshAccess function doesn't seem to work. Crafting the post manually doesn't seem to work either.

To use the facebook extend api, I commandeered the existing refreshAccess function by:

MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
                params.add("fb_exchange_token", authToken);
                params.add("grant_type", "fb_exchange_token");
                AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().refreshAccess(authToken, 
                        params);

Is there a better way to do this?

pivotal-issuemaster commented 8 years ago

@JohnWPhillips Please sign the Contributor License Agreement!

Click here to manually synchronize the status of this Pull Request.

See the FAQ for frequently asked questions.