Search code examples
pythongoogle-app-enginefacebook-graph-apifacebookfacebook-oauth

How to enable Facebook OAuth 2.0 completely serverside?


I basically want to save the facebook ID of a given user so I can receive more material via Facebook. Ideally I want a solution that neither uses javascript nor cookie, just serverside but there was no example, just directions, so I've put together one we can discuss. Here's the code that I think works when I just link a user to the OAuth dialog for my website:

https://www.facebook.com/dialog/oauth?client_id=164355773607006&redirect_uri=http://www.kewlbusiness.com/oauth

Then I handle it like this to get the userdata:

class OAuthHandler(webapp2.RequestHandler):
    def get(self):
      args = dict(
        code = self.request.get('code'),
        client_id = facebookconf.FACEBOOK_APP_ID,
        client_secret = facebookconf.FACEBOOK_APP_SECRET,
        redirect_uri = 'http://www.koolbusiness.com/oauth',
      )
      file = urllib.urlopen("https://graph.facebook.com/oauth/access_token?" + urllib.urlencode(args))
      try:
        token_response = file.read()
      finally:
        file.close()
      access_token = cgi.parse_qs(token_response)["access_token"][-1]
      graph = facebook.GraphAPI(access_token)
      user = graph.get_object("me")   
      self.response.out.write(user["id"])
      self.response.out.write(user["name"])

So with this I can "log in with Facebook" for my website without a lot of messy javascript and cookies we don't want. I wanted to enable "log in with Facebook" for my website. It should work without cookies and without javascript but the first thing they try to make you do is Javascript and cookies. So I've made a solution that seems to work without cookie and without javascript, just OAuth 2.0: Can you say something about my "solution"? The practical use I'm looking for is enabling simple function which FB user did what on my website and admitting facebook accounts to log in the way that is getting standardized.

I just thought it must work without javascript SDK and without cookie and it seems it is the case. Can you tell what are my advantages and disdvantages with this "solution"? I think it's much better than Javascript + Cookie so why do they trick us to use javascript and cookie when the minimal example seems to be 100 % serverside?

Thank you

Update It seems right and behaves right and I can also use the userdata with the datatore and render my FB name to the front page with no javascript and no cookie, just python:

class FBUser(db.Model):
    id = db.StringProperty(required=True)
    created = db.DateTimeProperty(auto_now_add=True)
    updated = db.DateTimeProperty(auto_now=True)
    name = db.StringProperty(required=True)
    profile_url = db.StringProperty()
    access_token = db.StringProperty(required=True)
    name = db.StringProperty(required=True)
    picture = db.StringProperty()
    email = db.StringProperty()
    friends = db.StringListProperty()
    dirty = db.BooleanProperty()

