Question

I'm having trouble auth'ing into the Github API as an application in GAE (GAE throws exceptions when I use Github3).

import os, sys
sys.path.append("lib")
import jinja2, webapp2, urllib

from google.appengine.api import users, oauth, urlfetch

JINJA_ENVIRONMENT = jinja2.Environment(
    loader=jinja2.FileSystemLoader(os.path.dirname(__file__)),
    extensions=['jinja2.ext.autoescape'],
    autoescape=True)

class ConsoleLogin(webapp2.RequestHandler):

    def get(self):
        google_user = users.get_current_user()

        if google_user:
            fields = {
                "client_id" : os.environ.get('CLIENT_ID'),
                "scope" : "user, repo"
            }
            url = 'https://github.com/login/oauth/authorize'
            data = urllib.urlencode(fields)
            result = urlfetch.fetch(url=url,
                payload=data,
                method=urlfetch.GET
            )

After this point in the code, you're supposed to get a temporary code from Github.

PROBLEM: I simply can't find it. I saw in the guides that you're supposed to fetch it as a environment variable, but I can't see it.

Extra points for anyone who helps me finish the script in Python. ;)

Was it helpful?

Solution 3

I'm not saying it's pretty - it's not. This code is ugly as sin, but it works using GAE, Webapp2 and urllib2, rather than other frameworks/libraries.

import os, sys, cgi, json, cookielib
sys.path.append("lib")
import jinja2, webapp2, urllib, urllib2

from google.appengine.api import users, oauth, urlfetch
from webapp2_extras import sessions

JINJA_ENVIRONMENT = jinja2.Environment(
    loader=jinja2.FileSystemLoader(os.path.dirname(__file__)),
    extensions=['jinja2.ext.autoescape'],
    autoescape=True)

class BaseHandler(webapp2.RequestHandler):
    def dispatch(self):
        # Get a session store for this request.
        self.session_store = sessions.get_store(request=self.request)

        try:
            # Dispatch the request.
            webapp2.RequestHandler.dispatch(self)
        finally:
            # Save all sessions.
            self.session_store.save_sessions(self.response)

    @webapp2.cached_property
    def session(self):
        # Returns a session using the default cookie key.
        return self.session_store.get_session()

class ConsoleLogin(BaseHandler):
    def get(self):
        # Set variables to avoid problems later
        code = ''
        url = ''
        access_token = ''
        scope = ''
        username = ''

        google_user = users.get_current_user()

        if google_user:
            url = self.request.url
            if ('code' not in url and not self.session.get('access_token')):
                # First time user coming to site. Redirect to GH for code
                url = 'https://github.com/login/oauth/authorize?scope=user,repo&client_id=' + os.environ.get('CLIENT_ID')
                self.redirect(url)
            elif 'code' in url:
                # User has been to GH, continue with auth process
                code = url.replace('http://localhost:8080/?code=', '')

                # We have code, now get Access Token
                fields = {
                    "client_id" : os.environ.get('CLIENT_ID'),
                    "client_secret" : os.environ.get('CLIENT_SECRET'),
                    "code" : code
                }
                url = 'https://github.com/login/oauth/access_token'
                data = urllib.urlencode(fields)
                result = urlfetch.fetch(url=url,
                    payload=data,
                    method=urlfetch.POST
                )

                # Get the query string
                query_string = str(result.content)

                # Get the access token out of the full query string
                access_token = query_string[13:]
                end_access = access_token.find('&')
                access_token = access_token[:end_access]

                # Get the scope out of the full query string
                start_scope = query_string.find('scope')
                end_scope = query_string.find('token_type')
                start_scope = start_scope + 6   # remove the word 'scope='
                end_scope = end_scope - 1       # remove the & symobol
                scope = query_string[start_scope:end_scope]
                scope = scope.split('%2C')

            # Store the Access Token in a Session Variable
            self.session['access_token'] = access_token
            self.session['scope'] = scope

            # And redirect to the base URL for neatness and to avoid other issues
            self.redirect('/')

            access_token = self.session.get('access_token')
            scope = self.session.get('scope')

            context = {
                'access_token' : access_token,
                'scope' : scope,
                'username' : username,
            }

            # Template Settings
            temp = 'templates/index.html'

            template = JINJA_ENVIRONMENT.get_template(temp)
            self.response.write(template.render(context))


config = {}
config['webapp2_extras.sessions'] = {
    'secret_key': 'the-beatles-will-always-rule',
}

application = webapp2.WSGIApplication([
    ('/', ConsoleLogin),
], debug=True, config=config)

OTHER TIPS

Here is the actual implementation for GitHub oAuth authentication. Its is build on Flask instead of Webapp2 but you can easily port the handler to Webapp2. You can have a look to a gae bootstrap project gae-init and the particular snippet was take from a fork that accommodates various oAuth provides gae-init-auth. (note: decorator @github.tokengetter is provided by flask_oauth.py)

github_oauth = oauth.OAuth()

github = github_oauth.remote_app(
    'github',
    base_url='https://api.github.com/',
    request_token_url=None,
    access_token_url='https://github.com/login/oauth/access_token',
    authorize_url='https://github.com/login/oauth/authorize',
    consumer_key=config.CONFIG_DB.github_client_id,
    consumer_secret=config.CONFIG_DB.github_client_secret,
    request_token_params={'scope': 'user:email'},
  )


@app.route('/_s/callback/github/oauth-authorized/')
@github.authorized_handler
def github_authorized(resp):
  if resp is None:
    return 'Access denied: error=%s' % flask.request.args['error']
  flask.session['oauth_token'] = (resp['access_token'], '')
  me = github.get('user')
  user_db = retrieve_user_from_github(me.data)
  return signin_user_db(user_db)


@github.tokengetter
def get_github_oauth_token():
  return flask.session.get('oauth_token')


@app.route('/signin/github/')
def signin_github():
  return github.authorize(
      callback=flask.url_for('github_authorized',
          next=util.get_next_url(),
          _external=True,
        )
    )


def retrieve_user_from_github(response):
  auth_id = 'github_%s' % str(response['id'])
  user_db = model.User.retrieve_one_by('auth_ids', auth_id)
  if user_db:
    return user_db
  return create_user_db(
      auth_id,
      response['name'] or response['login'],
      response['login'],
      response['email'] or '',
    )

You seem to be missing a few items when you run your OAuth authorize request. According to the GitHub API docs, you need to pass 4 parameters to the authorize request (this is the standard for OAuth 2 protocol anyway):

  • the client_id (it comes from your GitHub app registration - are you sure it resides in an OS environment variable? Did you put it there yourself? For testing purposes you could start by putting it as a plain string in code ; you'll do better later once everything works fine) ;
  • the scope (you've already defined it - this is OK) ;
  • a random state that you choose and that will be echoed back to you by GitHub in the next step ;
  • and, more importantly, a redirect_uri to which GitHub will forward the client to once the user has allowed access to his account: it must be a URL on your own website that you will need to handle to fetch both code and state parameters

The redirect_uri could be - for example - http://localhost:8080/oauth/accept_github, and then you need to prepare your app.yaml file and Python code to handle requests made to /oauth/accept_github. In the code that handles these requests, try to show the content of: self.request.get('state') and self.request.get('code'): if everything works fine, they should contain what the GitHub API sends you back. Now you're ready for the next step: convert your code into an access_token :)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top