Skip to content

Commit e0d72a0

Browse files
committed
Initial version of Django REST Framework SSO
0 parents  commit e0d72a0

File tree

11 files changed

+506
-0
lines changed

11 files changed

+506
-0
lines changed

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# IDE stuff
2+
.idea/
3+
4+
# Byte-compiled / optimized / DLL files
5+
__pycache__/
6+
*.py[cod]
7+
*$py.class
8+
9+
# C extensions
10+
*.so

rest_framework_sso/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# coding: utf-8
2+
from __future__ import absolute_import, unicode_literals
3+
4+
__title__ = 'djangorestframework-sso'
5+
__version__ = '0.0.1'
6+
__author__ = 'Lenno Nagel'
7+
__license__ = 'MIT'
8+
__copyright__ = 'Copyright 2016 Namespace OÜ'
9+
10+
# Version synonym
11+
VERSION = __version__
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# coding: utf-8
2+
from __future__ import absolute_import, unicode_literals
3+
4+
import jwt
5+
from django.contrib.auth import get_user_model
6+
from django.utils.translation import ugettext as _
7+
from rest_framework import exceptions
8+
from rest_framework.authentication import (
9+
BaseAuthentication, get_authorization_header
10+
)
11+
12+
from rest_framework_sso.settings import api_settings
13+
14+
decode_jwt_token = api_settings.DECODE_JWT_TOKEN
15+
16+
17+
class JWTAuthentication(BaseAuthentication):
18+
"""
19+
JWT token based authentication.
20+
21+
Clients should authenticate by passing the token key in the "Authorization"
22+
HTTP header, prepended with the string "JWT ". For example:
23+
24+
Authorization: JWT eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJsb2NhbG...
25+
"""
26+
27+
def authenticate(self, request):
28+
auth = get_authorization_header(request).split()
29+
30+
if not auth or auth[0].lower() != api_settings.AUTHENTICATE_HEADER.lower().encode('utf-8'):
31+
return None
32+
33+
if len(auth) == 1:
34+
msg = _('Invalid token header. No credentials provided.')
35+
raise exceptions.AuthenticationFailed(msg)
36+
elif len(auth) > 2:
37+
msg = _('Invalid token header. Token string should not contain spaces.')
38+
raise exceptions.AuthenticationFailed(msg)
39+
40+
try:
41+
token = auth[1].decode()
42+
except UnicodeError:
43+
msg = _('Invalid token header. Token string should not contain invalid characters.')
44+
raise exceptions.AuthenticationFailed(msg)
45+
46+
try:
47+
payload = decode_jwt_token(token=token)
48+
except jwt.ExpiredSignature:
49+
msg = _('Signature has expired.')
50+
raise exceptions.AuthenticationFailed(msg)
51+
except jwt.DecodeError:
52+
msg = _('Error decoding signature.')
53+
raise exceptions.AuthenticationFailed(msg)
54+
except jwt.InvalidTokenError:
55+
raise exceptions.AuthenticationFailed()
56+
57+
return self.authenticate_credentials(payload=payload)
58+
59+
def authenticate_credentials(self, payload):
60+
from rest_framework_sso.models import SessionToken
61+
62+
user_model = get_user_model()
63+
64+
if not SessionToken._meta.abstract:
65+
try:
66+
SessionToken.objects.active().get(pk=payload.get('sid'), uid=payload.get('uid'))
67+
except SessionToken.DoesNotExist:
68+
raise exceptions.AuthenticationFailed(_('Invalid token.'))
69+
70+
try:
71+
user = user_model.objects.get(pk=payload.get('uid'))
72+
except user_model.DoesNotExist:
73+
raise exceptions.AuthenticationFailed(_('Invalid token.'))
74+
75+
if not user.is_active:
76+
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
77+
78+
return user, payload
79+
80+
def authenticate_header(self, request):
81+
return api_settings.AUTHENTICATE_HEADER
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.9.4 on 2016-06-20 08:04
3+
from __future__ import unicode_literals
4+
5+
from django.conf import settings
6+
from django.db import migrations, models
7+
import django.db.models.deletion
8+
import uuid
9+
10+
11+
class Migration(migrations.Migration):
12+
13+
initial = True
14+
15+
dependencies = [
16+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
17+
]
18+
19+
operations = [
20+
migrations.CreateModel(
21+
name='SessionToken',
22+
fields=[
23+
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
24+
('ip_address', models.GenericIPAddressField(blank=True, db_index=True, null=True)),
25+
('user_agent', models.CharField(blank=True, max_length=1000)),
26+
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
27+
('revoked_at', models.DateTimeField(blank=True, db_index=True, null=True)),
28+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='user')),
29+
],
30+
options={
31+
'abstract': False,
32+
'verbose_name': 'Session token',
33+
'verbose_name_plural': 'Session tokens',
34+
},
35+
),
36+
]

