Skip to content

Commit

Permalink
Merge pull request #31 from anx-abruckner/issue28_tokenName
Browse files Browse the repository at this point in the history
issue28: add optional token_name
  • Loading branch information
nezhar authored Oct 21, 2022
2 parents 48084f9 + 3736dcc commit b3b7ccf
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 27 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## [Unreleased]

- Added optional "name" field for MultiToken model
- Updated README test instructions

## [2.0.0]

Expand Down
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ urlpatterns = [

The following endpoints are provided:

* `login` - takes username and password; on success an auth token is returned
* `login` - takes username, password and an optional token_name; on success an auth token is returned
* `logout`

## Signals
Expand All @@ -71,10 +71,20 @@ The following endpoints are provided:
See folder [tests/](tests/). Basically, all endpoints are covered with multiple
unit tests.

Use this code snippet to run tests:
Follow below instructions to run the tests.
You may exchange the installed Django and DRF versions according to your requirements.
:warning: Depending on your local environment settings you might need to explicitly call `python3` instead of `python`.
```bash
pip install tox
tox
# install dependencies
python -m pip install --upgrade pip
pip install -r requirements.txt

# setup environment
pip install -e .
python setup.py install

# run tests
cd tests && python manage.py test
```

## Cache Backend
Expand Down
2 changes: 1 addition & 1 deletion drf_multitokenauth/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@

@admin.register(MultiToken)
class MultiTokenAdmin(admin.ModelAdmin):
list_display = ('user', 'key', 'user_agent')
list_display = ('user', 'name', 'key', 'user_agent')
18 changes: 18 additions & 0 deletions drf_multitokenauth/migrations/0004_multitoken_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.16 on 2022-10-21 05:49

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('drf_multitokenauth', '0003_pk_migration'),
]

operations = [
migrations.AddField(
model_name='multitoken',
name='name',
field=models.CharField(default='', max_length=256, verbose_name='Token name'),
),
]
9 changes: 7 additions & 2 deletions drf_multitokenauth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ class MultiToken(models.Model):
verbose_name=_("HTTP User Agent"),
default=""
)
name = models.CharField(
max_length=256,
verbose_name=_("Token name"),
default=""
)

class Meta:
# Work around for a bug in Django:
Expand All @@ -72,6 +77,6 @@ def generate_key():
return binascii.hexlify(os.urandom(32)).decode()

