Skip to content

Commit d89c8f7

Browse files
committed
Add authtoken app
1 parent 873b41b commit d89c8f7

File tree

11 files changed

+255
-0
lines changed

11 files changed

+255
-0
lines changed

Diff for: authtoken/__init__.py

Whitespace-only changes.

Diff for: authtoken/admin.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from django.contrib import admin
2+
3+
# Register your models here.

Diff for: authtoken/apps.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.apps import AppConfig
2+
3+
4+
class AuthConfig(AppConfig):
5+
name = 'authtoken'

Diff for: authtoken/auth.py

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from base64 import urlsafe_b64decode
2+
import binascii
3+
4+
from django.utils.translation import ugettext_lazy as _
5+
from rest_framework import HTTP_HEADER_ENCODING
6+
from rest_framework.authentication import BaseAuthentication, get_authorization_header
7+
from rest_framework.exceptions import AuthenticationFailed
8+
9+
from .models import AuthToken
10+
11+
12+
class AuthTokenAuthentication(BaseAuthentication):
13+
def authenticate(self, request):
14+
"""
15+
Authenticate the request and return a two-tuple of (user, token).
16+
"""
17+
auth = get_authorization_header(request).split()
18+
19+
if not auth or auth[0].lower() != b'token':
20+
return None
21+
22+
if len(auth) == 1:
23+
msg = _('Invalid auth token header. No credentials provided.')
24+
raise AuthenticationFailed(msg)
25+
elif len(auth) > 2:
26+
msg = _('Invalid auth token.')
27+
raise AuthenticationFailed(msg)
28+
29+
try:
30+
token = urlsafe_b64decode(auth[1])
31+
except ValueError:
32+
msg = _('Invalid auth token.')
33+
raise AuthenticationFailed(msg)
34+
35+
return self.authenticate_credentials(token, request)
36+
37+
def authenticate_credentials(self, token: bytes, request=None):
38+
"""
39+
Authenticate the token with optional request for context.
40+
"""
41+
user = AuthToken.get_user_for_token(token)
42+
43+
if user is None:
44+
raise AuthenticationFailed(_('Invalid auth token.'))
45+
46+
if not user.is_active:
47+
raise AuthenticationFailed(_('User inactive or deleted.'))
48+
49+
return user, token
50+
51+
def authenticate_header(self, request):
52+
return 'Token'

Diff for: authtoken/migrations/0001_initial.py

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Generated by Django 2.0.7 on 2018-07-19 11:46
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
import django.utils.timezone
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
initial = True
12+
13+
dependencies = [
14+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15+
]
16+
17+
operations = [
18+
migrations.CreateModel(
19+
name='AuthToken',
20+
fields=[
21+
('hashed_token', models.BinaryField(primary_key=True, serialize=False)),
22+
('created', models.DateTimeField(default=django.utils.timezone.now)),
23+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='auth_tokens', to=settings.AUTH_USER_MODEL)),
24+
],
25+
),
26+
]

Diff for: authtoken/migrations/__init__.py

Whitespace-only changes.

Diff for: authtoken/models.py

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from datetime import timedelta
2+
from hashlib import sha512
3+
from os import urandom
4+
from typing import Union
5+
6+
from django.conf import settings
7+
from django.contrib.auth import get_user_model
8+
from django.db import models
9+
from django.utils import timezone
10+
11+
12+
class AuthToken(models.Model):
13+
hashed_token = models.BinaryField(primary_key=True)
14+
15+
user = models.ForeignKey(
16+
settings.AUTH_USER_MODEL,
17+
related_name='auth_tokens',
18+
on_delete=models.CASCADE)
19+
20+
created = models.DateTimeField(default=timezone.now)
21+
22+
def __str__(self) -> str:
23+
return '{}: {}'.format(self.user, self.hashed_token)
24+
25+
@property
26+
def age(self) -> timedelta:
27+
return timezone.now() - self.created
28+
29+
def logout(self, token: Union[str, None] = None):
30+
"""
31+
Log this token out.
32+
"""
33+
self.delete()
34+
35+
@staticmethod
36+
def create_token_for_user(user: get_user_model()) -> bytes:
37+
"""
38+
Create a new random auth token for user.
39+
"""
40+
token = urandom(48)
41+
AuthToken.objects.create(
42+
hashed_token=AuthToken._hash_token(token),
43+
user=user)
44+
return token
45+
46+
@staticmethod
47+
def get_user_for_token(token: bytes) -> Union[get_user_model(), None]:
48+
auth_token = AuthToken.get_auth_token(token)
49+
if auth_token:
50+
return auth_token.user
51+
52+
@staticmethod
53+
def get_auth_token(token: bytes) -> Union[get_user_model(), None]:
54+
try:
55+
auth_token = AuthToken.objects.get(
56+
hashed_token=AuthToken._hash_token(token))
57+
58+
if auth_token.age > settings.AUTH_TOKEN_VALIDITY:
59+
# token expired.
60+
auth_token.delete()
61+
return None
62+
63+
return auth_token
64+
except AuthToken.DoesNotExist:
65+
return None
66+
67+
@staticmethod
68+
def _hash_token(token: bytes) -> bytes:
69+
"""
70+
Hash a token.
71+
"""
72+
return sha512(token).digest()