rest_framework_sso/migrations/__init__.py

Whitespace-only changes.

rest_framework_sso/models.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# coding: utf-8
2+
from __future__ import absolute_import, unicode_literals
3+
4+
import uuid
5+
6+
from django.conf import settings
7+
from django.db import models
8+
from django.utils import six
9+
from django.utils.encoding import python_2_unicode_compatible
10+
from django.utils.translation import ugettext_lazy as _
11+
12+
# Prior to Django 1.5, the AUTH_USER_MODEL setting does not exist.
13+
# Note that we don't perform this code in the compat module due to
14+
# bug report #1297
15+
# See: https://github.com/tomchristie/django-rest-framework/issues/1297
16+
from rest_framework_sso.querysets import SessionTokenQuerySet
17+
18+
AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
19+
20+
21+
@python_2_unicode_compatible
22+
class SessionToken(models.Model):
23+
"""
24+
The default session token model.
25+
"""
26+
id = models.UUIDField(
27+
primary_key=True,
28+
default=uuid.uuid4,
29+
editable=False,
30+
db_index=True,
31+
)
32+
user = models.ForeignKey(
33+
to=AUTH_USER_MODEL,
34+
related_name='+',
35+
on_delete=models.CASCADE,
36+
verbose_name=_("user"),
37+
)
38+
ip_address = models.GenericIPAddressField(null=True, blank=True, db_index=True)
39+
user_agent = models.CharField(max_length=1000, blank=True)
40+
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
41+
revoked_at = models.DateTimeField(null=True, blank=True, db_index=True)
42+
43+
objects = SessionTokenQuerySet.as_manager()
44+
45+
class Meta:
46+
# Work around for a bug in Django:
47+
# https://code.djangoproject.com/ticket/19422
48+
#
49+
# Also see corresponding ticket:
50+
# https://github.com/tomchristie/django-rest-framework/issues/705
51+
abstract = 'rest_framework_sso' not in settings.INSTALLED_APPS
52+
verbose_name = _("Session token")
53+
verbose_name_plural = _("Session tokens")
54+
55+
def __str__(self):
56+
return six.text_type(self.id)
57+
58+
def update_attributes(self, request):
59+
self.ip_address = request.META.get('HTTP_X_FORWARDED_FOR') \
60+
if request.META.get('HTTP_X_FORWARDED_FOR') \
61+
else request.META.get('REMOTE_ADDR')
62+
self.user_agent = request.META.get('HTTP_USER_AGENT')[:1000]

rest_framework_sso/querysets.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# coding: utf-8
2+
from __future__ import absolute_import, unicode_literals
3+
4+
from django.db.models import QuerySet, Q
5+
from django.utils import timezone
6+
7+
8+
class SessionTokenQuerySet(QuerySet):
9+
def active(self):
10+
return self.filter(Q(revoked_at__isnull=True) | Q(revoked_at__gt=timezone.now()))
11+
12+
def first_or_create(self, defaults=None, **kwargs):
13+
"""
14+
Looks up an object with the given kwargs, creating one if necessary.
15+
Returns a tuple of (object, created), where created is a boolean
16+
specifying whether an object was created.
17+
"""
18+
lookup, params = self._extract_model_params(defaults, **kwargs)
19+
# The get() needs to be targeted at the write database in order
20+
# to avoid potential transaction consistency problems.
21+
self._for_write = True
22+
23+
obj = self.filter(**lookup).first()
24+
if obj:
25+
return obj, False
26+
else:
27+
return self._create_object_from_params(lookup, params)