class I18NPage(I18NHandler):

    def get(self):
    if self.request.get('code'):
          args = dict(
            code = self.request.get('code'),
            client_id = facebookconf.FACEBOOK_APP_ID,
            client_secret = facebookconf.FACEBOOK_APP_SECRET,
            redirect_uri = 'http://www.kewlbusiness.com/',
          )
      logging.debug("client_id"+str(args))
          file = urllib.urlopen("https://graph.facebook.com/oauth/access_token?" + urllib.urlencode(args))
          try:
        logging.debug("reading file")
            token_response = file.read()
        logging.debug("read file"+str(token_response))
          finally:
            file.close()
          access_token = cgi.parse_qs(token_response)["access_token"][-1]
          graph = main.GraphAPI(access_token)
          user = graph.get_object("me")   #write the access_token to the datastore
      fbuser = main.FBUser.get_by_key_name(user["id"])
          logging.debug("fbuser "+str(fbuser))

          if not fbuser:
            fbuser = main.FBUser(key_name=str(user["id"]),
                                id=str(user["id"]),
                                name=user["name"],
                                profile_url=user["link"],
                                access_token=access_token)
            fbuser.put()
          elif fbuser.access_token != access_token:
            fbuser.access_token = access_token
            fbuser.put()
           self.render_jinja(
                'home_jinja',request=self.request,fbuser=user,...

having the variable fbuser for a facebook user and the variable user for a google user now admits me to use facebook for my website without buggy messy unneccessary javascript + cookie.

Now I could render and view my facebook name from my website which is great that it finally works the way it's supposed to indepedent of javascript and doesn't need a cookie.

Why does the documentation recommend javascript + cookie when serverside OAuth 2.0 is the cleanest solution? Do you agree that this is the best solution since it doesn't depend on if you use javascript or cookie?

Update Possible duplicate question when I now have seen that other guys couldn't log out with server code, they had to resort to the Javascript SDK and a requirement can be that it must and should work with no javascript so I did some debugging and found that "clearing" the cookie and I had to change the name of the cookie and though it works I would like your comment and/or test how you think my project solved this. A logout link like this one should work but it doesn't. If I log out twice it works and that's why this is a strange bug since I could fix it but I still don't know what makes logout require a 2nd hit:

https://www.facebook.com/logout.php?next=http://www.myappengineproject.com&access_token=AAACVewZBArF4BACUDwnDap5OrQQ5dx0jsHEKPJkIJJ8GdXlYdni5K50xKw6s8BSIDZCpKBtVWF9maHMoJeF9ZCRRYM1zgZD

It looks like I was not the only one trying to avoid javascript altogether for a solution with OAuth 2.0 serverside. People could do everything but they couldn't logout:

Facebook Oauth Logout

The official documentation for OAuth 2.0 with Facebook says:

You can log a user out of their Facebook session by directing them to the following URL:

https://www.facebook.com/logout.php?next=YOUR_URL&access_token=ACCESS_TOKEN

YOUR_URL must be a URL in your site domain, as defined in the Developer App.

I wanted to do everything serverside and I found that the suggested way to link leaves the cookie so that the logout link doesn't work: https://www.facebook.com/logout.php?next=http://{{host}}&access_token={{current_user.access_token}} It does redirect but it doesn't log the user our of my website. It seemed like a Heisenbug since this was changing to me and there was not much documentation. I anyway seemed to be able to achieve the functionality with a handler that manipulates the cookie so that in effect the user is logged out:

class LogoutHandler(webapp2.RequestHandler):
    def get(self):
        self.set_cookie("fbsr_" + facebookconf.FACEBOOK_APP_ID, None, expires=time.time() - 86400)
        self.redirect("/")
    def set_cookie(self, name, value, expires=None):

        if value is None:
            value = 'deleted'
            expires = datetime.timedelta(minutes=-50000)
        jar = Cookie.SimpleCookie()
        jar[name] = value
        jar[name]['path'] = '/'
        if expires:
            if isinstance(expires, datetime.timedelta):
                expires = datetime.datetime.now() + expires
            if isinstance(expires, datetime.datetime):
                expires = expires.strftime('%a, %d %b %Y %H:%M:%S')
            jar[name]['expires'] = expires
        self.response.headers.add_header(*jar.output().split(': ', 1))

So mapping the handler to /auth/logout and setting this to the link effectively logs the user out of my site (without logging the user out of facebook, hopefully and untested)

Some other parts of my code is handling the OAuth tokens and cookie lookups for the Oauth communication:

def get(self):
    fbuser=None
    profile = None
    access_token = None
    accessed_token = None
    logout = False
    if self.request.get('code'):
      args = dict(
        code = self.request.get('code'),
        client_id = facebookconf.FACEBOOK_APP_ID,
        client_secret = facebookconf.FACEBOOK_APP_SECRET,
        redirect_uri = 'http://self.get_host()/',
      )
      file = urllib.urlopen("https://graph.facebook.com/oauth/access_token?" + urllib.urlencode(args))
      try:
        token_response = file.read()
      finally:
        file.close()
      access_token = cgi.parse_qs(token_response)["access_token"][-1]
      graph = main.GraphAPI(access_token)
      user = graph.get_object("me")   #write the access_token to the datastore
      fbuser = main.FBUser.get_by_key_name(user["id"])
      logging.debug("fbuser "+fbuser.name)

      if not fbuser:
        fbuser = main.FBUser(key_name=str(user["id"]),
                            id=str(user["id"]),
                            name=user["name"],
                            profile_url=user["link"],
                            access_token=access_token)
        fbuser.put()
      elif fbuser.access_token != access_token:
        fbuser.access_token = access_token
        fbuser.put()

    current_user = main.get_user_from_cookie(self.request.cookies, facebookconf.FACEBOOK_APP_ID, facebookconf.FACEBOOK_APP_SECRET)
    if current_user:
      graph = main.GraphAPI(current_user["access_token"])
      profile = graph.get_object("me")
      accessed_token = current_user["access_token"]

I didn't make a loginhandler since login basically is the code above at my root request handler. My user class is as follows:

class FBUser(db.Model):
    id = db.StringProperty(required=True)
    created = db.DateTimeProperty(auto_now_add=True)
    updated = db.DateTimeProperty(auto_now=True)
    name = db.StringProperty(required=True)
    profile_url = db.StringProperty()
    access_token = db.StringProperty(required=True)
    name = db.StringProperty(required=True)
    picture = db.StringProperty()
    email = db.StringProperty()

I mocked together two basic providers enter image description here And I use the variable current_user for the facebook user and the variable user for the google user and the variable fbuser for a user who is logging in and therefore has no cookie match.

The cookie lookup code I use is the following and I think I understand it and that it does what I want:

def get_user_from_cookie(cookies, app_id, app_secret):
    """Parses the cookie set by the official Facebook JavaScript SDK.

    cookies should be a dictionary-like object mapping cookie names to
    cookie values.

    If the user is logged in via Facebook, we return a dictionary with the
    keys "uid" and "access_token". The former is the user's Facebook ID,
    and the latter can be used to make authenticated requests to the Graph API.
    If the user is not logged in, we return None.

    Download the official Facebook JavaScript SDK at
    http://github.com/facebook/connect-js/. Read more about Facebook
    authentication at http://developers.facebook.com/docs/authentication/.
    """
    logging.debug('getting user by cookie')
    cookie = cookies.get("fbsr_" + app_id, "")
    if not cookie:
        logging.debug('no cookie found')
        return None
    logging.debug('cookie found')
    response = parse_signed_request(cookie, app_secret)
    if not response:
        logging.debug('returning none')
        return None

    args = dict(
        code = response['code'],
        client_id = app_id,
        client_secret = app_secret,
        redirect_uri = '',
    )

    file = urllib.urlopen("https://graph.facebook.com/oauth/access_token?" + urllib.urlencode(args))
    try:
        token_response = file.read()
    finally:
        file.close()

    access_token = cgi.parse_qs(token_response)["access_token"][-1]
    logging.debug('returning cookie')
    return dict(
        uid = response["user_id"],
        access_token = access_token,
    )

I had to learn cookies to solve this and I hope you can comment more. Outputting the welcome message required 3 variables, one for google user, one for logged in facebook user and the variable fbuser for the case mentioned in the answer:

JavaScript/Cookies are used when you're trying to authenticate an new user. That aren't exists in your database, and you don't have his accessToken.

So I had to use 3 variables, maybe you can do it with only 2 variables?

    {% if user or current_user or fbuser %}
        <div id="user-ident">
            <span>{% trans %}Welcome,{% endtrans %} <b>{{ current_user.name }}{% if not current_user and user %}{{ user.nickname() }}{% endif %}{% if not current_user and not user and fbuser %}{{ fbuser.name }}{% endif %}</span>
        </div>
        {% endif %}

Solution

  • JavaScript/Cookies are used when you're trying to authenticate an new user. That aren't exists in your database, and you don't have his accessToken.

    When you you have user's accessToken - you can access Facebook API from any programing language, through HTTP, without cookies/javascript if you wish. There is python client, for example: http://github.com/pythonforfacebook/facebook-sdk (was https://github.com/facebook/python-sdk)