Skip to content
This repository was archived by the owner on May 26, 2020. It is now read-only.

Long running refresh tokens #123

Closed
wants to merge 5 commits into from
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Make refreshtoken a separate app.
ticosax committed May 26, 2015

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
commit b0b75702dff4b14c78ff63e6d1c1f4791139ad4f
53 changes: 53 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -101,6 +101,59 @@ Refresh with tokens can be repeated (token1 -> token2 -> token3), but this chain

A typical use case might be a web app where you'd like to keep the user "logged in" the site without having to re-enter their password, or get kicked out by surprise before their token expired. Imagine they had a 1-hour token and are just at the last minute while they're still doing something. With mobile you could perhaps store the username/password to get a new token, but this is not a great idea in a browser. Each time the user loads the page, you can check if there is an existing non-expired token and if it's close to being expired, refresh it to extend their session. In other words, if a user is actively using your site, they can keep their "session" alive.

## Long Running Refresh Token

This allows for a client to request refresh tokens. These refresh tokens do not expire.
They can be revoked (deleted). When a JWT has expired, it's possible to send a request
with the refresh token in the header, and get back a new JWT.

Declare the app
```python
INSTALLED_APPS = [
...,
'rest_framework_jwt.refreshtoken',
]

```

Run migrations

```bash
$ python manage.py migrate refreshtoken
```

Configure your urls to add new endpoint

```python
from rest_framework_jwt.refreshtoken.routers import urlpatterns as jwt_urlpatterns

urlpatterns = [
url(...),
] + jwt_urlpatterns

```

You can include this refresh token in your JWT_RESPONSE_PAYLOAD_HANDLER

```python

def jwt_response_payload_handler(token, user=None, request=None):
return {
'token': token,
'user': UserSerializer(user).data,
'refresh_token': user.refresh_tokens.first().key,
}

```

Then your user can ask a new JWT token as long as the refresh_token exists.

```bash
$ curl -X POST -H "Authorization: RefreshToken <REFRESH_TOKEN>" http://localhost:8000/delegate/
'{"token": "your_jwt_token_..."}'

```

## Verify Token

In some microservice architectures, authentication is handled by a single service. Other services delegate the responsibility of confirming that a user is logged in to this authentication service. This usually means that a service will pass a JWT received from the user to the authentication service, and wait for a confirmation that the JWT is valid before returning protected resources to the user.
46 changes: 3 additions & 43 deletions rest_framework_jwt/authentication.py
Original file line number Diff line number Diff line change
@@ -2,13 +2,12 @@
from django.utils.encoding import smart_text
from django.utils.translation import ugettext as _
from rest_framework import exceptions
from rest_framework.authentication import (
BaseAuthentication, get_authorization_header, TokenAuthentication
)
from rest_framework.authentication import (BaseAuthentication,
get_authorization_header)

from rest_framework_jwt import utils
from rest_framework_jwt.settings import api_settings
from rest_framework_jwt.refreshtoken.models import RefreshToken


jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
jwt_get_user_id_from_payload = api_settings.JWT_PAYLOAD_GET_USER_ID_HANDLER
@@ -98,42 +97,3 @@ def authenticate_header(self, request):
authentication scheme should return `403 Permission Denied` responses.
"""
return 'JWT realm="{0}"'.format(self.www_authenticate_realm)


class RefreshTokenAuthentication(TokenAuthentication):
"""
Subclassed from rest_framework.authentication.TokenAuthentication

Auth header:
Authorization: RefreshToken 401f7ac837da42b97f613d789819ff93537bee6a
"""
model = RefreshToken

def authenticate(self, request):
auth = get_authorization_header(request).split()

if not auth or auth[0].lower() != b'refreshtoken':
return None

if len(auth) == 1:
msg = _('Invalid token header. No credentials provided.')
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = _('Invalid token header. Token string should not contain spaces.')
raise exceptions.AuthenticationFailed(msg)

return self.authenticate_credentials(auth[1])

def authenticate_credentials(self, key):
try:
token = self.model.objects.select_related('user').get(key=key)
except self.model.DoesNotExist:
raise exceptions.AuthenticationFailed(_('Invalid token.'))

if not token.user.is_active:
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))

return (token.user, token)

def authenticate_header(self, request):
return 'RefreshToken'
46 changes: 46 additions & 0 deletions rest_framework_jwt/refreshtoken/authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from django.utils.translation import ugettext as _
from rest_framework.authentication import (
TokenAuthentication,
get_authorization_header,
)
from rest_framework import exceptions
from rest_framework_jwt.refreshtoken.models import RefreshToken


class RefreshTokenAuthentication(TokenAuthentication):
"""
Subclassed from rest_framework.authentication.TokenAuthentication

