Skip to content

Commit

Permalink
Initial commit with some code
Browse files Browse the repository at this point in the history
  • Loading branch information
anx-ckreuzberger committed Oct 6, 2016
1 parent 65115ad commit e94efaa
Show file tree
Hide file tree
Showing 14 changed files with 442 additions and 17 deletions.
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
*.pyc
__pycache__
*.pyo
dist/**
*.egg-info/**
.idea/
venv/
build/
.tox/
Empty file added .travis.yml
Empty file.
40 changes: 23 additions & 17 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
MIT License
Copyright (c) 2016, Christian Kreuzberger.
All rights reserved.

Copyright (c) 2016 Christian Kreuzberger
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
3. Neither the name of django_userforeignkey nor the names of its contributors may be used
to endorse or promote products derived from this software without
specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include LICENSE
include README.md
recursive-include docs *
Empty file added README.md
Empty file.
Empty file.
8 changes: 8 additions & 0 deletions django_rest_multitokenauth/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
""" contains basic admin views for MultiToken """
from django.contrib import admin
from eric.coreauth.models import MultiToken


@admin.register(MultiToken)
class MultiTokenAdmin(admin.ModelAdmin):
list_display = ('user', 'key', 'user_agent')
25 changes: 25 additions & 0 deletions django_rest_multitokenauth/coreauthentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""
Provides our custom MultiToken Authentication (based on normal Token Authentication)
"""
from __future__ import unicode_literals

from rest_framework.authentication import TokenAuthentication

from eric.coreauth.models import MultiToken


class MultiTokenAuthentication(TokenAuthentication):
"""
Simple token based authentication.
Clients should authenticate by passing the token key in the "Authorization"
HTTP header, prepended with the string "Token ". For example:
Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a
"""

# override get_model (use a custom model)
def get_model(self):
if self.model is not None:
return self.model
return MultiToken
69 changes: 69 additions & 0 deletions django_rest_multitokenauth/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import binascii
import os

from rest_framework.authtoken.models import Token
from django.conf import settings
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _

# Prior to Django 1.5, the AUTH_USER_MODEL setting does not exist.
# Note that we don't perform this code in the compat module due to
# bug report #1297
# See: https://github.com/tomchristie/django-rest-framework/issues/1297
AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')


@python_2_unicode_compatible
class MultiToken(models.Model):
"""
The multi token model with user agent and IP address.
"""
key = models.CharField(
_("Key"),
max_length=64,
primary_key=True
)
user = models.ForeignKey(
AUTH_USER_MODEL,
related_name='auth_tokens',
on_delete=models.CASCADE,
verbose_name=_("User")
)
created = models.DateTimeField(
_("Created"),
auto_now_add=True
)
last_known_ip = models.GenericIPAddressField(
_("The IP address of this session"),
default="127.0.0.1"
)
user_agent = models.CharField(
max_length=256,
verbose_name=_("HTTP User Agent"),
default=""
)

class Meta:
# Work around for a bug in Django:
# https://code.djangoproject.com/ticket/19422
#
# Also see corresponding ticket:
# https://github.com/tomchristie/django-rest-framework/issues/705
abstract = 'eric.coreauth' not in settings.INSTALLED_APPS
verbose_name = _("Token")
verbose_name_plural = _("Tokens")

def save(self, *args, **kwargs):
if not self.key:
self.key = self.generate_key()
return super(MultiToken, self).save(*args, **kwargs)

@staticmethod
def generate_key():
""" generates a pseudo random code using os.urandom and binascii.hexlify """
return binascii.hexlify(os.urandom(32)).decode()

def __str__(self):
return self.key + " (user " + str(self.user) + " with IP " + self.last_known_IP + \
" and user agent " + self.user_agent + ")"
201 changes: 201 additions & 0 deletions django_rest_multitokenauth/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import json

from django.contrib.auth.models import User
from rest_framework.test import APITestCase
from rest_framework import status

from eric.coreauth.models import MultiToken

# read http://www.django-rest-framework.org/api-guide/testing/ for more info about testing with django rest framework


class LoginLogoutTest(APITestCase):
""" Extensive testing of login and logout using multitoken"""

def setUp(self):
""" set up a couple of users"""
self.user1 = User.objects.create_user(
username='johndoe', email='[email protected]', password='top_secret')

self.user2 = User.objects.create_user(
username='foobar', email='[email protected]', password='foobar')

self.user3 = User.objects.create_user(
username='alice', email='[email protected]', password='alice_secret')

self.user4 = User.objects.create_user(
username='bob', email='[email protected]', password='bob_secret')

def set_client_credentials(self, token):
self.client.credentials(HTTP_AUTHORIZATION='Token ' + token)

def reset_client_credentials(self):
self.client.credentials()

def logout_with_token(self, token, HTTP_USER_AGENT='API_TEST_CLIENT', REMOTE_ADDR='127.0.0.1'):
""" Checks if the token exists, then log sthe user out, and checks that the token no longer exists """
# token should be in database
self.assertEqual(len(MultiToken.objects.filter(key=token)), 1)
# set client credentials to the current token
self.set_client_credentials(token)

# call logout
response = self.client.post('/api/auth/logout', HTTP_USER_AGENT=HTTP_USER_AGENT, REMOTE_ADDR=REMOTE_ADDR)

# make sure the response is "logged_out"
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertContains(response, "{\"status\":\"logged out\"}")

# verify the token has been deleted
self.assertEqual(len(MultiToken.objects.filter(key=token)), 0)

def login_and_return_token(self, username, password, HTTP_USER_AGENT='API_TEST_CLIENT', REMOTE_ADDR='127.0.0.1'):
""" logs in and returns the token
checks with assert calls that the login was successful (token exists, user agent and remote addr are set)
"""

# reset auth token in header, if it exists
self.reset_client_credentials()

# check if the user exists
avail_users = User.objects.filter(username=username)
self.assertEqual(len(avail_users), 1)
cur_user = avail_users[0]

# login with self.user1, a given user agent and remote address
response = self.client.post('/api/auth/login',
{'username': username, 'password': password},
HTTP_USER_AGENT=HTTP_USER_AGENT, REMOTE_ADDR=REMOTE_ADDR)

# check if login was successful
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertContains(response, "{\"token\":\"")

content = json.loads(response.content.decode())
token = content['token']

# there should now be exactly one token
self.assertEqual(len(MultiToken.objects.filter(key=token, user=cur_user)), 1)
self.assertEqual(MultiToken.objects.filter(key=token, user=cur_user)[0].user_agent, HTTP_USER_AGENT)
self.assertEqual(MultiToken.objects.filter(key=token, user=cur_user)[0].last_known_ip, REMOTE_ADDR)

return token

def test_login_logout_user1_success(self):
""" Tries to login and checks if a token has been created,
logs out after that and checks if token has been deleted"""

# initial state: no tokens should exist
self.assertEqual(len(MultiToken.objects.all()), 0)

# login and get token
token = self.login_and_return_token(self.user1.username, 'top_secret')
# check that token is not empty
self.assertNotEqual(token, "")
self.assertEqual(len(MultiToken.objects.all()), 1)

# add credentials for the logout call
self.client.credentials(HTTP_AUTHORIZATION='Token ' + token)
response = self.client.post('/api/auth/logout')

self.assertEqual(response.status_code, status.HTTP_200_OK)

self.assertEqual(len(MultiToken.objects.all()), 0)

def test_login_logout_user1_wrong_password(self):
""" Tries to login with a wrong password or wrong username, and checks if a token has been created.
in the end, the login is successful with correct password."""

# initial state: no tokens should exist
self.assertEqual(len(MultiToken.objects.all()), 0)

# reset auth token in header, if it exists
self.reset_client_credentials()

# login with self.user1 but a wrong password
response = self.client.post('/api/auth/login',
{'username': "johndoe", 'password': "top_sicret"},
HTTP_USER_AGENT="TestClient", REMOTE_ADDR="127.0.0.1")

self.assertEqual(len(MultiToken.objects.all()), 0)

# check if login failed
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertTrue("Unable to log in" in response.content.decode(),
msg="Checking if 'Unable to log in' is in the response")

# login with a wrong user name, but the correct password
response = self.client.post('/api/auth/login',
{'username': "john_doe", 'password': "top_secret"},
HTTP_USER_AGENT="TestClient", REMOTE_ADDR="127.0.0.1")

self.assertEqual(len(MultiToken.objects.all()), 0)

# check if login failed
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertTrue("Unable to log in" in response.content.decode(),
msg="Checking if 'Unable to log in' is in the response")

# login with a wrong user name, and a wrong password
response = self.client.post('/api/auth/login',
{'username': "john_doe", 'password': "top_sicret"},
HTTP_USER_AGENT="TestClient", REMOTE_ADDR="127.0.0.1")

self.assertEqual(len(MultiToken.objects.all()), 0)

# check if login failed
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertTrue("Unable to log in" in response.content.decode(),
msg="Checking if 'Unable to log in' is in the response")

# finally, log in with the right username and password
response = self.client.post('/api/auth/login',
{'username': "johndoe", 'password': "top_secret"},
HTTP_USER_AGENT="TestClient", REMOTE_ADDR="127.0.0.1")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(MultiToken.objects.all()), 1)

def test_logout_with_wrong_token(self):
""" Tests that logout does not work with an invalid token """
self.assertEqual(len(MultiToken.objects.all()), 0)
token1 = self.login_and_return_token("johndoe", "top_secret")
self.assertEqual(len(MultiToken.objects.all()), 1)
# add credentials for the logout call
self.client.credentials(HTTP_AUTHORIZATION='Token ' + "wrong token")
response = self.client.post('/api/auth/logout')

self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(len(MultiToken.objects.all()), 1)

def test_login_logout_multiple_times_success(self):
""" Tries to login and checks if a token has been created,
logs out after that and checks if token has been deleted"""

# initial state: no tokens should exist
self.assertEqual(len(MultiToken.objects.all()), 0)

token1 = self.login_and_return_token(self.user1.username, 'top_secret')
self.assertEqual(len(MultiToken.objects.all()), 1)

token2 = self.login_and_return_token(self.user1.username, 'top_secret', REMOTE_ADDR="127.0.0.2")
self.assertEqual(len(MultiToken.objects.all()), 2)
# tokens should not be equal
self.assertNotEqual(token1, token2)

token3 = self.login_and_return_token(self.user1.username, 'top_secret', REMOTE_ADDR="127.0.0.3")
self.assertEqual(len(MultiToken.objects.all()), 3)
self.assertNotEqual(token1, token3)
self.assertNotEqual(token2, token3)

# now log out with token2
self.logout_with_token(token2)
# check the other two tokens still exist
self.assertEqual(len(MultiToken.objects.all()), 2)
self.assertEqual(len(MultiToken.objects.filter(key=token1)), 1)
self.assertEqual(len(MultiToken.objects.filter(key=token3)), 1)

self.logout_with_token(token1)
self.logout_with_token(token3)

self.assertEqual(len(MultiToken.objects.all()), 0)

9 changes: 9 additions & 0 deletions django_rest_multitokenauth/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
""" URL Configuration for core auth
"""
from django.conf.urls import url, include
from eric.coreauth.views import login_and_obtain_auth_token, logout_and_delete_auth_token

urlpatterns = [
url(r'^login', login_and_obtain_auth_token), # normal login with session
url(r'^logout', logout_and_delete_auth_token),
]
Loading

0 comments on commit e94efaa

Please sign in to comment.