- Notifications
You must be signed in to change notification settings - Fork31
JWT authentication for Pyramid
License
wichert/pyramid_jwt
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
This package implements an authentication policy for Pyramid that usingJSONWeb Tokens. This standard (RFC 7519) is often used to secure backend APIs.The excellentPyJWT library isused 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 the Authorization HTTP headerwith 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 anAudience 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 newcreate_jwt_token
method isadded to the request. You can use this in your view to create tokens. A simpleauthentication 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' }
Unless you are using JWT cookies within cookies (see the next section), thestandardremember()
andforget()
functions from Pyramid are not useful.Trying to use them while regular (header-based) JWT authentication is enabledwill result in a warning.
Optionally, you can use cookies as a transport for the JWT Cookies. This is anuseful technique to allow browser-based web apps to consume your REST APIswithout the hassle of managing token storage (where to store JWT cookies is aknown-issue), sincehttp_only
cookies cannot be handled by Javascriptrunning on the page
Using JWT within cookies have some added benefits, the first one beingslidingsessions: Tokens inside cookies will automatically be reissued wheneverreissue_time
is past.
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_cookie_authentication_policy('secret',reissue_time=7200 )
When working with JWT alone, there's no standard for manually invalidating atoken: Either the token validity expires, or the application needs to handle atoken blacklist (or even better, a whitelist)
On the other hand, when using cookies, this library allows the app tologouta given user by erasing its cookie: This policy follows the standard cookiedeletion mechanism respected by most browsers, so a call to Pyramid'sforget()
function will instruct the browser remove that cookie, effectivelythrowing that JWT token away, even though it may still be valid.
SeeCreating a JWT within a cookie for examples.
Normally pyramid_jwt only makes a single JWT claim: thesubject (orsub
claim) is set to the principal. You can also add extra claims to thetoken by passing keyword parameters to thecreate_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 thejwt_claims
dictionary property on a request. For the above example you can retrieve thename 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_claims
only reflects the claims from a JWTtoken and do not check if the user is valid: the callback configured for theauthentication policy isnot checked. For this reason you should always userequest.authenticated_userid
instead ofrequest.jwt_claims['sub']
.
You can also use extra claims to manage extra principals for users. For exampleyou could claims to represent add group membership or roles for a user. Thisrequires 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 claiminto 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']), ]
After creating and returning the token through your API withcreate_jwt_token
you can test by issuing an HTTP authorization header typefor JWT.
GET /resource HTTP/1.1Host: server.example.comAuthorization: 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)defsome_action(request):ifrequest.authenticated_userid:# Do something
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 theconfig.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. Onlyused with asymmetric algorithms. | |
algorithm | jwt.algorithm | HS512 | Hash or encryption algorithm |
expiration | jwt.expiration | Number of seconds (or a datetime.timedeltainstance) before a token expires. | |
audience | jwt.audience | Proposed audience for the token | |
leeway | jwt.leeway | 0 | Number of seconds a token is allowed to beexpired 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 Authorizationheader. Unused for other HTTP headers. |
json_encoder | None | A subclass of JSONEncoder to be usedto encode principal and claims infos. |
The follow options applies to the cookie-based authentication policy:
Parameter | ini-file entry | Default | Description |
---|---|---|---|
cookie_name | jwt.cookie_name | Authorization | Key used to identify the cookie. |
cookie_path | jwt.cookie_path | None | Path for cookie. |
https_only | jwt.https_only_cookie | True | Whether or not the token should only besent through a secure HTTPS transport |
reissue_time | jwt.cookie_reissue_time | None | Number of seconds (or a datetime.timedeltainstance) before a cookie (and the tokenwithin it) is reissued |
This is a basic guide (that will assume for all following statements that youhave followed the Readme for this project) that will explain how (and why) touse JWT to secure/restrict access to a pyramid REST style backend API, thisguide will explain a basic overview on:
- Creating JWT's
- Decoding JWT's
- Restricting access to certain pyramid views via JWT's
First off, lets start with the first view in our pyramid project, this wouldnormally 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 applicationyou may have, with the user details, along with their permissions, this willreturn a decoded token such as:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyTmFtZSI6Imx1a2UiLCJyb2xlcyI6WyJhZG1pbiIsInJlcG9ydHMiXSwic3ViIjo0LCJpYXQiOjE1MTkwNDQyNzB9.__KjyW1U-tpAEvTbSJsasS-8CaFyXH784joUPONH6hQ
Now I would suggest heading over toJWT.io, copy this datainto 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, ifyou change the "secret" at the bottom, it will say "NOT Verified" this isbecause 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 JWTis accessible and readable by anyone.
The following section would also work if pyramid did not create the JWT, all itneeds to know to decode a JWT is the "secret" or "private key" used tocreate/sign the original JWT.By their nature JWT's aren't secure, but they canbe used "to secure". In our example above, we returned the "roles" array in ourJWT, this had two properties "admin" and "reports" so we could then in ourpyramid application, setup an ACL to map JWT permissions to pyramid basedsecurity, 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):pass
What this ACL will do is allow anyone with the "admin" role in their JWT accessto all views protected via a permission, where as users with "reports" in theirJWT 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 pyramidssecurity 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 themthrough the ACL and then tie them into pyramids security framework.
Since cookie-based authentication is already standardized within Pyramid by theremember()
andforget()
calls, you should simply use them:
frompyramid.responseimportResponsefrompyramid.securityimportremember@view_config('login',request_method='POST',renderer='json')deflogin_with_cookies(request):'''Create a login view '''login=request.POST['login']password=request.POST['password']user=authenticate(login,password)# From the previous snippetifuser:headers=remember(user['userid'],roles=user['roles'],userName=user['user_name'] )returnResponse(headers=headers,body="OK")# Or maybe redirect somewhere elsereturnResponse(status=403)# Or redirect back to login
Please note that since the JWT cookies will be stored inside the cookies,there's no need for your app to explicitly include it on the response body.The browser (or whatever consuming this response) is responsible to keep thatcookie for as long as it's valid, and re-send it on the following requests.
Also note that there's no need to decode the cookie manually. The Policyhandles that through the existingrequest.jwt_claims
.
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 notactually have access to. WRONG! pyramid_jwt checks the signature of all JWTtokens as part of the decode process, if it notices that the signature of thetoken is not as expected, it means either the application has been setupcorrectly with the wrong private key, OR an attacker has tried to manipulatethe token.
The major security concern when working with JWT tokens is where to store thetoken itself: While pyramid_jwt is able to detect tampered tokens, nothing canbe done if the actual valid token leaks. Any user with a valid token will becorrectly authenticated within your app. Storing the token securely is outsidethe scope of this library.
When using JWT within a cookie, the browser (or tool consuming the cookie) isresponsible for storing it, but pyramid_jwt does set thehttp_only
flag onall cookies, so javascript running on the page cannot access these cookies,which helps mitigate XSS attacks. It's still mentioning that the tokens arestill visible through the browser's debugging/inspection tools.
In the example posted above we creating an "admin" role that we gaveALL_PERMISSIONS access in our ACL, so any user with this role could access anyview 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):return
This user would be able to access both of these views, however any user withthe "reports" permission would not be able to access any of these views, theycould only access permissions with "reports". Obviously in our use case, oneuser had both "admin" and "reports" permissions, so they would be able toaccess any view regardless.
About
JWT authentication for Pyramid