hug
hug copied to clipboard
Added JWT authentication.
I've included examples of use in the examples folder.
A possible enhancement is that the jwt token is considered for renewal while it is being verified.
Coverage decreased (-1.7%) to 98.299% when pulling 5d16cfb4ee790c8670798dfcb5ba6bbffb5de1ff on dsmurrell:develop into 78a6be2c2a8edfc7b0c4f6c259fe5e39ae9cfddb on timothycrosley:develop.
Coverage decreased (-1.5%) to 98.48% when pulling a5cd7f52d3213438c75df122110c10489df6d100 on dsmurrell:develop into 78a6be2c2a8edfc7b0c4f6c259fe5e39ae9cfddb on timothycrosley:develop.
I've modified the code to catch jwt.InvalidTokenError exceptions.
This is the base class which includes exceptions such as jwt.ExpiredSignatureError and jwt.DecodeError. jwt.ExpiredSignatureError will be far more common so it should definitely catch these silently. I'm less convinced about catching jwt.DecodeErrors silently as this is relatively unexpected.
It's possible I should only be catching jwt.ExpiredSignatureError exceptions?
For someone trying to get this working from the client side, it would be quite useful to know which exception is being caught. Another question is, how would someone who is trying to get this working for the first time get feedback about what exception is being thrown?
Hi @dsmurrell,
In general, the design currently is such that if there is an error you want to customize output for the correct way to do that is simply to allow the exception to bubble up, so incorrect credentials should be handled as False, and for others simply raise. In the case that you want to provide additional information beyond this for handling the exception, you could add contextual information to the request.context or in the exception that you raise for things higher up to introspect.
Thanks!
~Timothy
Hey guys, how is this going? JWT is a must-have auth backend for every serious backend microservice so what's left? can we help?
Cheers, Dgz
We have already implemented the JWT authentication using the provided hug.hug.authentication.token that supports treating Authorization header in a raw way, I can't see the point to implement full fledge JWT authenticator, microframeworks are meant to not add unnecessary stuff, if you @timothycrosley wants us to do a PR with a fully functional example in the examples folder we can do it, I paste here a working summary of it, it raises a falcon.HTTPUnauthorized producing a proper 401 response with the corresponding error as message:
import hug
import jwt # pip install pyjwt
from datetime import datetime, timedelta
from falcon import HTTPUnauthorized
# These can be in a constants/settings file
JWT_OPTIONS = {
'verify_signature': True,
'verify_exp': True,
'verify_nbf': False,
'verify_iat': False,
'verify_aud': True,
'verify_iss': True,
'require_exp': True,
'require_iat': False,
'require_nbf': False
}
SECRET_KEY = 'some-super-secret'
JWT_ISSUER = 'some-issuer'
JWT_AUDIENCE = 'some-audience'
JWT_OPTIONS_ALGORITHM = 'HS256'
def decode_token(token, options=JWT_OPTIONS):
"""
Decodes a JWT token and returns payload info
:return:
"""
return jwt.decode(
token,
SECRET_KEY,
issuer=JWT_ISSUER,
audience=JWT_AUDIENCE,
options=options,
algorithms=(JWT_OPTIONS_ALGORITHM,)
)
def create_token(payload, exp=None) -> object:
"""
Creates JWT Token
:return:
"""
if exp is None:
exp = datetime.utcnow() + timedelta(seconds=3600)
_token = {
'aud': JWT_AUDIENCE,
'exp': exp,
'iss': JWT_ISSUER,
}
_token.update(payload)
return jwt.encode(
_token,
SECRET_KEY,
algorithm=JWT_OPTIONS_ALGORITHM
).decode('utf-8')
def parse_header(authorization_header, exception_class=HTTPUnauthorized):
"""
Parses an authorization header. Accepts a custom Exception if any failure
:param authorization_header:
:param exception_class:
:return:
"""
if authorization_header:
parts = authorization_header.split()
if parts[0].lower() != 'bearer' or len(parts) == 1 or len(
parts) > 2:
raise exception_class("invalid_header")
jwt_token = parts[1]
try:
# Decode token
payload = decode_token(jwt_token)
return jwt_token, payload
except jwt.ExpiredSignature: # pragma: no cover
raise exception_class("token is expired")
except jwt.InvalidAudienceError: # pragma: no cover
raise exception_class("incorrect audience")
except jwt.DecodeError: # pragma: no cover
raise exception_class("token signature is invalid")
except jwt.InvalidIssuerError: # pragma: no cover
raise exception_class("token issuer is invalid")
except Exception as exc: # pragma: no cover
raise exception_class(exc)
else: # pragma: no cover
raise exception_class("Authorization header not present")
def jwt_token_verify(auth_header):
"""
Verifier function for hug token authenticator
:param auth_header:
:return:
"""
# Hug do not extract Bearer prefix
auth_token, payload = parse_header(auth_header)
return payload
token_key_authentication = hug.authentication.token(jwt_token_verify)
# Then the protected API
@hug.get('/jwt', requires=token_key_authentication)
def some_jwt_endpoint():
return "I'm JWT!"
# Then the JWT Token Generator
@hug.get('/token')
def some_jwt_endpoint():
return create_token({'some': 'payload'})
Hello @timothycrosley, @dsmurrell and @OGKevin. What's the status of this PR? It seemed to be almost mergeable some months ago, but nothing new since. Is there something we can do to help? I've seen with interest @danigosa 's solution, but I'm not sure whether it's unit-tested somewhere, respecting the standard, and it misses the token refreshing capabilities that I rely on.
@briceparent im not an official maintainer. It is up to @timothycrosley to merge.
I included everyone that seemed to be involved in this thread, as you all might have direct or off-the-record information about that too. But thanks for your very quick answer!
@dsmurrell are you planning on finishing this?
@tadeoiiit, I did want to finish this at some point. I ended up moving to work on another project that had flask serving its backend so I never got around to finishing this.
@timothycrosley, has much changed on the authentication side? Is this PR still something that could benefit people if completed? Is so, I can put in some work this weekend to finish it.
@dsmurrell authentication is mostly the same, I think finishing it would be useful! We may end up moving it into a plugin - but either way it's important to support :) Thank you for your work on it!