diff --git a/Pipfile b/Pipfile index 87c5bf6..864801c 100644 --- a/Pipfile +++ b/Pipfile @@ -38,6 +38,7 @@ django-shorturls = "*" accept-types = "*" django-analytical = "*" django-model-utils = "*" +pyjwt = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 95545cd..8c5fda9 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0c229a0ec55a08b58868faee7ce35a6adc3621d8e8619a67ec8939afcccc7dfe" + "sha256": "2afb7a864b8a0b60e8c0935dab11b91989cb33749272f4a5271dde8b53cafa43" }, "host-environment-markers": { "implementation_name": "cpython", @@ -66,10 +66,10 @@ }, "django": { "hashes": [ - "sha256:7ab6a9c798a5f9f359ee6da3677211f883fb02ef32cebe9b29751eb7a871febf", - "sha256:c3b42ca1efa1c0a129a9e863134cc3fe705c651dea3a04a7998019e522af0c60" + "sha256:75ce405d60f092f6adf904058d023eeea0e6d380f8d9c36134bac73da736023d", + "sha256:8918e392530d8fc6965a56af6504229e7924c27265893f3949aa0529cd1d4b99" ], - "version": "==1.11.6" + "version": "==1.11.7" }, "django-activeurl": { "hashes": [ @@ -193,10 +193,10 @@ }, "html5lib": { "hashes": [ - "sha256:08a3efc117a4fc8c82c3c6d10d6f58ae266428d57ed50258a1466d2cd88de745", - "sha256:0d5fd54d5b2b79b876007a70c033a4023577768d18022c15681c00561432a0f9" + "sha256:b8934484cf22f1db684c0fae27569a0db404d0208d20163fbf51cc537245d008", + "sha256:ee747c0ffd3028d2722061936b5c65ee4fe13c8e4613519b4447123fc4546298" ], - "version": "==1.0b10" + "version": "==0.999999999" }, "idna": { "hashes": [ @@ -351,6 +351,13 @@ ], "version": "==2.7.3.2" }, + "pyjwt": { + "hashes": [ + "sha256:a4e5f1441e3ca7b382fd0c0b416777ced1f97c64ef0c33bfa39daf38505cfd2f", + "sha256:500be75b17a63f70072416843dc80c8821109030be824f4d14758f114978bae7" + ], + "version": "==1.5.3" + }, "python-memcached": { "hashes": [ "sha256:2775829cb54b9e4c5b3bbd8028680f0c0ab695db154b9c46f0f074ff97540eb6" diff --git a/lemonauth/migrations/0002_delete_indieauthcode.py b/lemonauth/migrations/0002_delete_indieauthcode.py new file mode 100644 index 0000000..69ad679 --- /dev/null +++ b/lemonauth/migrations/0002_delete_indieauthcode.py @@ -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', + ), + ] diff --git a/lemonauth/models.py b/lemonauth/models.py deleted file mode 100644 index 2360431..0000000 --- a/lemonauth/models.py +++ /dev/null @@ -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) diff --git a/lemonauth/tokens.py b/lemonauth/tokens.py new file mode 100644 index 0000000..fd04906 --- /dev/null +++ b/lemonauth/tokens.py @@ -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) diff --git a/lemonauth/views/indie.py b/lemonauth/views/indie.py index bdeacd0..72f3489 100644 --- a/lemonauth/views/indie.py +++ b/lemonauth/views/indie.py @@ -11,7 +11,7 @@ from django.views.decorators.http import require_POST from lemoncurry import breadcrumbs, utils from urllib.parse import urlencode, urljoin, urlunparse, urlparse -from ..models import IndieAuthCode +from .. import tokens breadcrumbs.add('lemonauth:indie', label='indieauth', parent='home:index') @@ -88,28 +88,23 @@ class IndieView(TemplateView): def post(self, request): post = request.POST.dict() try: - code = IndieAuthCode.objects.get(code=post.get('code')) - except IndieAuthCode.DoesNotExist: + code = tokens.verify_auth_code(post.get('code')) + except Exception: + # if anything at all goes wrong when decoding the auth code, bail + # out immediately. return utils.forbid('invalid auth code') - # We always delete the code immediately to ensure it's only single-use. - # 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': + if code['response_type'] != 'id': return utils.bad_req( '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') - if post.get('redirect_uri') != code.redirect_uri: + if post.get('redirect_uri') != code['redirect_uri']: return utils.forbid('redirect uri did not match') # 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/x-www-form-urlencoded': utils.form_encoded_response, }) @@ -118,9 +113,6 @@ class IndieView(TemplateView): @login_required @require_POST def approve(request): - code = IndieAuthCode.objects.create_from_qdict(request.POST) - code.save() - params = {'code': code.code, 'me': code.me} - if 'state' in request.POST: - params['state'] = request.POST['state'] - return redirect(code.redirect_uri + '?' + urlencode(params)) + post = request.POST + params = tokens.gen_auth_code(post) + return redirect(post['redirect_uri'] + '?' + urlencode(params))