Diff for: authtoken/serializers.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from django.contrib.auth import get_user_model
2+
from rest_framework import serializers
3+
from rest_framework.exceptions import ValidationError
4+
5+
6+
class UserRegistrationSerializer(serializers.ModelSerializer):
7+
class Meta:
8+
model = get_user_model()
9+
fields = (
10+
'username',
11+
'password',
12+
)
13+
14+
def validate_password(self, value):
15+
if len(value) < 6:
16+
raise ValidationError('the password is too short.')
17+
return value
18+
19+
def create(self, validated_data):
20+
password = validated_data.pop('password')
21+
22+
instance = self.Meta.model(**validated_data)
23+
instance.set_password(password)
24+
instance.save()
25+
return instance

Diff for: authtoken/tests.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from django.test import TestCase
2+
3+
# Create your tests here.

Diff for: authtoken/urls.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from rest_framework.routers import SimpleRouter
2+
3+
from . import views
4+
5+
6+
app_name = 'authtoken'
7+
8+
router = SimpleRouter()
9+
router.register('login', views.LoginViewSet, base_name='login')
10+
router.register('register', views.RegisterViewSet, base_name='register')
11+
12+
urlpatterns = router.urls

Diff for: authtoken/views.py

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from base64 import urlsafe_b64encode
2+
3+
from django.conf import settings
4+
from django.contrib.auth import authenticate
5+
from django.utils.translation import ugettext_lazy as _
6+
from rest_framework import status
7+
from rest_framework.request import Request
8+
from rest_framework.response import Response
9+
from rest_framework.settings import APISettings
10+
from rest_framework.viewsets import GenericViewSet, ViewSet
11+
12+
from .models import AuthToken
13+
from .serializers import UserRegistrationSerializer
14+
15+
16+
authtoken_settings = APISettings(
17+
{
18+
'USER_SERIALIZER': getattr(settings, 'USER_SERIALIZER', None),
19+
},
20+
{
21+
'USER_SERIALIZER': None,
22+
},
23+
{'USER_SERIALIZER'}
24+
)
25+
26+
27+
class LoginViewSet(ViewSet):
28+
def create(self, request: Request) -> Response:
29+
user = authenticate(
30+
username=request.data.get('username'),
31+
password=request.data.get('password'))
32+
if not user:
33+
return Response(
34+
_('invalid credentials.'),
35+
status=status.HTTP_401_UNAUTHORIZED)
36+
37+
token = AuthToken.create_token_for_user(user)
38+
39+
data = {
40+
'token': urlsafe_b64encode(token),
41+
}
42+
43+
if authtoken_settings.USER_SERIALIZER:
44+
data['user'] = authtoken_settings.USER_SERIALIZER(
45+
instance=user, read_only=True).data
46+
47+
return Response(data)
48+
49+
50+
class RegisterViewSet(GenericViewSet):
51+
serializer_class = UserRegistrationSerializer
52+
53+
def create(self, request: Request) -> Response:
54+
serializer = self.get_serializer(data=request.data)
55+
serializer.is_valid(raise_exception=True)
56+
serializer.save()
57+
return Response('', status=status.HTTP_201_CREATED)

0 commit comments

Comments
 (0)