-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement JWT authentication based on RSA keys. Re-enable keybar.clie…
…nt tests.
- Loading branch information
Showing
8 changed files
with
197 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import jwt | ||
|
||
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 | ||
) | ||
|
||
from keybar.models.device import Device | ||
from keybar.utils.jwt import decode_token | ||
|
||
|
||
class JSONWebTokenAuthentication(BaseAuthentication): | ||
"""Token based authentication using the JSON Web Token standard. | ||
Clients should authenticate by passing the token key in the "Authorization" | ||
HTTP header, prepended with the string "JWT". | ||
.. code-block:: | ||
Authorization: JWT eyJhbGciOiAiSFMyNTYiLCAidHlwIj... | ||
""" | ||
www_authenticate_realm = 'api' | ||
auth_header_prefix = 'JWT' | ||
|
||
def get_jwt_value(self, request): | ||
auth = get_authorization_header(request).split() | ||
auth_header_prefix = self.auth_header_prefix.lower() | ||
|
||
if not auth or smart_text(auth[0].lower()) != auth_header_prefix: | ||
return None | ||
|
||
if len(auth) == 1: | ||
msg = _('Invalid Authorization header. No credentials provided.') | ||
raise exceptions.AuthenticationFailed(msg) | ||
elif len(auth) > 2: | ||
msg = _('Invalid Authorization header. Credentials string ' | ||
'should not contain spaces.') | ||
raise exceptions.AuthenticationFailed(msg) | ||
|
||
return auth[1] | ||
|
||
def authenticate(self, request): | ||
"""Authenticate `request`. | ||
Returns a two-tuple of `User` and token if a valid signature has been | ||
supplied using JWT-based authentication. Otherwise returns `None`. | ||
""" | ||
jwt_value = self.get_jwt_value(request) | ||
|
||
if jwt_value is None: | ||
return None | ||
|
||
unverified_data = jwt.decode(jwt_value, verify=False) | ||
if 'iss' not in unverified_data: | ||
msg = 'JWT iss (issuer) claim is missing' | ||
raise exceptions.AuthenticationFailed(detail=msg) | ||
|
||
device = Device.objects.get(pk=unverified_data['iss']) | ||
|
||
try: | ||
payload = decode_token(jwt_value, device) | ||
except jwt.ExpiredSignature: | ||
msg = _('Signature has expired.') | ||
raise exceptions.AuthenticationFailed(msg) | ||
except jwt.DecodeError: | ||
msg = _('Error decoding signature.') | ||
raise exceptions.AuthenticationFailed(msg) | ||
except jwt.InvalidTokenError: | ||
raise exceptions.AuthenticationFailed() | ||
|
||
if unverified_data['iss'] != payload['iss']: | ||
raise exceptions.AuthenticationFailed() | ||
|
||
user = self.authenticate_credentials(device) | ||
|
||
return (user, jwt_value) | ||
|
||
def authenticate_credentials(self, device): | ||
# TODO: | ||
# if not user.is_active: | ||
# msg = _('User account is disabled.') | ||
# raise exceptions.AuthenticationFailed(msg) | ||
|
||
return device.user | ||
|
||
def authenticate_header(self, request): | ||
""" | ||
Return a string to be used as the value of the `WWW-Authenticate` | ||
header in a `401 Unauthenticated` response, or `None` if the | ||
authentication scheme should return `403 Permission Denied` responses. | ||
""" | ||
return '{0} realm="{1}"'.format(self.auth_header_prefix, self.www_authenticate_realm) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from rest_framework import views | ||
from rest_framework.response import Response | ||
|
||
from keybar.utils import json | ||
|
||
|
||
class AuthenticatedDummyEndpoint(views.APIView): | ||
def get(self, request, *args, **kwargs): | ||
return Response(json.dumps({'dummy': 'ok'})) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,8 @@ | ||
from django.conf.urls import url | ||
|
||
from .endpoints.dummy import AuthenticatedDummyEndpoint | ||
|
||
|
||
urlpatterns = [ | ||
url(r'^dummy/$', AuthenticatedDummyEndpoint.as_view()) | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
from datetime import datetime, timedelta | ||
|
||
import jwt | ||
from django.conf import settings | ||
|
||
from keybar.utils.crypto import load_private_key | ||
|
||
|
||
JWT_OPTIONS = { | ||
'verify_signature': True, | ||
'verify_exp': True, | ||
'verify_nbf': True, | ||
'verify_iat': True, | ||
'verify_aud': True, | ||
'require_exp': True, | ||
'require_iat': True, | ||
'require_nbf': True | ||
} | ||
|
||
LEEWAY = 10 | ||
EXPIRATION_DELTA = timedelta(seconds=60) | ||
|
||
|
||
def decode_token(token, device): | ||
return jwt.decode( | ||
token, | ||
verify=True, | ||
key=device.loaded_public_key, | ||
options=JWT_OPTIONS, | ||
leeway=LEEWAY, | ||
audience=settings.KEYBAR_HOST, | ||
algorithms=['RS256'] | ||
) | ||
|
||
|
||
def encode_token(device_id, private_key): | ||
# TODO: make aud configurable | ||
issued_at = datetime.utcnow() | ||
payload = { | ||
'iss': device_id, | ||
'exp': issued_at + EXPIRATION_DELTA, | ||
'iat': issued_at, | ||
'nbf': issued_at, | ||
'aud': 'local.keybar.io:9999', | ||
} | ||
|
||
return jwt.encode( | ||
payload, | ||
key=load_private_key(private_key), | ||
algorithm='RS256' | ||
).decode('utf-8') |