Auth header:
Authorization: RefreshToken 401f7ac837da42b97f613d789819ff93537bee6a
"""
model = RefreshToken

def authenticate(self, request):
auth = get_authorization_header(request).split()

if not auth or auth[0].lower() != b'refreshtoken':
return None

if len(auth) == 1:
msg = _('Invalid token header. No credentials provided.')
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = _('Invalid token header. Token string should not contain spaces.')
raise exceptions.AuthenticationFailed(msg)

return self.authenticate_credentials(auth[1])

def authenticate_credentials(self, key):
try:
token = self.model.objects.select_related('user').get(key=key)
except self.model.DoesNotExist:
raise exceptions.AuthenticationFailed(_('Invalid token.'))

if not token.user.is_active:
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))

return (token.user, token)

def authenticate_header(self, request):
return 'RefreshToken'
24 changes: 24 additions & 0 deletions rest_framework_jwt/refreshtoken/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations
from django.conf import settings


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='RefreshToken',
fields=[
('key', models.CharField(max_length=40, primary_key=True, serialize=False)),
('app', models.CharField(unique=True, max_length=255)),
('created', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(related_name='refresh_tokens', to=settings.AUTH_USER_MODEL)),
],
),
]
Empty file.
52 changes: 39 additions & 13 deletions rest_framework_jwt/refreshtoken/serializers.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,43 @@
from .models import RefreshToken
from calendar import timegm
from datetime import datetime

from rest_framework import serializers
from rest_framework_jwt.settings import api_settings
from rest_framework_jwt.serializers import (
jwt_encode_handler,
jwt_payload_handler,
)

from .models import RefreshToken


class RefreshTokenSerializer(serializers.ModelSerializer):
"""
Serializer for refresh tokens (Not RefreshJWTToken)
"""

class Meta:
model = RefreshToken
fields = ('key', 'user', 'created', 'app')
read_only_fields = ('key', 'user', 'created')

def validate(self, attrs):
attrs['user'] = self.context['request'].user
return attrs
"""
Serializer for refresh tokens (Not RefreshJWTToken)
"""

class Meta:
model = RefreshToken
fields = ('key', 'user', 'created', 'app')
read_only_fields = ('key', 'user', 'created')

def validate(self, attrs):
attrs['user'] = self.context['request'].user
return attrs


class DelegateJSONWebTokenSerializer(serializers.Serializer):
def validate(self, attrs):
user = self.context['request'].user
payload = jwt_payload_handler(user)

# Include original issued at time for a brand new token,
# to allow token refresh
if api_settings.JWT_ALLOW_REFRESH:
payload['orig_iat'] = timegm(
datetime.utcnow().utctimetuple()
)

attrs['token'] = jwt_encode_handler(payload)
attrs['user'] = user
return attrs
10 changes: 8 additions & 2 deletions rest_framework_jwt/refreshtoken/views.py
Original file line number Diff line number Diff line change
@@ -8,11 +8,16 @@

from rest_framework_jwt.settings import api_settings
from rest_framework_jwt.views import JSONWebTokenAPIView
from rest_framework_jwt.authentication import RefreshTokenAuthentication
from rest_framework_jwt.refreshtoken.authentication import (
RefreshTokenAuthentication,
)

from .permissions import IsOwnerOrAdmin
from .models import RefreshToken
from .serializers import RefreshTokenSerializer
from .serializers import (
DelegateJSONWebTokenSerializer,
RefreshTokenSerializer,
)

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
@@ -24,6 +29,7 @@ class DelegateJSONWebToken(JSONWebTokenAPIView):
is valid.
"""
authentication_classes = (RefreshTokenAuthentication, )
serializer_class = DelegateJSONWebTokenSerializer

def post(self, request):
user = request.user