Skip to content

Commit

Permalink
Add JWT token auth
Browse files Browse the repository at this point in the history
  • Loading branch information
fdobrovolny committed Sep 25, 2017
1 parent a0764f9 commit a7fe25d
Show file tree
Hide file tree
Showing 14 changed files with 479 additions and 16 deletions.
9 changes: 9 additions & 0 deletions docs/contributing/platform_api/app/auth.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
pulp.app.auth
=================

.. automodule:: pulpcore.app.auth

pulp.app.auth.jwt
-------------------------

.. automodule:: pulpcore.app.auth.jwt
1 change: 1 addition & 0 deletions docs/contributing/platform_api/app/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pulp.app
.. toctree::

apps
auth
fields
models
response
Expand Down
26 changes: 26 additions & 0 deletions docs/installation/configuration-files.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,29 @@ logging
'':
handlers: ["myCustomHandler"]
level: DEBUG
JWT_AUTH
^^^^^^^^

The configuration section for `JSON Web Tokens <https://jwt.io/>`_ authentication.

JWT_VERIFY_EXPIRATION
You can turn off JWT token expiration time verification by setting
`JWT_VERIFY_EXPIRATION` to `False`. Without expiration verification, tokens will last forever
meaning a leaked token could be used by an attacker indefinitely (token can be
invalidated by changing user's secret, which will lead to invalidation of all user's tokens).

Default is `True`.

JWT_EXPIRATION_DELTA
This is number of seconds for which is token valid if `JWT_VERIFY_EXPIRATION` enabled.

Default is `1209600`. (14 days)

.. warning::
Change of this value will affect only newly generated tokens.

JWT_ALLOW_SETTING_USER_SECRET
Allow setting user's secret via REST API. This is needed for offline token generation.

Default is `False`.
154 changes: 154 additions & 0 deletions docs/integration_guide/rest_api/authentication.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
Authentication
==============

All calls to REST API endpoints must be authenticated as a particular User.

.. tip::
The password for the "admin" user can be set using two methods.

``pulp-manager reset-admin-password``

The above command prompts the user to enter a new password for "admin" user.

``pulp-manager reset-admin-password --random``

The above command generates a random password for "admin" user and prints it to the screen.

.. tip::
If you are using django rest framework browsable API these browser addons may come handy:

* Chrome `ModHeader <https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj>`_
* Firefox `Modify Headers <https://addons.mozilla.org/cs/firefox/addon/modify-headers/>`_

Basic Authentication
--------------------

Any call to the REST API may use
`HTTP basic authentication <http://tools.ietf.org/html/rfc1945#section-11.1>`_ to provide
a username and password.

JWT Authentication
------------------

Alternatively you can use `JSON Web Tokens authentication <https://tools.ietf.org/html/rfc7519>`_.

Token Structure
^^^^^^^^^^^^^^^

The structure of the token consists of ``username`` and ``exp``. The tokens are signed by each
user's individual secret by HMAC SHA-256 ("HS256") algorithm.

Example:
::
{
"username": "admin",
"exp": 1501172472
}

Obtaining token from server
^^^^^^^^^^^^^^^^^^^^^^^^^^^

You can "login" to Pulp by supplying your credentials in POST request and obtain a JWT token
with expiration time set by ``JWT_AUTH.JWT_EXPIRATION_DELTA``


* **method:** ``post``
* **path:** ``api/v3/jwt/``
* response list:
* **code:** ``200`` - credentials were accepted
* **code:** ``400`` - credentials are wrong

**Sample POST request:**
::
{
"username": "admin",
"password": "admin_password"
}


**Sample 200 Response Body:**
::
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNTAyMzgzMDExfQ.3ZpcclxV6hN8ui2HUbwXLJsHl2lhesiCPeDVV2GIbJg"
}

Using a token
^^^^^^^^^^^^^

For using JWT tokens you have to set ``Authorization`` header as follows:
::
Authorization: JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNTAyMzgzMDExfQ.3ZpcclxV6hN8ui2HUbwXLJsHl2lhesiCPeDVV2GIbJg

User secret
^^^^^^^^^^^

To achieve ability to invalidate all user's tokens each user have their own secret key which is
used to sign their tokens. The change of the secret will lead to invalidation of all user's
tokens. The change is independent on password.

You can reset user secret to random by setting ``reset_jwt_secret`` on user API endpoint to True.

If you have enabled ``JWT_AUTH.JWT_ALLOW_SETTING_USER_SECRET`` you can set the user's secret
via user API endpoint.

The secret is stored in User model in field ``jwt_secret``.

Offline token generation
^^^^^^^^^^^^^^^^^^^^^^^^

If you have enabled ``JWT_AUTH.JWT_ALLOW_SETTING_USER_SECRET`` users can set their secrets and
therefore are able to generate tokens offline.

If you have pulpcore installed in your environment you can do the following:

.. code-block:: python
from datetime import timedelta
from pulpcore.app.auth.jwt_utils import generate_token_offline
username = "admin"
jwt_secret = "admin_token_secret"
exp_delta = timedelta(days=7) # This value is optional, default 14 days
token = generate_token_offline(username, jwt_secret, exp_delta)
If not you can implement the above function like this:

.. code-block:: python
import jwt # pip install pyjwt
from datetime import datetime, timedelta
def generate_token_offline(username, jwt_secret, exp_delta=timedelta(days=14)):
"""
Generate JWT token for pulp offline from username and secret.
This function can be used for JWT token generation on client without
the need of connection to pulp server. The only things you need to
know are `username` and `jwt_secret`.
Args:
username (str): username
jwt_secret (str): User's JWT token secret
exp_delta (datetime.timedelta, optional):
Token expiration time delta. This will be added to
`datetime.utcnow()` to set the expiration time.
If not set default 14 days is used.
Returns:
str: JWT token
"""
return jwt.encode(
{
'username': username,
'exp': datetime.utcnow() + exp_delta
},
jwt_secret,
'HS256',
).decode("utf-8")
.. warning::
When tokens are generated on client. The client can set **ANY** expiration time they want
no matter what is set in ``JWT_EXPIRATION_DELTA``.
12 changes: 3 additions & 9 deletions docs/integration_guide/rest_api/index.rst
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
REST API Reference
==================

All REST API endpoints are protected with basic authentication. The password for the "admin"
user can be set using two methods.
.. toctree::
:maxdepth: 3

``pulp-manager reset-admin-password``

The above command prompts the user to enter a new password for "admin" user.

``pulp-manager reset-admin-password --random``

The above command generates a random password for "admin" user and prints it to the screen.
authentication
Empty file.
63 changes: 63 additions & 0 deletions platform/pulpcore/app/auth/jwt_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import jwt
from django.contrib.auth import get_user_model
from django.utils.translation import ugettext as _
from rest_framework import exceptions
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework_jwt.settings import api_settings


class PulpJSONWebTokenAuthentication(JSONWebTokenAuthentication):
"""
Authenticate user by JWT token.
"""

def authenticate(self, request):
"""
Returns a two-tuple of `User` and token if a valid signature has been
supplied using JWT-based authentication. Otherwise returns `None`.
"""
User = get_user_model()
jwt_value = self.get_jwt_value(request)
if jwt_value is None:
return None

try:
payload = api_settings.JWT_DECODE_HANDLER(jwt_value)
except User.DoesNotExist:
msg = _('User not found.')
raise exceptions.AuthenticationFailed(msg)
except jwt.ExpiredSignature:
msg = _('Token has expired.')
raise exceptions.AuthenticationFailed(msg)
except jwt.DecodeError:
msg = _('Invalid token.')
raise exceptions.AuthenticationFailed(msg)
except jwt.InvalidTokenError:
raise exceptions.AuthenticationFailed()

user = self.authenticate_credentials(payload)

return (user, jwt_value)

def authenticate_credentials(self, payload):
"""
Returns an active user that matches the payload's user id and email.
"""
User = get_user_model()
username = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER(payload)

if not username:
msg = _('Invalid token.')
raise exceptions.AuthenticationFailed(msg)

try:
user = User.objects.get_by_natural_key(username)
except User.DoesNotExist:
msg = _('Invalid token. User not found.')
raise exceptions.AuthenticationFailed(msg)

if not user.is_active:
msg = _('Invalid token. User account is disabled.')
raise exceptions.AuthenticationFailed(msg)

return user
Loading

0 comments on commit a7fe25d

Please sign in to comment.