-
-
Notifications
You must be signed in to change notification settings - Fork 387
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added JWT authentication. #537
base: develop
Are you sure you want to change the base?
Conversation
Remove trailing white space that linter is complaining about
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few comments and question👀
Sentences are not beginning with a Capital letter and don't end with a .
, some print
statements and redundant else
's
hug/authentication.py
Outdated
return jwt.encode({'user_id': decoding['user_id'], | ||
'exp': datetime.utcnow() + timedelta(seconds=token_expiration_seconds)}, | ||
jwt_secret, algorithm='HS256').decode("utf-8") | ||
else: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this else
statement redundant ? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does a Python function return None if it reaches its end? Even so, isn't it better to be explicit about where you return None and what it means?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wel, yes python automatically returns None
but there is no harm in explicitly returning None
yourself. My point here was the following:
if x:
return x
else:
return y
Can be rewritten to:
if x:
return x
return y
As of there is no need to say else
because the if
statement exists the function if its true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I guess it is strictly redundant. It also takes the same space. I prefer the readability of the former though as it links the code in the else to if statement. Is there a general standard on the best way in Python?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well it is opinion based really as you can see here.
So i guess we should see what @timothycrosley thinks about this one. Or we can search in the project's code for an if else return block code and use that as reference to be consistent with the already written code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the feedback btw. I'm new to this open source collaboration stuff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dsmurrell, I want to stress that for being new to open source collaboration, this is a very good high quality first pull request, great work!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a slight preference for removing the return None
in both places, and then remove the else and let it fall through
hug/authentication.py
Outdated
try: | ||
token = authorization.split(' ')[1] | ||
decoding = jwt.decode(token, jwt_secret, algorithm='HS256') | ||
print(decoding) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this print statement suppose to be there ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right. It's not.
hug/authentication.py
Outdated
return decoding['user_id'] | ||
except Exception as ex: | ||
template = "An exception of type {0} occurred. Arguments:\n{1!r}" | ||
print(template.format(type(ex).__name__, ex.args)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above ☝️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the standard way to report on something unexpected? I would normally use a logger, but it's not in this scope. @timothycrosley how do you normally handle reporting unexpected exceptions in hug?
hug/authentication.py
Outdated
return None | ||
except Exception as ex: | ||
template = "An exception of type {0} occurred. Arguments:\n{1!r}" | ||
print(template.format(type(ex).__name__, ex.args)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suppose to print ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above. Looking for the way hug reports or outputs unexpected exceptions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @dsmurrell,
The answer is that you should catch any errors you expect and want to use to change output, and let any errors you don't expect bubble up and be handled by code higher up.
Thanks!
~Timothy
examples/authentication.py
Outdated
replace_this = False | ||
config = { | ||
'jwt_secret': 'super-secret-key-please-change', | ||
# token will expire in 3600 seconds if it is not refreshed and the user will be required to log in again |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your sentences do not begin with a Capital letter and don't end with a dot(.
), is this intended ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty new to collaboration. Is that the standard for comments? Begin with a capital letter and end with a dot?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I mean normally sentences have a Capital letter at the start and ends with a dot 😝. But I can see that this projects sentences don't end with a dot xD
Ex: https://github.com/timothycrosley/hug/blob/a2dfdd71dc5cb809107ca710fd6c0629c6802cfc/hug/api.py#L77
So lets stick with the project's standard and use a Capital letter for the begging of the sentence. 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PEP seems to indicate that "If a comment is short, the period at the end can be omitted."
I usually start comments with lower case, but yeah, that doesn't seem compliant. I'll change this then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yea that is true, guess we have different standards for short comments 😁 .
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the few small changes here that you and @OGKevin
had talked about implemented, I would be happy to pull this in, thanks for the great work!
~Timothy
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exception questions 🤔
hug/authentication.py
Outdated
except Exception as ex: | ||
template = "An exception of type {0} occurred. Arguments:\n{1!r}" | ||
print(template.format(type(ex).__name__, ex.args)) | ||
except: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you catching all exceptions ? 😮 Wouldn't this make things hard to debug ?
Isn't there a specific exception that can be caught ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it would, I'll go figure out which one/s I'm expecting to catch when the token is no longer valid. Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@OGKevin If an exception is thrown that isn't caught here what happens to the server? Does it continue running and print and error message, or does it break? Where is the code that handles the exceptions that get bubbled up?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dsmurrell The individual request will die, but the server itself will keep running, if you have an @hug.exception handler you can manage how that exception is handled
hug/authentication.py
Outdated
except Exception as ex: | ||
template = "An exception of type {0} occurred. Arguments:\n{1!r}" | ||
print(template.format(type(ex).__name__, ex.args)) | ||
except: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as above ☝️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2 new lines between functions that are not nested.
# Enable authenticated endpoints, example @authenticated.get('/users/me'). | ||
authenticated = hug.http(requires=hug.authentication.json_web_token(hug.authentication.verify_jwt, config['jwt_secret'])) | ||
|
||
# Check the token and issue a new one if it is about to expire (within token_refresh_seconds from expiry). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Normally there will be 2 new lines between function and classes and one new line between class methods. Please add a new line.
if token: | ||
response.set_header('token', token) | ||
|
||
@hug.post('/login') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
☝️
hug/authentication.py
Outdated
|
||
return wrapper | ||
|
||
@jwt_authenticator |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
☝️ 2 new lines between functions
else: | ||
return False | ||
return None | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
☝️
hug/authentication.py
Outdated
'exp': datetime.utcnow() + timedelta(seconds=token_expiration_seconds)}, | ||
jwt_secret, algorithm='HS256').decode("utf-8") | ||
|
||
def refresh_jwt(authorization, token_refresh_seconds, token_expiration_seconds, jwt_secret): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
☝️
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 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, |
We have already implemented the JWT authentication using the provided 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. |
@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! |
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.