Session API

This commit is contained in:
Laura Klünder 2018-11-23 22:28:04 +01:00
parent 1491fc5de3
commit cdb14a1e2c
5 changed files with 188 additions and 0 deletions

96
src/c3nav/api/api.py Normal file
View file

@ -0,0 +1,96 @@
from django.contrib.auth import login, logout
from django.contrib.auth.forms import AuthenticationForm
from django.middleware import csrf
from django.utils.translation import ugettext_lazy as _
from rest_framework.authentication import SessionAuthentication
from rest_framework.decorators import action
from rest_framework.exceptions import ParseError, PermissionDenied
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet
from c3nav.api.models import Token
class SessionViewSet(ViewSet):
"""
Session for Login, Logout, etc
Don't forget to set X-Csrftoken for POST requests!
/login POST with fields token or username and password to log in
/get_token POST with fields username and password to get a login token
/logout - POST to log out
"""
def list(self, request, *args, **kwargs):
return Response({
'is_authenticated': request.user.is_authenticated,
'csrf_token': csrf.get_token(request),
})
@action(detail=False, methods=['post'])
def login(self, request, *args, **kwargs):
# django-rest-framework doesn't do this for logged out requests
SessionAuthentication().enforce_csrf(request)
if request.user.is_authenticated:
return ParseError(_('Log out first.'))
try:
data = request.json_body
except AttributeError:
data = request.POST
if 'token' in data:
try:
token = Token.get_by_token(data['token'])
except Token.DoesNotExist:
raise PermissionDenied(_('This token does not exist or is no longer valid.'))
user = token.user
elif 'username' in data:
form = AuthenticationForm(request, data=data)
if not form.is_valid():
raise ParseError(form.errors)
user = form.user_cache
else:
raise ParseError(_('You need to send a token or username and password.'))
login(request, user)
return Response({
'detail': _('Login successful.'),
'csrf_token': csrf.get_token(request),
})
@action(detail=False, methods=['post'])
def get_token(self, request, *args, **kwargs):
# django-rest-framework doesn't do this for logged out requests
SessionAuthentication().enforce_csrf(request)
try:
data = request.json_body
except AttributeError:
data = request.POST
form = AuthenticationForm(request, data=data)
if not form.is_valid():
raise ParseError(form.errors)
token = form.user_cache.login_tokens.create()
return Response({
'token': token.get_token(),
})
@action(detail=False, methods=['post'])
def logout(self, request, *args, **kwargs):
# django-rest-framework doesn't do this for logged out requests
SessionAuthentication().enforce_csrf(request)
if not request.user.is_authenticated:
return ParseError(_('Not logged in.'))
logout(request)
return Response({
'detail': _('Logout successful.'),
'csrf_token': csrf.get_token(request),
})

View file

@ -0,0 +1,31 @@
# Generated by Django 2.1.1 on 2018-11-23 21:19
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Token',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('secret', models.CharField(max_length=64, verbose_name='secret')),
('session_auth_hash', models.CharField(max_length=128, verbose_name='session auth hash')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='login_tokens', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'login tokens',
'verbose_name_plural': 'login tokens',
'default_related_name': 'login_tokens',
},
),
]

View file

56
src/c3nav/api/models.py Normal file
View file

@ -0,0 +1,56 @@
import string
from django.conf import settings
from django.db import models
from django.utils.crypto import constant_time_compare, get_random_string
from django.utils.translation import ugettext_lazy as _
class Token(models.Model):
"""
Token for log in via API
"""
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
secret = models.CharField(max_length=64, verbose_name=_('secret'))
session_auth_hash = models.CharField(_('session auth hash'), max_length=128)
class Meta:
verbose_name = _('login tokens')
verbose_name_plural = _('login tokens')
default_related_name = 'login_tokens'
def save(self, *args, **kwargs):
if not self.secret:
self.secret = get_random_string(64, string.ascii_letters + string.digits)
if not self.session_auth_hash:
# noinspection PyUnresolvedReferences
self.session_auth_hash = self.user.get_session_auth_hash()
super().save(*args, **kwargs)
def get_token(self):
return '%d:%s' % (self.pk, self.secret)
def verify(self):
# noinspection PyUnresolvedReferences
return constant_time_compare(
self.session_auth_hash,
self.user.get_session_auth_hash()
)
@classmethod
def get_by_token(cls, token: str):
try:
pk, secret = token.split(':', 1)
except ValueError:
raise cls.DoesNotExist
if not pk.isdigit() or not secret:
raise cls.DoesNotExist
obj = cls.objects.select_related('user').get(pk=pk, secret=secret)
if not obj.verify():
obj.delete()
raise cls.DoesNotExist
return obj

View file

@ -8,6 +8,7 @@ from rest_framework.generics import GenericAPIView
from rest_framework.response import Response
from rest_framework.routers import SimpleRouter
from c3nav.api.api import SessionViewSet
from c3nav.editor.api import ChangeSetViewSet, EditorViewSet
from c3nav.mapdata.api import (AccessRestrictionGroupViewSet, AccessRestrictionViewSet, AreaViewSet, BuildingViewSet,
ColumnViewSet, CrossDescriptionViewSet, DoorViewSet, HoleViewSet,
@ -49,6 +50,7 @@ router.register(r'routing', RoutingViewSet, base_name='routing')
router.register(r'editor', EditorViewSet, base_name='editor')
router.register(r'changesets', ChangeSetViewSet)
router.register(r'session', SessionViewSet, base_name='session')
class APIRoot(GenericAPIView):
@ -77,6 +79,9 @@ class APIRoot(GenericAPIView):
continue
if name == 'editor-detail':
name = 'editor-api'
elif base == 'session':
if name == 'session-list':
name = 'session-info'
if '-' in name:
urls.setdefault(base, OrderedDict())[name.split('-', 1)[1]] = url
else: