How to setup Github authentication?

In this tutorial, we will authenticate users using Github.

Users obtain a Bearer token from Github and use it in an Authenticatìon header. Leveraging Kinto pluggability, a custom authentication policy is specified in settings in order to validate it.

Custom authentication

Create a file kinto_github.py with the following scaffold:

from pyramid.authentication import CallbackAuthenticationPolicy
from pyramid.interfaces import IAuthenticationPolicy
from zope.interface import implementer

@implementer(IAuthenticationPolicy)
class GithubAuthenticationPolicy(CallbackAuthenticationPolicy):
    def __init__(self, realm='Realm'):
        self.realm = realm

    def unauthenticated_userid(self, request):
        user_id = self._get_credentials(request)
        return user_id

    def forget(self, request):
        return [('WWW-Authenticate', 'Bearer realm="%s"' % self.realm)]

    def _get_credentials(self, request):
        authorization = request.headers.get('Authorization', '')
        print('Check Github')

Don’t be scared by those lines. It just implements the necessary methods to match the IAuthenticationPolicy Pyramid interface.

Add it to Python path

For the simplicity in this tutorial, we will just alter the PYTHONPATH system environment variable. Specify the path to the folder containing the kinto_github.py:

$ export PYTHONPATH="/path/to/folder:${PYTHONPATH}"

In order to test that it works, simply try to import it from a python script:

