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

Commit 83cbfdf

Browse files
committed
Make refreshtoken a separate app.
1 parent 4fa96fc commit 83cbfdf

File tree

7 files changed

+173
-58
lines changed

7 files changed

+173
-58
lines changed

docs/index.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,59 @@ Refresh with tokens can be repeated (token1 -> token2 -> token3), but this chain
101101

102102
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.
103103

104+
## Long Running Refresh Token
105+
106+
This allows for a client to request refresh tokens. These refresh tokens do not expire.
107+
They can be revoked (deleted). When a JWT has expired, it's possible to send a request
108+
with the refresh token in the header, and get back a new JWT.
109+
110+
Declare the app
111+
```python
112+
INSTALLED_APPS = [
113+
...,
114+
'rest_framework_jwt.refreshtoken',
115+
]
116+
117+
```
118+
119+
Run migrations
120+
121+
```bash
122+
$ python manage.py migrate refreshtoken
123+
```
124+
125+
Configure your urls to add new endpoint
126+
127+
```python
128+
from rest_framework_jwt.refreshtoken.routers import urlpatterns as jwt_urlpatterns
129+
130+
urlpatterns = [
131+
url(...),
132+
] + jwt_urlpatterns
133+
134+
```
135+
136+
You can include this refresh token in your JWT_RESPONSE_PAYLOAD_HANDLER
137+
138+
```python
139+
140+
def jwt_response_payload_handler(token, user=None, request=None):
141+
return {
142+
'token': token,
143+
'user': UserSerializer(user).data,
144+
'refresh_token': user.refresh_tokens.first().key,
145+
}
146+
147+
```
148+
149+
Then your user can ask a new JWT token as long as the refresh_token exists.
150+
151+
```bash
152+
$ curl -X POST -H "Authorization: RefreshToken <REFRESH_TOKEN>" http://localhost:8000/delegate/
153+
'{"token": "your_jwt_token_..."}'
154+
155+
```
156+
104157
## Verify Token
105158

106159
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.

rest_framework_jwt/authentication.py

Lines changed: 3 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22
from django.utils.encoding import smart_text
33
from django.utils.translation import ugettext as _
44
from rest_framework import exceptions
5-
from rest_framework.authentication import (
6-
BaseAuthentication, get_authorization_header, TokenAuthentication
7-
)
5+
from rest_framework.authentication import (BaseAuthentication,
6+
get_authorization_header)
87

98
from rest_framework_jwt import utils
109
from rest_framework_jwt.settings import api_settings
11-
from rest_framework_jwt.refreshtoken.models import RefreshToken
10+
1211

1312
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
1413
jwt_get_user_id_from_payload = api_settings.JWT_PAYLOAD_GET_USER_ID_HANDLER
@@ -98,42 +97,3 @@ def authenticate_header(self, request):
9897
authentication scheme should return `403 Permission Denied` responses.
9998
"""
10099
return 'JWT realm="{0}"'.format(self.www_authenticate_realm)
101-
102-
103-
class RefreshTokenAuthentication(TokenAuthentication):
104-
"""
105-
Subclassed from rest_framework.authentication.TokenAuthentication
106-
107-
Auth header:
108-
Authorization: RefreshToken 401f7ac837da42b97f613d789819ff93537bee6a
109-
"""
110-
model = RefreshToken
111-
112-
def authenticate(self, request):
113-
auth = get_authorization_header(request).split()
114-
115-
if not auth or auth[0].lower() != b'refreshtoken':
116-
return None
117-
118-
if len(auth) == 1:
119-
msg = _('Invalid token header. No credentials provided.')
120-
raise exceptions.AuthenticationFailed(msg)
121-
elif len(auth) > 2:
122-
msg = _('Invalid token header. Token string should not contain spaces.')
123-
raise exceptions.AuthenticationFailed(msg)
124-
125-
return self.authenticate_credentials(auth[1])
126-
127-
def authenticate_credentials(self, key):
128-
try:
129-
token = self.model.objects.select_related('user').get(key=key)
130-
except self.model.DoesNotExist:
131-
raise exceptions.AuthenticationFailed(_('Invalid token.'))
132-
133-
if not token.user.is_active:
134-
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
135-
136-
return (token.user, token)
137-
138-
def authenticate_header(self, request):
139-
return 'RefreshToken'
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from django.utils.translation import ugettext as _
2+
from rest_framework.authentication import (
3+
TokenAuthentication,
4+
get_authorization_header,
5+
)
6+
from rest_framework import exceptions
7+
from rest_framework_jwt.refreshtoken.models import RefreshToken
8+
9+
10+
class RefreshTokenAuthentication(TokenAuthentication):
11+
"""
12+
Subclassed from rest_framework.authentication.TokenAuthentication
13+
14+
Auth header:
15+
Authorization: RefreshToken 401f7ac837da42b97f613d789819ff93537bee6a
16+
"""
17+
model = RefreshToken
18+
19+
def authenticate(self, request):
20+
auth = get_authorization_header(request).split()
21+
22+
if not auth or auth[0].lower() != b'refreshtoken':
23+
return None
24+
25+
if len(auth) == 1:
26+
msg = _('Invalid token header. No credentials provided.')
27+
raise exceptions.AuthenticationFailed(msg)
28+
elif len(auth) > 2:
29+
msg = _('Invalid token header. Token string should not contain spaces.')
30+
raise exceptions.AuthenticationFailed(msg)
31+
32+
return self.authenticate_credentials(auth[1])
33+
34+
def authenticate_credentials(self, key):
35+
try:
36+
token = self.model.objects.select_related('user').get(key=key)
37+
except self.model.DoesNotExist:
38+
raise exceptions.AuthenticationFailed(_('Invalid token.'))
39+
40+
if not token.user.is_active:
41+
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
42+
43+
return (token.user, token)
44+
45+
def authenticate_header(self, request):
46+
return 'RefreshToken'
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import models, migrations
5+
from django.conf import settings
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='RefreshToken',
17+
fields=[
18+
('key', models.CharField(max_length=40, primary_key=True, serialize=False)),
19+
('app', models.CharField(unique=True, max_length=255)),
20+
('created', models.DateTimeField(auto_now_add=True)),
21+
('user', models.ForeignKey(related_name='refresh_tokens', to=settings.AUTH_USER_MODEL)),
22+
],
23+
),
24+
]

rest_framework_jwt/refreshtoken/migrations/__init__.py

Whitespace-only changes.
Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,43 @@
1-
from .models import RefreshToken
1+
from calendar import timegm
2+
from datetime import datetime
3+
24
from rest_framework import serializers
5+
from rest_framework_jwt.settings import api_settings
6+
from rest_framework_jwt.serializers import (
7+
jwt_encode_handler,
8+
jwt_payload_handler,
9+
)
10+
11+
from .models import RefreshToken
312

413

514
class RefreshTokenSerializer(serializers.ModelSerializer):
6-
"""
7-
Serializer for refresh tokens (Not RefreshJWTToken)
8-
"""
9-
10-
class Meta:
11-
model = RefreshToken
12-
fields = ('key', 'user', 'created', 'app')
13-
read_only_fields = ('key', 'user', 'created')
14-
15-
def validate(self, attrs):
16-
attrs['user'] = self.context['request'].user
17-
return attrs
15+
"""
16+
Serializer for refresh tokens (Not RefreshJWTToken)
17+
"""
18+
19+
class Meta:
20+
model = RefreshToken
21+
fields = ('key', 'user', 'created', 'app')
22+
read_only_fields = ('key', 'user', 'created')
23+
24+
def validate(self, attrs):
25+
attrs['user'] = self.context['request'].user
26+
return attrs
27+
28+
29+
class DelegateJSONWebTokenSerializer(serializers.Serializer):
30+
def validate(self, attrs):
31+
user = self.context['request'].user
32+
payload = jwt_payload_handler(user)
33+
34+
# Include original issued at time for a brand new token,
35+
# to allow token refresh
36+
if api_settings.JWT_ALLOW_REFRESH:
37+
payload['orig_iat'] = timegm(
38+
datetime.utcnow().utctimetuple()
39+
)
40+
41+
attrs['token'] = jwt_encode_handler(payload)
42+
attrs['user'] = user
43+
return attrs

rest_framework_jwt/refreshtoken/views.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@
88

99
from rest_framework_jwt.settings import api_settings
1010
from rest_framework_jwt.views import JSONWebTokenAPIView
11-
from rest_framework_jwt.authentication import RefreshTokenAuthentication
11+
from rest_framework_jwt.refreshtoken.authentication import (
12+
RefreshTokenAuthentication,
13+
)
1214

1315
from .permissions import IsOwnerOrAdmin
1416
from .models import RefreshToken
15-
from .serializers import RefreshTokenSerializer
17+
from .serializers import (
18+
DelegateJSONWebTokenSerializer,
19+
RefreshTokenSerializer,
20+
)
1621

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

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

0 commit comments

Comments
 (0)