jgorset / fandjango

Fandjango makes it really easy to create Facebook applications with Django
MIT License
253 stars 56 forks source link

Support for Facebook Mobile Web apps #73

Closed kdmukai closed 12 years ago

kdmukai commented 12 years ago

Fandjango is working great for my Canvas app. Thank you so much!!

Now I'm trying to add Mobile Web support by just rendering an alternate, mobile-friendly template. That's working fine thanks to some middleware.

But the facebook_authorization_required decorator is causing problems for the mobile site.

Here's the issue, as best as I understand it:

Facebook handles Mobile Web apps very differently from Canvas apps. The only real integration point with facebook is the Mobile Web URL that's specified in the facebook app dashboard.

Mobile Web apps are not hosted within a mobile version of the Canvas iframe. Facebook Mobile Web apps aren't even really "apps" - they're basically just mobile-friendly websites that use facebook for authentication and that facebook will direct link out to if it detects that you're on a mobile device. Facebook automatically handles the switching between the Canvas URL vs the Mobile Web URL, depending on how you try to access the app (desktop vs mobile device).

But the current facebook_authorization_required decorator uses a default redirect back into the Canvas URL. On a mobile device that redirect authorization fails and returns:

API Error Code: 196 API Error Description: Cannot redirect to desktop web canvas URL on a mobile device Error Message: redirect_url is not owned by the application

The API Error Description makes sense to me (even though the Error Message doesn't seem correct--I think it's just confused by the Mobile Web vs Canvas App conflict).

So I tried to edit Fandjango's redirect logic. First I added a new value in settings.py:

FACEBOOK_APPLICATION_MOBILE_WEB_HOST = 'm.mydomain.com'

And then in get_post_authorization_redirect_url:

# Mobile Web apps need to be redirected back to the MOBILE_WEB_HOST, *not* the CANVAS_URL
current_host = request.META.get('HTTP_HOST', '')
if current_host == settings.FACEBOOK_APPLICATION_MOBILE_WEB_HOST:
    redirect_uri = 'http://%(domain)s%(path)s' % {
        'domain': current_host,
        'path': path
    }

else:    
    redirect_uri = 'http://%(domain)s/%(namespace)s%(path)s' % {
        'domain': FACEBOOK_APPLICATION_DOMAIN,
        'namespace': FACEBOOK_APPLICATION_NAMESPACE,
        'path': path
    }

print('redirect_uri: %s' % redirect_uri)

I confirmed that the redirect_uri now correctly points back to the intended page on the Mobile Web domain for mobile users.

But this is causing an infinite authentication loop.

The first authentication request shows up as a 401, but then the redirect arrives with a ?code=xxxxxxxx param added, presumably by facebook. My code then mistakes this as another request to that same page, but it thinks it still needs to be authenticated, so it sends another authentication request out... and over and over.

The "code" param coming back from facebook seems promising, but I can't tell where in the Fandjango magic it's supposed to be handled. Right now it looks like that param is just being ignored.

I've taken this as far as I can go. I'm hoping this is an easy fix that I'm just not seeing.

thanks!

Morpho commented 12 years ago

Hi kdmukai, I already tried the same. I think its not so easy to do that because fandjango just handles "signed requests" which are sent via POST when apps are inside the facebook-canvas. Mobile apps, desktop apps and external websites wont have that.

To make that work you have to implement the following inside fandjango's middleware: https://developers.facebook.com/docs/authentication/server-side/

Basically:

  1. Grab the "code" GET-param
  2. Exchange it for an access_token
  3. Authenticate user

Using this form of authentication, fandjango would work for desktop, web-canvas and mobile.

jgorset commented 12 years ago

@morpho is exactly right; Fandjango currently only supports signed requests, but I'm happy to accept pull requests that extend it to exchange authentication codes for access tokens.

Morpho commented 12 years ago

Ok, I looked further into that. Its actually not too complicated to create that behaviour. @jgorset , thanks to your excellent work with facepy we could just fake a signed_request, built with that access_token we obtain after we exchanged the "code"-param.

We just have to extend middleware.py with something like that:

