Switch from database-persisted auth codes to stateless JSON Web Tokens :)
This commit is contained in:
parent
41d490ea80
commit
1c09be1b1c
6 changed files with 72 additions and 56 deletions
1
Pipfile
1
Pipfile
|
@ -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
21
Pipfile.lock
generated
|
@ -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"
|
||||||
|
|
18
lemonauth/migrations/0002_delete_indieauthcode.py
Normal file
18
lemonauth/migrations/0002_delete_indieauthcode.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
|
@ -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
27
lemonauth/tokens.py
Normal 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)
|
|
@ -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))
|
|
||||||
|
|
Loading…
Reference in a new issue