Skip to main content

JWT authentication policy for Pyramid

Project description

JWT authentication for Pyramid

This package implements an authentication policy for Pyramid that using JSON Web Tokens. This standard (RFC 7519) is often used to secure backens APIs. The excellent PyJWT library is used for the JWT encoding / decoding logic.

Enabling JWT support in a Pyramid application is very simple:

from pyramid.config import Configurator
from pyramid.authorization import ACLAuthorizationPolicy

def main():
    config = Configurator()
    # Pyramid requires an authorization policy to be active.
    config.set_authorization_policy(ACLAuthorizationPolicy())
    # Enable JWT authentication.
    config.include('pyramid_jwt')
    config.set_jwt_authentication_policy('secret')

This will set a JWT authentication policy using the Authorization HTTP header with a JWT scheme to retrieve tokens. Using another HTTP header is trivial:

config.set_jwt_authentication_policy('secret', http_header='X-My-Header')

If your application needs to decode tokens which contain an Audience claim you can extend this with:

config.set_jwt_authentication_policy('secret',
                                    auth_type='Bearer',
                                    callback=add_role_principals,
                                    audience="example.org")

To make creating valid tokens easier a new create_jwt_token method is added to the request. You can use this in your view to create tokens. A simple authentication view for a REST backend could look something like this:

@view_config('login', request_method='POST', renderer='json')
def login(request):
    login = request.POST['login']
    password = request.POST['password']
    user_id = authenticate(login, password)  # You will need to implement this.
    if user_id:
        return {
            'result': 'ok',
            'token': request.create_jwt_token(user_id)
        }
    else:
        return {
            'result': 'error'
        }

Since JWT is typically used via HTTP headers and does not use cookies the standard remember() and forget() functions from Pyramid are not useful. Trying to use them while JWT authentication is enabled will result in a warning.

Extra claims

Normally pyramid_jwt only makes a single JWT claim: the subject (or sub claim) is set to the principal. You can also add extra claims to the token by passing keyword parameters to the create_jwt_token method.

token = request.create_jwt_token(user.id,
    name=user.name,
    admin=(user.role == 'admin'))

All claims found in a JWT token can be accessed through the jwt_claims dictionary property on a request. For the above example you can retrieve the name and admin-status for the user directly from the request:

print('User id: %d' % request.authenticated_userid)
print('Users name: %s', request.jwt_claims['name'])
if request.jwt_claims['admin']:
   print('This user is an admin!')

Keep in mind that data jwt_claims only reflects the claims from a JWT token and do not check if the user is valid: the callback configured for the authentication policy is not checked. For this reason you should always use request.authenticated_userid instead of request.jwt_claims['sub'].

You can also use extra claims to manage extra principals for users. For example you could claims to represent add group membership or roles for a user. This requires two steps: first add the extra claims to the JWT token as shown above, and then use the authentication policy’s callback hook to turn the extra claim into principals. Here is a quick example:

def add_role_principals(userid, request):
   return ['role:%s' % role for role in request.jwt_claims.get('roles', [])]

config.set_jwt_authentication_policy(callback=add_role_principals)

You can then use the role principals in an ACL:

class MyView:
    __acl__ = [
        (Allow, Everyone, ['read']),
        (Allow, 'role:admin', ['create', 'update']),
    ]

Validation Example

After creating and returning the token through your API with create_jwt_token you can test by issuing an HTTP authorization header type for JWT.

GET /resource HTTP/1.1
Host: server.example.com
Authorization: JWT eyJhbGciOiJIUzI1NiIXVCJ9...TJVA95OrM7E20RMHrHDcEfxjoYZgeFONFh7HgQ

We can test using curl.

curl --header 'Authorization: JWT TOKEN' server.example.com/ROUTE_PATH
config.add_route('example', '/ROUTE_PATH')
@view_config(route_name=example)
def some_action(request):
    if request.authenticated_userid:
        # Do something

Settings

There are a number of flags that specify how tokens are created and verified. You can either set this in your .ini-file, or pass/override them directly to the config.set_jwt_authentication_policy() function.

Parameter

ini-file entry

Default

Description

private_key

jwt.private_key

Key used to hash or sign tokens.

public_key

jwt.public_key

Key used to verify token signatures. Only used with assymetric algorithms.

algorithm

jwt.algorithm

HS512

Hash or encryption algorithm

expiration

jwt.expiration

Number of seconds (or a datetime.timedelta instance) before a token expires.

audience

jwt.audience

Proposed audience for the token

leeway

jwt.leeway

0

Number of seconds a token is allowed to be expired before it is rejected.

http_header

jwt.http_header

Authorization

HTTP header used for tokens

auth_type

jwt.auth_type

JWT

Authentication type used in Authorization header. Unused for other HTTP headers.

json_encoder

None

A subclass of JSONEncoder to be used to encode principal and claims infos.

Pyramid JWT example use cases

This is a basic guide (that will assume for all following statements that you have followed the Readme for this project) that will explain how (and why) to use JWT to secure/restrict access to a pyramid REST style backend API, this guide will explain a basic overview on:

  • Creating JWT’s

  • Decoding JWT’s

  • Restricting access to certain pyramid views via JWT’s

Creating JWT’s

First off, lets start with the first view in our pyramid project, this would normally be say a login view, this view has no permissions associated with it, any user can access and post login credentials to it, for example:

def authenticate_user(login, password):
    # Note the below will not work, its just an example of returning a user
    # object back to the JWT creation.
    login_query = session.query(User).\
        filter(User.login == login).\
        filter(User.password == password).first()

    if login_query:
        user_dict = {
            'userid': login_query.id,
            'user_name': login_query.user_name,
            'roles': login_query.roles
        }
        # An example of login_query.roles would be a list
        # print(login_query.roles)
        # ['admin', 'reports']
        return user_dict
    else:
        # If we end up here, no logins have been found
        return None

@view_config('login', request_method='POST', renderer='json')
def login(request):
    '''Create a login view
    '''
    login = request.POST['login']
    password = request.POST['password']
    user = authenticate(login, password)
    if user:
        return {
            'result': 'ok',
            'token': request.create_jwt_token(
                                            user['userid'],
                                            roles=user['roles'],
                                            userName=user['user_name']
                                            )
        }
    else:
        return {
            'result': 'error',
            'token': None
        }

Now what this does is return your JWT back to whatever front end application you may have, with the user details, along with their permissions, this will return a decoded token such as:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyTmFtZSI6Imx1a2UiLCJyb2xlcyI6WyJhZG1pbiIsInJlcG9ydHMiXSwic3ViIjo0LCJpYXQiOjE1MTkwNDQyNzB9.__KjyW1U-tpAEvTbSJsasS-8CaFyXH784joUPONH6hQ

Now I would suggest heading over to JWT.io, copy this data into their page, and you will see the decoded token:

{
  "userName": "luke",
  "roles": [
    "admin",
    "reports"
  ],
  "sub": 4,
  "iat": 1519044270
}

Note, at the bottom of jwt.io’s webpage, that the signature shows verified, if you change the “secret” at the bottom, it will say “NOT Verified” this is because in order for any JWT process to be verified, the valid “secret” or “private key” must be used. It is important to note that any data sent in a JWT is accessible and readable by anyone.

Decoding JWT

The following section would also work if pyramid did not create the JWT, all it needs to know to decode a JWT is the “secret” or “private key” used to create/sign the original JWT.By their nature JWT’s aren’t secure, but they can be used “to secure”. In our example above, we returned the “roles” array in our JWT, this had two properties “admin” and “reports” so we could then in our pyramid application, setup an ACL to map JWT permissions to pyramid based security, for example in our projects __init__.py we could add:

from pyramid.security import ALL_PERMISSIONS

class RootACL(object):
    __acl__ = [
        (Allow, 'admin', ALL_PERMISSIONS),
        (Allow, 'reports', ['reports'])
    ]

    def __init__(self, request):
        pass

What this ACL will do is allow anyone with the “admin” role in their JWT access to all views protected via a permission, where as users with “reports” in their JWT will only have access to views protected via the “reports” permission.

Now this ACL in itself is not enough to map the JWT permission to pyramids security backend, we need to also add the following to __init__.py:

from pyramid.authorization import ACLAuthorizationPolicy


def add_role_principals(userid, request):
    return request.jwt_claims.get('roles', [])

def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """
    config = Configurator(settings=settings)
    ...
    # Enable JWT - JSON Web Token based authentication
    config.set_root_factory(RootACL)
    config.set_authorization_policy(ACLAuthorizationPolicy())
    config.include('pyramid_jwt')
    config.set_jwt_authentication_policy('myJWTsecretKeepThisSafe',
                                        auth_type='Bearer',
                                        callback=add_role_principals)

This code will map any properties of the “roles” attribute of the JWT, run them through the ACL and then tie them into pyramids security framework.

How is this secure?

For example, a JWT could easily be manipulated, anyone could hijack the token, change the values of the “roles” array to gain access to a view they do not actually have access to. WRONG! pyramid_jwt checks the signature of all JWT tokens as part of the decode process, if it notices that the signature of the token is not as expected, it means either the application has been setup correctly with the wrong private key, OR an attacker has tried to manipulate the token.

Securing views with JWT’s

In the example posted above we creating an “admin” role that we gave ALL_PERMISSIONS access in our ACL, so any user with this role could access any view e.g.:

@view_config(route_name='view_a', request_method='GET',
             permission="admin", renderer='json')
def view_a(request):
    return

@view_config(route_name='view_b', request_method='GET',
             permission="cpanel", renderer='json')
def view_b(request):
    return

This user would be able to access both of these views, however any user with the “reports” permission would not be able to access any of these views, they could only access permissions with “reports”. Obviously in our use case, one user had both “admin” and “reports” permissions, so they would be able to access any view regardless.

Changelog

1.4.1 - August 10, 2018

1.4 - August 9, 2018

1.3 - March 20, 2018

1.2 - May 25, 2017

1.1 - May 4, 2016

  • Issue #2: Support setting and reading extra claims in a JWT token.

  • Pull request #4: Fix parsing of expiration and leeway settings from a configuration value. Submitted by Daniel Kraus.

  • Pull request #3: Allow overriding the expiration timestamp for a token when creating a new token. Submitted by Daniel Kraus.

1.0 - December 17, 2015

  • First release

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyramid_jwt_policy-1.0.0.tar.gz (12.5 kB view details)

Uploaded Source

Built Distribution

pyramid_jwt_policy-1.0.0-py2.py3-none-any.whl (8.6 kB view details)

Uploaded Python 2 Python 3

File details

Details for the file pyramid_jwt_policy-1.0.0.tar.gz.

File metadata

  • Download URL: pyramid_jwt_policy-1.0.0.tar.gz
  • Upload date:
  • Size: 12.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.11.0 pkginfo/1.4.2 requests/2.18.4 setuptools/39.0.1 requests-toolbelt/0.8.0 tqdm/4.26.0 CPython/3.6.5

File hashes

Hashes for pyramid_jwt_policy-1.0.0.tar.gz
Algorithm Hash digest
SHA256 37a4980ffdf60dde072a0e4f526f83dac760e213a38a6b22d64abccdbf8e248e
MD5 fca7a9e6b6e339ceafe33e694819b235
BLAKE2b-256 b0121f9bde63879178f70223f0737221a78dfba538fee0980c4cd5fe96a1b3b4

See more details on using hashes here.

File details

Details for the file pyramid_jwt_policy-1.0.0-py2.py3-none-any.whl.

File metadata

  • Download URL: pyramid_jwt_policy-1.0.0-py2.py3-none-any.whl
  • Upload date:
  • Size: 8.6 kB
  • Tags: Python 2, Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.11.0 pkginfo/1.4.2 requests/2.18.4 setuptools/39.0.1 requests-toolbelt/0.8.0 tqdm/4.26.0 CPython/3.6.5

File hashes

Hashes for pyramid_jwt_policy-1.0.0-py2.py3-none-any.whl
Algorithm Hash digest
SHA256 8f8a12ef4bc55c2b77e87a9187c5b36368319f402e87397bb52264050e78d79d
MD5 f4c6d3122e74fc746a1dab438c2c2647
BLAKE2b-256 27e2f9f11c18da6bac02f8c691aa6597d1cca5d9bb2dc9b5c5dd1d056bfa595b

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page