Switch from database-persisted auth codes to stateless JSON Web Tokens :)

This commit is contained in:
Danielle McLean 2017-11-02 16:36:16 +11:00
parent 41d490ea80
commit 1c09be1b1c
Signed by: 00dani
GPG key ID: 5A5D2D1AFF12EEC5
6 changed files with 72 additions and 56 deletions

View file

@ -38,6 +38,7 @@ django-shorturls = "*"
accept-types = "*" accept-types = "*"
django-analytical = "*" django-analytical = "*"
django-model-utils = "*" django-model-utils = "*"
pyjwt = "*"
[dev-packages] [dev-packages]

21
Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "0c229a0ec55a08b58868faee7ce35a6adc3621d8e8619a67ec8939afcccc7dfe" "sha256": "2afb7a864b8a0b60e8c0935dab11b91989cb33749272f4a5271dde8b53cafa43"
}, },
"host-environment-markers": { "host-environment-markers": {
"implementation_name": "cpython", "implementation_name": "cpython",
@ -66,10 +66,10 @@
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:7ab6a9c798a5f9f359ee6da3677211f883fb02ef32cebe9b29751eb7a871febf", "sha256:75ce405d60f092f6adf904058d023eeea0e6d380f8d9c36134bac73da736023d",
"sha256:c3b42ca1efa1c0a129a9e863134cc3fe705c651dea3a04a7998019e522af0c60" "sha256:8918e392530d8fc6965a56af6504229e7924c27265893f3949aa0529cd1d4b99"
], ],
"version": "==1.11.6" "version": "==1.11.7"
}, },
"django-activeurl": { "django-activeurl": {
"hashes": [ "hashes": [
@ -193,10 +193,10 @@
}, },
"html5lib": { "html5lib": {
"hashes": [ "hashes": [
"sha256:08a3efc117a4fc8c82c3c6d10d6f58ae266428d57ed50258a1466d2cd88de745", "sha256:b8934484cf22f1db684c0fae27569a0db404d0208d20163fbf51cc537245d008",
"sha256:0d5fd54d5b2b79b876007a70c033a4023577768d18022c15681c00561432a0f9" "sha256:ee747c0ffd3028d2722061936b5c65ee4fe13c8e4613519b4447123fc4546298"
], ],
"version": "==1.0b10" "version": "==0.999999999"
}, },
"idna": { "idna": {
"hashes": [ "hashes": [
@ -351,6 +351,13 @@
], ],
"version": "==2.7.3.2" "version": "==2.7.3.2"
}, },
"pyjwt": {
"hashes": [
"sha256:a4e5f1441e3ca7b382fd0c0b416777ced1f97c64ef0c33bfa39daf38505cfd2f",
"sha256:500be75b17a63f70072416843dc80c8821109030be824f4d14758f114978bae7"
],
"version": "==1.5.3"
},
"python-memcached": { "python-memcached": {
"hashes": [ "hashes": [
"sha256:2775829cb54b9e4c5b3bbd8028680f0c0ab695db154b9c46f0f074ff97540eb6" "sha256:2775829cb54b9e4c5b3bbd8028680f0c0ab695db154b9c46f0f074ff97540eb6"

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.7 on 2017-11-02 05:35
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('lemonauth', '0001_initial'),
]
operations = [
migrations.DeleteModel(
name='IndieAuthCode',
),
]

View file

@ -1,29 +0,0 @@
from django.db import models
from secrets import token_hex
class IndieAuthCodeManager(models.Manager):
def create_from_qdict(self, d):
code = self.create(
me=d['me'],
client_id=d['client_id'],
redirect_uri=d['redirect_uri'],
response_type=d.get('response_type', 'id'),
scope=" ".join(d.getlist('scope')),
)
code.code = token_hex(32)
return code
class IndieAuthCode(models.Model):
objects = IndieAuthCodeManager()
code = models.CharField(max_length=64, unique=True)
me = models.CharField(max_length=255)
client_id = models.CharField(max_length=255)
redirect_uri = models.CharField(max_length=255)
response_type = models.CharField(
max_length=4,
choices=(('id', 'id'), ('code', 'code')),
default='id',
)
scope = models.CharField(max_length=200, blank=True)

27
lemonauth/tokens.py Normal file
View file

@ -0,0 +1,27 @@
import jwt
from datetime import datetime, timedelta
from django.conf import settings
def gen_auth_code(post):
params = {'me': post['me']}
if 'state' in post:
params['state'] = post['state']
code = {
'me': post['me'],
'client_id': post['client_id'],
'redirect_uri': post['redirect_uri'],
'response_type': post.get('response_type', 'id'),
'exp': datetime.utcnow() + timedelta(minutes=10),
}
if 'scope' in post:
code['scope'] = ' '.join(post.getlist('scope'))
params['code'] = jwt.encode(code, settings.SECRET_KEY)
return params
def verify_auth_code(c):
return jwt.decode(c, settings.SECRET_KEY)

View file

@ -11,7 +11,7 @@ from django.views.decorators.http import require_POST
from lemoncurry import breadcrumbs, utils from lemoncurry import breadcrumbs, utils
from urllib.parse import urlencode, urljoin, urlunparse, urlparse from urllib.parse import urlencode, urljoin, urlunparse, urlparse
from ..models import IndieAuthCode from .. import tokens
breadcrumbs.add('lemonauth:indie', label='indieauth', parent='home:index') breadcrumbs.add('lemonauth:indie', label='indieauth', parent='home:index')
@ -88,28 +88,23 @@ class IndieView(TemplateView):
def post(self, request): def post(self, request):
post = request.POST.dict() post = request.POST.dict()
try: try:
code = IndieAuthCode.objects.get(code=post.get('code')) code = tokens.verify_auth_code(post.get('code'))
except IndieAuthCode.DoesNotExist: except Exception:
# if anything at all goes wrong when decoding the auth code, bail
# out immediately.
return utils.forbid('invalid auth code') return utils.forbid('invalid auth code')
# We always delete the code immediately to ensure it's only single-use. if code['response_type'] != 'id':
# If you pass the right code but the wrong other info, bad luck, you
# need a new code.
code.delete()
# After deleting the code from the DB, we verify the other parameters
# of the request.
if code.response_type != 'id':
return utils.bad_req( return utils.bad_req(
'this endpoint only supports response_type=id' 'this endpoint only supports response_type=id'
) )
if post.get('client_id') != code.client_id: if post.get('client_id') != code['client_id']:
return utils.forbid('client id did not match') return utils.forbid('client id did not match')
if post.get('redirect_uri') != code.redirect_uri: if post.get('redirect_uri') != code['redirect_uri']:
return utils.forbid('redirect uri did not match') return utils.forbid('redirect uri did not match')
# If we got here, it's valid! Yay! # If we got here, it's valid! Yay!
return utils.choose_type(request, {'me': code.me}, { return utils.choose_type(request, {'me': code['me']}, {
'application/json': JsonResponse, 'application/json': JsonResponse,
'application/x-www-form-urlencoded': utils.form_encoded_response, 'application/x-www-form-urlencoded': utils.form_encoded_response,
}) })
@ -118,9 +113,6 @@ class IndieView(TemplateView):
@login_required @login_required
@require_POST @require_POST
def approve(request): def approve(request):
code = IndieAuthCode.objects.create_from_qdict(request.POST) post = request.POST
code.save() params = tokens.gen_auth_code(post)
params = {'code': code.code, 'me': code.me} return redirect(post['redirect_uri'] + '?' + urlencode(params))
if 'state' in request.POST:
params['state'] = request.POST['state']
return redirect(code.redirect_uri + '?' + urlencode(params))