def __str__(self):
return "{} (user {} with IP {} and user-agent {})".format(
self.key, self.user, self.last_known_ip, self.user_agent
return "{} ({} for user {} with IP {} and user-agent {})".format(
self.key, self.name, self.user, self.last_known_ip, self.user_agent
)
5 changes: 5 additions & 0 deletions drf_multitokenauth/serializers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from rest_framework import serializers
from rest_framework.authtoken.serializers import AuthTokenSerializer

__all__ = [
'EmailSerializer',
Expand All @@ -7,3 +8,7 @@

class EmailSerializer(serializers.Serializer):
email = serializers.EmailField()


class MultiAuthTokenSerializer(AuthTokenSerializer):
token_name = serializers.CharField(required=False, default="", allow_blank=True)
4 changes: 2 additions & 2 deletions drf_multitokenauth/urls.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
""" URL Configuration for core auth
"""
from django.conf.urls import include
from drf_multitokenauth.views import login_and_obtain_auth_token, logout_and_delete_auth_token
from django.urls import re_path

from drf_multitokenauth.views import login_and_obtain_auth_token, logout_and_delete_auth_token

app_name = 'drf_multitokenauth'

urlpatterns = [
Expand Down
24 changes: 11 additions & 13 deletions drf_multitokenauth/views.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
from datetime import timedelta
from django.conf import settings
from django.contrib.auth.models import User, update_last_login
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

from django.contrib.auth.models import update_last_login
from ipware import get_client_ip
from rest_framework import parsers, renderers, status
from rest_framework.authtoken.serializers import AuthTokenSerializer
from rest_framework.authentication import get_authorization_header
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.authentication import get_authorization_header

from drf_multitokenauth.models import MultiToken
from drf_multitokenauth.serializers import EmailSerializer
from drf_multitokenauth.serializers import MultiAuthTokenSerializer
from drf_multitokenauth.signals import pre_auth, post_auth

__all__ = [
Expand Down Expand Up @@ -50,20 +44,23 @@ class LoginAndObtainAuthToken(APIView):
permission_classes = ()
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
renderer_classes = (renderers.JSONRenderer,)
serializer_class = AuthTokenSerializer
serializer_class = MultiAuthTokenSerializer

def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)

user = serializer.validated_data['user']
token_name = serializer.validated_data['token_name']

# fire pre_auth signal
pre_auth.send(
sender=self.__class__,
username=request.data['username'],
password=request.data['password']
password=request.data['password'],
token_name=token_name
)

user = serializer.validated_data['user']

superuser_login_enabled = getattr(settings, 'AUTH_ENABLE_SUPERUSER_LOGIN', True)
if not superuser_login_enabled and user.is_superuser:
return Response({'error': 'superusers can\'t log in'}, status=status.HTTP_403_FORBIDDEN)
Expand All @@ -75,6 +72,7 @@ def post(self, request, *args, **kwargs):
user=user,
user_agent=request.META.get('HTTP_USER_AGENT', ''),
last_known_ip=get_client_ip(request)[0],
name=token_name,
)

# fire post_auth signal
Expand Down
97 changes: 92 additions & 5 deletions tests/test.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import json
from unittest.mock import patch

from django.contrib.auth.models import User
from django.db.models import Q
from django.test import override_settings
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase

from drf_multitokenauth.models import MultiToken
from unittest.mock import patch
from django.urls import reverse


class HelperMixin:
Expand All @@ -26,12 +28,16 @@ def reset_client_credentials(self):
""" reset all client credentials """
self.client.credentials()

def rest_do_login(self, username, password, HTTP_USER_AGENT='', REMOTE_ADDR='127.0.0.1'):
def rest_do_login(self, username, password, token_name=None, HTTP_USER_AGENT='', REMOTE_ADDR='127.0.0.1'):
""" REST API Wrapper for login """
data = {
'username': username,
'password': password
}

if token_name is not None:
data['token_name'] = token_name

return self.client.post(
self.login_url,
data,
Expand Down Expand Up @@ -64,8 +70,8 @@ def setUp(self):
self.user2 = User.objects.create_user("user2", "[email protected]", "secret2")
self.superuser = User.objects.create_superuser("superuser", "[email protected]", "secret3")

def login_and_obtain_token(self, username, password, HTTP_USER_AGENT='', REMOTE_ADDR='127.0.0.1'):
response = self.rest_do_login(username, password, HTTP_USER_AGENT, REMOTE_ADDR)
def login_and_obtain_token(self, username, password, token_name=None, HTTP_USER_AGENT='', REMOTE_ADDR='127.0.0.1'):
response = self.rest_do_login(username, password, token_name, HTTP_USER_AGENT, REMOTE_ADDR)
self.assertContains(response, "{\"token\":\"")

content = json.loads(response.content.decode())
Expand Down Expand Up @@ -143,6 +149,87 @@ def test_login_multiple_times(self):
2
)

def test_login_with_token_name(self):
""" tests login several times, using the same or different token_names for one or more users """
# there should be zero tokens
self.assertEqual(MultiToken.objects.all().count(), 0)

# login first time without token_name (defaults to '')
token1 = self.login_and_obtain_token('user1', 'secret1')
self.assertEqual(MultiToken.objects.all().count(), 1)
# verify the token is for user 1 and has an empty token_name
multi_token1 = MultiToken.objects.filter(key=token1).first()
self.assertEqual(
multi_token1.user.username,
'user1'
)
self.assertEqual(multi_token1.name, '')

# login second time with empty token_name ''
token2 = self.login_and_obtain_token('user1', 'secret1', '')
self.assertEqual(MultiToken.objects.all().count(), 2)
# verify the token is for user 1 and has an empty token_name
multi_token2 = MultiToken.objects.filter(key=token2).first()
self.assertEqual(
multi_token2.user.username,
'user1'
)
self.assertEqual(multi_token2.name, '')
# verify that token1 is not equal to token2
self.assertNotEqual(token1, token2)

# login third time, with token_name
token3 = self.login_and_obtain_token('user1', 'secret1', 'test_token_name1')
self.assertEqual(MultiToken.objects.all().count(), 3)
# verify the token is for user 1 and has an empty token_name
multi_token3 = MultiToken.objects.filter(key=token3).first()
self.assertEqual(
multi_token3.user.username,
'user1'
)
self.assertEqual(multi_token3.name, 'test_token_name1')
# verify that token2 is not equal to token3
self.assertNotEqual(token2, token3)

# login third time, with same token_name
token4 = self.login_and_obtain_token('user1', 'secret1', 'test_token_name1')
self.assertEqual(MultiToken.objects.all().count(), 4)
# verify the token is for user 1 and has an empty token_name
multi_token4 = MultiToken.objects.filter(key=token4).first()
self.assertEqual(
multi_token4.user.username,
'user1'
)
self.assertEqual(multi_token4.name, 'test_token_name1')
# verify that token3 is not equal to token4
self.assertNotEqual(token3, token4)

# login with another user and the same token_name
token5 = self.login_and_obtain_token('user2', 'secret2', 'test_token_name1')
self.assertEqual(MultiToken.objects.all().count(), 5)
# verify the token is for user 1 and has an empty token_name
multi_token5 = MultiToken.objects.filter(key=token5).first()
self.assertEqual(
multi_token5.user.username,
'user2'
)
self.assertEqual(multi_token5.name, 'test_token_name1')
# verify that token4 is not equal to token5
self.assertNotEqual(token4, token5)

# login with same user and different token_name
token6 = self.login_and_obtain_token('user2', 'secret2', 'test_token_name2')
self.assertEqual(MultiToken.objects.all().count(), 6)
# verify the token is for user 1 and has an empty token_name
multi_token6 = MultiToken.objects.filter(key=token6).first()
self.assertEqual(
multi_token6.user.username,
'user2'
)
self.assertEqual(multi_token6.name, 'test_token_name2')
# verify that token5 is not equal to token6
self.assertNotEqual(token5, token6)

def test_login_with_invalid_credentials(self):
""" tests login with invalid credentials """
# log in one time, just to be sure that the login works
Expand Down

0 comments on commit b3b7ccf

Please sign in to comment.