if 'code' in request.GET:
   tokenAndExpires = urllib.urlopen('https://graph.facebook.com/oauth/access_token?client_id=%s&redirect_uri=%s&client_secret=%s&code=%s...')
   signed_request_from_code = generateFromToken(tokenAndExpires['token'], tokenAndExpires['expires'])
else:
   signed_request_from_code = None

... a bit further on ...

if signed_request_from_code or 'signed_request' in request.REQUEST or 'signed_request' in request.COOKIES:
   ...

The generateFromToken function would be very similar to "facepy.SignedRequest.generate", something like that:

def generateFromToken(token, expires):
    payload = {
        'algorithm': 'HMAC-SHA256'
    }

    g = GraphAPI(token)
    me = g.get('me')

    payload['user'] = {}
    payload['user_id'] = me['id']
    payload['oauth_token'] = token
    payload['expires'] = int(time.mktime(datetime.now().timetuple()))+int(expires)
    payload['issued_at'] = int(time.mktime(datetime.now().timetuple()))
    encoded_payload = base64.urlsafe_b64encode(
        json.dumps(payload, separators=(',', ':'))
    )
    encoded_signature = base64.urlsafe_b64encode(hmac.new(FACEBOOK_APPLICATION_SECRET_KEY, encoded_payload, hashlib.sha256).digest())
    return '%(signature)s.%(payload)s' % {
        'signature': encoded_signature,
        'payload': encoded_payload
    }

I will try to make it work... It would be perfect to have a fandjango that also works for standalone websites and mobile apps and I think there are only a few parts missing. But I'm still not sure if it's a good idea to fake Signed Requests like that, but it would definetly work.

jgorset commented 12 years ago

I think it would probably be easier and more intuitive if we didn't generate signed requests. It might be worth considering to rename FacebookMiddleware to SignedRequestMiddleware and use a similiar implementation to initialize Fandjango using a different authentication mechanism, which would be mostly transparent to the user.

Morpho commented 12 years ago

Ok you are right, that would be a much more cleaner approach. Thanks for pointing me in that direction. I had the idea of faking SignedRequest because you store the signed request in a cookie to keep the session alive. Having more data in the session, which would be necessary when using another middleware, would be a litlte bit redundant, but anyway, its way more cleaner. What do you think about storing the request.facebook object directly in the session/cookie?

Morpho commented 12 years ago

I wrote an extra middleware for mobile-app support: https://github.com/Morpho/fandjango/commit/7a03ee8b307e51697f4dd56fb433d4638db17f76 (This is the first "git push" ever in my life!) ;)

@jgorset, I'd be glad if you could take a look at it. It's still not perfect but it works very well. But I'm too shy to send a pull request yet.. ;) I use a separate django-app called minidetector to check if the user is using a mobile device and I modified FacebookMiddleware() to support only non-mobile devices. So only one of two middlewares will be executed depending on whether the user is using a mobile device or not.

FacebookMobileMiddleware() will save the oauth_token inside the django session if there is any and handle everything properly.

There is a lot of repetitive code. Maybe we could even have a BaseFacebookMiddleware and extend it for "mobile", "external", "canvas" etc.. And I'm not sure about those paths either. I introduced a new settings variable called FACEBOOK_APPLICATION_MOBILE_URL but afterwards I realized that it might not even be necessary, because the mobile URL will almost always be the absolute URL of the django project.

So I'd love to hear your feedback!

jgorset commented 10 years ago

Hi @Morpho!

I was just browsing old issues about non-canvas applications to collect my thoughts in regards to #98 and realized I must have missed your comment before. I hope I didn't give you the wrong impression — I certainly didn't mean to ignore you.

Morpho commented 10 years ago

Hey @jgorset, no worries, I'm sure you re quite busy. Actually I'm glad that some process has been done by @ademuk using parts of my code. This is great and I it would be even better if we could have mobile-support in Fandjango's master one day. I use my mobileMiddleware-approach in my projects and it works well for me. But its by far not ready for a pull.

But Im thinking about writing some updates. So what do you think? Do you want to support mobile devices? And what about middlewares? What do you think about the BaseMittleware approach? What is the best way to store the token?

jgorset commented 10 years ago

I'm not sure yet! I'd love to hear some compelling arguments for using Fandjango over a clear-cut OAuth implementation like Django Social Auth.