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
{
"http_api_version": "1.2",
"project_docs": "https://kinto.readthedocs.io/",
"project_name": "kinto",
"project_version": "1.11.0.dev0",
"settings": {
"attachment.base_url": "http://localhost:7777",
"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 [kinto.core.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 kinto.core 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']
return user_id
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
{
"http_api_version": "1.2",
"project_docs": "https://kinto.readthedocs.io/",
"project_name": "kinto",
"project_version": "1.11.0.dev0",
"settings": {
"attachment.base_url": "http://localhost:7777",
"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!