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