Package: pyramid-jwt-policy
JWT authentication for PyramidThis package implements an authentication policy for Pyramid that usingJSON
Web Tokens. This standard (RFC 7519) is often used to secure backens APIs.
The excellentPyJWTlibrary is
used for the JWT encoding / decoding logic.Enabling JWT support in a Pyramid application is very simple:frompyramid.configimportConfiguratorfrompyramid.authorizationimportACLAuthorizationPolicydefmain():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 theAuthorizationHTTP header
with aJWTscheme 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 anAudienceclaim 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 newcreate_jwt_tokenmethod 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')deflogin(request):login=request.POST['login']password=request.POST['password']user_id=authenticate(login,password)# You will need to implement this.ifuser_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
standardremember()andforget()functions from Pyramid are not useful.
Trying to use them while JWT authentication is enabled will result in a warning.Extra claimsNormally pyramid_jwt only makes a single JWT claim: thesubject(orsubclaim) is set to the principal. You can also add extra claims to the
token by passing keyword parameters to thecreate_jwt_tokenmethod.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 thejwt_claimsdictionary 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'])ifrequest.jwt_claims['admin']:print('This user is an admin!')Keep in mind that datajwt_claimsonly reflects the claims from a JWT
token and do not check if the user is valid: the callback configured for the
authentication policy isnotchecked. For this reason you should always userequest.authenticated_useridinstead ofrequest.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:defadd_role_principals(userid,request):return['role:%s'%roleforroleinrequest.jwt_claims.get('roles',[])]config.set_jwt_authentication_policy(callback=add_role_principals)You can then use the role principals in an ACL:classMyView:__acl__=[(Allow,Everyone,['read']),(Allow,'role:admin',['create','update']),]Validation ExampleAfter creating and returning the token through your API withcreate_jwt_tokenyou can test by issuing an HTTP authorization header type
for JWT.GET /resource HTTP/1.1
Host: server.example.com
Authorization: JWT eyJhbGciOiJIUzI1NiIXVCJ9...TJVA95OrM7E20RMHrHDcEfxjoYZgeFONFh7HgQWe can test using curl.curl--header'Authorization: JWT TOKEN'server.example.com/ROUTE_PATHconfig.add_route('example','/ROUTE_PATH')@view_config(route_name=example)defsome_action(request):ifrequest.authenticated_userid:# Do somethingSettingsThere 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 theconfig.set_jwt_authentication_policy()function.Parameterini-file entryDefaultDescriptionprivate_keyjwt.private_keyKey used to hash or sign tokens.public_keyjwt.public_keyKey used to verify token signatures. Only
used with assymetric algorithms.algorithmjwt.algorithmHS512Hash or encryption algorithmexpirationjwt.expirationNumber of seconds (or a datetime.timedelta
instance) before a token expires.audiencejwt.audienceProposed audience for the tokenleewayjwt.leeway0Number of seconds a token is allowed to be
expired before it is rejected.http_headerjwt.http_headerAuthorizationHTTP header used for tokensauth_typejwt.auth_typeJWTAuthentication type used in Authorization
header. Unused for other HTTP headers.json_encoderNoneA subclass of JSONEncoder to be used
to encode principal and claims infos.Pyramid JWT example use casesThis 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’sDecoding JWT’sRestricting access to certain pyramid views via JWT’sCreating JWT’sFirst 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:defauthenticate_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()iflogin_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']returnuser_dictelse:# If we end up here, no logins have been foundreturnNone@view_config('login',request_method='POST',renderer='json')deflogin(request):'''Create a login view
    '''login=request.POST['login']password=request.POST['password']user=authenticate(login,password)ifuser: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-8CaFyXH784joUPONH6hQNow I would suggest heading over toJWT.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 JWTThe 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:frompyramid.securityimportALL_PERMISSIONSclassRootACL(object):__acl__=[(Allow,'admin',ALL_PERMISSIONS),(Allow,'reports',['reports'])]def__init__(self,request):passWhat 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:frompyramid.authorizationimportACLAuthorizationPolicydefadd_role_principals(userid,request):returnrequest.jwt_claims.get('roles',[])defmain(global_config,**settings):""" This function returns a Pyramid WSGI application.
    """config=Configurator(settings=settings)...# Enable JWT - JSON Web Token based authenticationconfig.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’sIn 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')defview_a(request):return@view_config(route_name='view_b',request_method='GET',permission="cpanel",renderer='json')defview_b(request):returnThis 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.Changelog1.4.1 - August 10, 2018Pull request #23:
Allow specifying the audience in the app configuration, fromJohn Stevens II.1.4 - August 9, 2018Pull request #21:
add support for JWT aud claims, fromLuke Crooks.1.3 - March 20, 2018Issue #20:
Fix handling of public keys.Pull request #17:
a lot of documentation improvements fromLuke Crooks.1.2 - May 25, 2017Fix alog.warndeprecation warning on Python 3.6.Documentation improvements, courtesy ofÉric AraujoandGuillermo Cruz.Pull request #10Allow use of a custom JSON encoder.
Submitted byJulien Meyer.1.1 - May 4, 2016Issue #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 byDaniel Kraus.Pull request #3:
Allow overriding the expiration timestamp for a token when creating a new
token. Submitted byDaniel Kraus.1.0 - December 17, 2015First release