rest_framework_sso/serializers.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# coding: utf-8
2+
from __future__ import absolute_import, unicode_literals
3+
4+
from django.contrib.auth import authenticate
5+
from django.utils.translation import ugettext_lazy as _
6+
from rest_framework import serializers
7+
8+
from rest_framework_sso.settings import api_settings
9+
10+
create_authorization_payload = api_settings.CREATE_AUTHORIZATION_PAYLOAD
11+
encode_jwt_token = api_settings.ENCODE_JWT_TOKEN
12+
13+
14+
class SessionTokenSerializer(serializers.Serializer):
15+
username = serializers.CharField(label=_("Username"))
16+
password = serializers.CharField(label=_("Password"), style={'input_type': 'password'})
17+
18+
def validate(self, attrs):
19+
username = attrs.get('username')
20+
password = attrs.get('password')
21+
22+
if username and password:
23+
user = authenticate(username=username, password=password)
24+
25+
if user:
26+
if not user.is_active:
27+
msg = _('User account is disabled.')
28+
raise serializers.ValidationError(msg)
29+
else:
30+
msg = _('Unable to log in with provided credentials.')
31+
raise serializers.ValidationError(msg)
32+
else:
33+
msg = _('Must include "username" and "password".')
34+
raise serializers.ValidationError(msg)
35+
36+
attrs['user'] = user
37+
return attrs

rest_framework_sso/settings.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# coding: utf-8
2+
from __future__ import absolute_import, unicode_literals
3+
4+
import datetime
5+
6+
from django.conf import settings
7+
from rest_framework.settings import APISettings
8+
9+
10+
USER_SETTINGS = getattr(settings, 'REST_FRAMEWORK_SSO', None)
11+
12+
DEFAULTS = {
13+
'CREATE_SESSION_PAYLOAD': 'rest_framework_sso.utils.create_session_payload',
14+
'CREATE_AUTHORIZATION_PAYLOAD': 'rest_framework_sso.utils.create_authorization_payload',
15+
'ENCODE_JWT_TOKEN': 'rest_framework_sso.utils.encode_jwt_token',
16+
'DECODE_JWT_TOKEN': 'rest_framework_sso.utils.decode_jwt_token',
17+
18+
'ENCODE_ALGORITHM': 'RS256',
19+
'DECODE_ALGORITHMS': None,
20+
'VERIFY_SIGNATURE': True,
21+
'VERIFY_EXPIRATION': True,
22+
'EXPIRATION_LEEWAY': 0,
23+
'SESSION_EXPIRATION': None,
24+
'AUTHORIZATION_EXPIRATION': datetime.timedelta(seconds=300),
25+
26+
'IDENTITY': None,
27+
'SESSION_AUDIENCE': None,
28+
'AUTHORIZATION_AUDIENCE': None,
29+
'ACCEPTED_ISSUERS': None,
30+
'PUBLIC_KEYS': {},
31+
'PRIVATE_KEYS': {},
32+
33+
'AUTHENTICATE_HEADER': 'JWT',
34+
}
35+
36+
# List of settings that may be in string import notation.
37+
IMPORT_STRINGS = (
38+
'CREATE_SESSION_PAYLOAD',
39+
'CREATE_AUTHORIZATION_PAYLOAD',
40+
'ENCODE_JWT_TOKEN',
41+
'DECODE_JWT_TOKEN',
42+
)
43+
44+
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)

0 commit comments

Comments
 (0)