$ python
Python 2.7.9 (default, Apr  2 2015, 15:33:21)
[GCC 4.9.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import kinto_github
>>>

Enable in configuration

As explained in the settings section, just enable a new policy pointing to your Python class:

multiauth.policies = github basicauth

multiauth.policy.github.use = kinto_github.GithubAuthenticationPolicy

Kinto should start without errors.

Test it

Since we left basicauth in settings, it should still be accepted:

$ http GET http://localhost:8888/v1/ --auth token:alice-token
{
    "cliquet_protocol_version": "2",
    "http_api_version": "1.2",
    "project_docs": "https://kinto.readthedocs.org/",
    "project_name": "kinto",
    "project_version": "1.11.0.dev0",
    "settings": {
        "attachment.base_url": "http://localhost:7777",
        "batch_max_requests": 25,
        "cliquet.batch_max_requests": 25,
        "readonly": false
    },
    "url": "http://localhost:8888/v1/",
    "user": {
        "bucket": "71aefbc6-d333-832b-8e39-18da76d11bae",
        "id": "basicauth:63279e82e351f8f318eea09ae5e3bcfc3b9e3eee06e9befacbf17102e0595dad"
    }
}

And since the github authentication is also enabled (but does nothing yet), you should see its output in the console when a request comes in.

Starting server in PID 8079.
serving on http://0.0.0.0:8888
Check Github
2016-01-26 11:59:04,918 INFO  [cliquet.initialization][waitress] "GET   /v1/" 200 (1 ms) request.summary lang=None; uid=63279e82e351f8f318eea09ae5e3bcfc3b9e3eee06e9befacbf17102e0595dad; errno=None; agent=HTTPie/0.9.2; authn_type=BasicAuth; time=2016-01-26T11:59:04

Github token validation

We don’t want to make a call to the Github API if the request does not use a Github Bearer token.

Let’s limit this policy to requests with github+Bearer in Authorization header.

from pyramid.authentication import CallbackAuthenticationPolicy
from pyramid.interfaces import IAuthenticationPolicy
from zope.interface import implementer

GITHUB_METHOD = 'github+bearer'

@implementer(IAuthenticationPolicy)
class GithubAuthenticationPolicy(CallbackAuthenticationPolicy):
    def __init__(self, realm='Realm'):
        self.realm = realm

    def unauthenticated_userid(self, request):
        user_id = self._get_credentials(request)
        return user_id

    def forget(self, request):
        return [('WWW-Authenticate', '%s realm="%s"' % (GITHUB_METHOD, self.realm)]

    def _get_credentials(self, request):
        authorization = request.headers.get('Authorization', '')
        try:
            authmeth, token = authorization.split(' ', 1)
            authmeth = authmeth.lower()
        except ValueError:
            return None
        if authmeth != GITHUB_METHOD.lower():
            return None
        print('Check Github')

Now using Basic Authentication it should be skipped, but with this request it should print it in the server console:

$ http http://localhost:8888/v1/ "Authorization: github+Bearer foobartoken"

Validate token while obtaining user id from Github

We will simply make a call to the Github user API and try to obtain the login attribute (i.e. user name).

import requests
from cliquet import logger
from pyramid.authentication import CallbackAuthenticationPolicy
from pyramid.interfaces import IAuthenticationPolicy
from zope.interface import implementer

GITHUB_METHOD = 'Github+Bearer'

@implementer(IAuthenticationPolicy)
class GithubAuthenticationPolicy(CallbackAuthenticationPolicy):
    def __init__(self, realm='Realm'):
        self.realm = realm

    def unauthenticated_userid(self, request):
        user_id = self._get_credentials(request)
        return user_id

    def forget(self, request):
        return [('WWW-Authenticate', '%s realm="%s"' % (GITHUB_METHOD, self.realm)]

    def _get_credentials(self, request):
        authorization = request.headers.get('Authorization', '')
        try:
            authmeth, token = authorization.split(' ', 1)
            authmeth = authmeth.lower()
        except ValueError:
            return None
        if authmeth != GITHUB_METHOD.lower():
            return None
        try:
            headers = {"Authorization": "token %s" % token}
            resp = requests.get("https://api.github.com/user", headers=headers)
            resp.raise_for_status()
            userinfo = resp.json()
            user_id = userinfo['login']
        except Exception as e:
            logger.warn(e)
            return None

Let’s try to create an object on Kinto, it should fail using a 401 Unauthorized error response using a dummy token:

$ http PUT http://localhost:8888/v1/buckets/test "Authorization: github+Bearer foobartoken"
HTTP/1.1 401 Unauthorized
Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff
Content-Length: 110
Content-Type: application/json; charset=UTF-8
Date: Tue, 26 Jan 2016 11:07:05 GMT
Server: waitress
Www-Authenticate: Github+Bearer realm="Realm"
Www-Authenticate: Basic realm="Realm"

{
    "code": 401,
    "errno": 104,
    "error": "Unauthorized",
    "message": "Please authenticate yourself to use this endpoint."
}

Test it!

Obtain a Personal Access token

Create a Personal access token using the Github API using your user/pass:

$ echo '{"note": "Kinto Github tutorial"}' | http POST https://api.github.com/authorizations --auth token:user-token

It is returned in the token attribute in the JSON response:

HTTP/1.1 201 Created
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *

{
    "app": {
        "client_id": "00000000000000000000",
        "name": "Kinto Github tutorial",
        "url": "https://developer.github.com/v3/oauth_authorizations/"
    },
    "created_at": "2016-01-26T11:09:02Z",
    "fingerprint": null,
    "hashed_token": "15eb9f...e8aa4502",
    "id": 27212889,
    "note": "kinto",
    "note_url": null,
    "scopes": [],
    "token": "7f7f911969279d8b16a12f44b8bc6e2d216dc51e",
    "token_last_eight": "c30211c6",
    "updated_at": "2016-01-26T11:09:02Z",
    "url": "https://api.github.com/authorizations/27212889"
}

Note

If you have two-factor auth enabled, please refer to the Github API documentation for obtaining a Personal access token using the appropriate headers.

Check your user id

$ http http://localhost:8888/v1/ "Authorization: github+Bearer 7f7f911969279d8b16a12f44b8bc6e2d216dc51e"
HTTP/1.1 200 OK
Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff
Content-Length: 406
Content-Type: application/json; charset=UTF-8
Date: Tue, 26 Jan 2016 11:05:09 GMT
Server: waitress

{
    "cliquet_protocol_version": "2",
    "http_api_version": "1.2",
    "project_docs": "https://kinto.readthedocs.org/",
    "project_name": "kinto",
    "project_version": "1.11.0.dev0",
    "settings": {
        "attachment.base_url": "http://localhost:7777",
        "batch_max_requests": 25,
        "cliquet.batch_max_requests": 25,
        "readonly": false
    },
    "url": "http://localhost:8888/v1/",
    "user": {
        "bucket": "8f730aef-55cb-f1d0-4b0e-c8afbe767c63",
        "id": "github:leplatrem"
    }
}

Use it in permissions

The user id github:<username> can now be used in permissions definitions. It is much more convenient than Basic Auth identifiers!

$ echo '{"permissions": {"read": ["github:leplatrem"]}}' | \
    http PUT http://localhost:8888/v0/buckets/test  --auth='token:another-user-token'

Cache the token validation

Using the following snippet you can cache the association between a token and the user id, in order to avoid requesting Github at each request.

It uses Kinto internal cache backend (if configured):

if not hasattr(request.registry, 'cache'):
    return fetch_github(token)

cache = request.registry.cache
cache_key = "token_github:" + token
user_id = cache.get(cache_key)
if not user_id:
    user_id = fetch_github(token)
    cache.set(cache_key, user_id, ttl=3600*24)  # cache during 24H

Next steps

Now that this policy works as expected, you can bring it to the next level!

For example:

  • Contribute it as built-in policy in Kinto! (We need you!)
  • Contribute another policy based on another method (e.g. Twitter, JSON Web token etc.)
  • Build a Webpage and try obtaining a token in a Web flow (see Github docs)
  • Allow passing the Github token in the querystring in addition to Authorization header (for convience)

Don’t hesitate to contact us!