1
0
mirror of https://github.com/RaidMax/IW4M-Admin.git synced 2025-07-02 18:10:33 -05:00

Add server version to master api

Add IsEvadedOffense to EFPenalty
Fix remote log reading in not Windows
This commit is contained in:
RaidMax
2018-12-16 21:16:56 -06:00
parent 4067217830
commit 9f92b64135
1676 changed files with 287228 additions and 66 deletions

View File

@ -0,0 +1,12 @@
from .jwt_manager import JWTManager
from .view_decorators import (
jwt_required, fresh_jwt_required, jwt_refresh_token_required, jwt_optional
)
from .utils import (
create_refresh_token, create_access_token, get_jwt_identity,
get_jwt_claims, set_access_cookies, set_refresh_cookies,
unset_jwt_cookies, get_raw_jwt, get_current_user, current_user,
get_jti, decode_token, get_csrf_token
)
__version__ = '3.8.1'

View File

@ -0,0 +1,247 @@
import datetime
from warnings import warn
from flask import current_app
# Older versions of pyjwt do not have the requires_cryptography set. Also,
# older versions will not be adding new algorithms to them, so I can hard code
# the default version here and be safe. If there is a newer algorithm someone
# wants to use, they will need newer versions of pyjwt and it will be included
# in their requires_cryptography set, and if they attempt to use it in older
# versions of pyjwt, it will kick it out as an unrecognized algorithm.
try:
from jwt.algorithms import requires_cryptography
except ImportError: # pragma: no cover
requires_cryptography = {'RS256', 'RS384', 'RS512', 'ES256', 'ES384',
'ES521', 'ES512', 'PS256', 'PS384', 'PS512'}
class _Config(object):
"""
Helper object for accessing and verifying options in this extension. This
is meant for internal use of the application; modifying config options
should be done with flasks ```app.config```.
Default values for the configuration options are set in the jwt_manager
object. All of these values are read only. This is simply a loose wrapper
with some helper functionality for flasks `app.config`.
"""
@property
def is_asymmetric(self):
return self.algorithm in requires_cryptography
@property
def encode_key(self):
return self._private_key if self.is_asymmetric else self._secret_key
@property
def decode_key(self):
return self._public_key if self.is_asymmetric else self._secret_key
@property
def token_location(self):
locations = current_app.config['JWT_TOKEN_LOCATION']
if not isinstance(locations, list):
locations = [locations]
for location in locations:
if location not in ('headers', 'cookies'):
raise RuntimeError('JWT_TOKEN_LOCATION can only contain '
'"headers" and/or "cookies"')
return locations
@property
def jwt_in_cookies(self):
return 'cookies' in self.token_location
@property
def jwt_in_headers(self):
return 'headers' in self.token_location
@property
def header_name(self):
name = current_app.config['JWT_HEADER_NAME']
if not name:
raise RuntimeError("JWT_ACCESS_HEADER_NAME cannot be empty")
return name
@property
def header_type(self):
return current_app.config['JWT_HEADER_TYPE']
@property
def access_cookie_name(self):
return current_app.config['JWT_ACCESS_COOKIE_NAME']
@property
def refresh_cookie_name(self):
return current_app.config['JWT_REFRESH_COOKIE_NAME']
@property
def access_cookie_path(self):
return current_app.config['JWT_ACCESS_COOKIE_PATH']
@property
def refresh_cookie_path(self):
return current_app.config['JWT_REFRESH_COOKIE_PATH']
@property
def cookie_secure(self):
return current_app.config['JWT_COOKIE_SECURE']
@property
def cookie_domain(self):
return current_app.config['JWT_COOKIE_DOMAIN']
@property
def session_cookie(self):
return current_app.config['JWT_SESSION_COOKIE']
@property
def cookie_samesite(self):
return current_app.config['JWT_COOKIE_SAMESITE']
@property
def csrf_protect(self):
return self.jwt_in_cookies and current_app.config['JWT_COOKIE_CSRF_PROTECT']
@property
def csrf_request_methods(self):
return current_app.config['JWT_CSRF_METHODS']
@property
def csrf_in_cookies(self):
return current_app.config['JWT_CSRF_IN_COOKIES']
@property
def access_csrf_cookie_name(self):
return current_app.config['JWT_ACCESS_CSRF_COOKIE_NAME']
@property
def refresh_csrf_cookie_name(self):
return current_app.config['JWT_REFRESH_CSRF_COOKIE_NAME']
@property
def access_csrf_cookie_path(self):
return current_app.config['JWT_ACCESS_CSRF_COOKIE_PATH']
@property
def refresh_csrf_cookie_path(self):
return current_app.config['JWT_REFRESH_CSRF_COOKIE_PATH']
@staticmethod
def _get_depreciated_csrf_header_name():
# This used to be the same option for access and refresh header names.
# This gives users a warning if they are still using the old behavior
old_name = current_app.config.get('JWT_CSRF_HEADER_NAME', None)
if old_name:
msg = (
"JWT_CSRF_HEADER_NAME is depreciated. Use JWT_ACCESS_CSRF_HEADER_NAME "
"or JWT_REFRESH_CSRF_HEADER_NAME instead"
)
warn(msg, DeprecationWarning)
return old_name
@property
def access_csrf_header_name(self):
return self._get_depreciated_csrf_header_name() or \
current_app.config['JWT_ACCESS_CSRF_HEADER_NAME']
@property
def refresh_csrf_header_name(self):
return self._get_depreciated_csrf_header_name() or \
current_app.config['JWT_REFRESH_CSRF_HEADER_NAME']
@property
def access_expires(self):
delta = current_app.config['JWT_ACCESS_TOKEN_EXPIRES']
if not isinstance(delta, datetime.timedelta) and delta is not False:
raise RuntimeError('JWT_ACCESS_TOKEN_EXPIRES must be a datetime.timedelta or False')
return delta
@property
def refresh_expires(self):
delta = current_app.config['JWT_REFRESH_TOKEN_EXPIRES']
if not isinstance(delta, datetime.timedelta) and delta is not False:
raise RuntimeError('JWT_REFRESH_TOKEN_EXPIRES must be a datetime.timedelta or False')
return delta
@property
def algorithm(self):
return current_app.config['JWT_ALGORITHM']
@property
def blacklist_enabled(self):
return current_app.config['JWT_BLACKLIST_ENABLED']
@property
def blacklist_checks(self):
check_type = current_app.config['JWT_BLACKLIST_TOKEN_CHECKS']
if not isinstance(check_type, list):
check_type = [check_type]
for item in check_type:
if item not in ('access', 'refresh'):
raise RuntimeError('JWT_BLACKLIST_TOKEN_CHECKS must be "access" or "refresh"')
return check_type
@property
def blacklist_access_tokens(self):
return 'access' in self.blacklist_checks
@property
def blacklist_refresh_tokens(self):
return 'refresh' in self.blacklist_checks
@property
def _secret_key(self):
key = current_app.config['JWT_SECRET_KEY']
if not key:
key = current_app.config.get('SECRET_KEY', None)
if not key:
raise RuntimeError('JWT_SECRET_KEY or flask SECRET_KEY '
'must be set when using symmetric '
'algorithm "{}"'.format(self.algorithm))
return key
@property
def _public_key(self):
key = current_app.config['JWT_PUBLIC_KEY']
if not key:
raise RuntimeError('JWT_PUBLIC_KEY must be set to use '
'asymmetric cryptography algorithm '
'"{}"'.format(self.algorithm))
return key
@property
def _private_key(self):
key = current_app.config['JWT_PRIVATE_KEY']
if not key:
raise RuntimeError('JWT_PRIVATE_KEY must be set to use '
'asymmetric cryptography algorithm '
'"{}"'.format(self.algorithm))
return key
@property
def cookie_max_age(self):
# Returns the appropiate value for max_age for flask set_cookies. If
# session cookie is true, return None, otherwise return a number of
# seconds a long ways in the future
return None if self.session_cookie else 2147483647 # 2^31
@property
def identity_claim_key(self):
return current_app.config['JWT_IDENTITY_CLAIM']
@property
def user_claims_key(self):
return current_app.config['JWT_USER_CLAIMS']
@property
def exempt_methods(self):
return {"OPTIONS"}
@property
def json_encoder(self):
return current_app.json_encoder
config = _Config()

View File

@ -0,0 +1,100 @@
"""
These are the default methods implementations that are used in this extension.
All of these can be updated on an app by app basis using the JWTManager
loader decorators. For further information, check out the following links:
http://flask-jwt-extended.readthedocs.io/en/latest/changing_default_behavior.html
http://flask-jwt-extended.readthedocs.io/en/latest/tokens_from_complex_object.html
"""
from flask import jsonify
def default_user_claims_callback(userdata):
"""
By default, we add no additional claims to the access tokens.
:param userdata: data passed in as the ```identity``` argument to the
```create_access_token``` and ```create_refresh_token```
functions
"""
return {}
def default_user_identity_callback(userdata):
"""
By default, we use the passed in object directly as the jwt identity.
See this for additional info:
:param userdata: data passed in as the ```identity``` argument to the
```create_access_token``` and ```create_refresh_token```
functions
"""
return userdata
def default_expired_token_callback():
"""
By default, if an expired token attempts to access a protected endpoint,
we return a generic error message with a 401 status
"""
return jsonify({'msg': 'Token has expired'}), 401
def default_invalid_token_callback(error_string):
"""
By default, if an invalid token attempts to access a protected endpoint, we
return the error string for why it is not valid with a 422 status code
:param error_string: String indicating why the token is invalid
"""
return jsonify({'msg': error_string}), 422
def default_unauthorized_callback(error_string):
"""
By default, if a protected endpoint is accessed without a JWT, we return
the error string indicating why this is unauthorized, with a 401 status code
:param error_string: String indicating why this request is unauthorized
"""
return jsonify({'msg': error_string}), 401
def default_needs_fresh_token_callback():
"""
By default, if a non-fresh jwt is used to access a ```fresh_jwt_required```
endpoint, we return a general error message with a 401 status code
"""
return jsonify({'msg': 'Fresh token required'}), 401
def default_revoked_token_callback():
"""
By default, if a revoked token is used to access a protected endpoint, we
return a general error message with a 401 status code
"""
return jsonify({'msg': 'Token has been revoked'}), 401
def default_user_loader_error_callback(identity):
"""
By default, if a user_loader callback is defined and the callback
function returns None, we return a general error message with a 401
status code
"""
return jsonify({'msg': "Error loading the user {}".format(identity)}), 401
def default_claims_verification_callback(user_claims):
"""
By default, we do not do any verification of the user claims.
"""
return True
def default_claims_verification_failed_callback():
"""
By default, if the user claims verification failed, we return a generic
error message with a 400 status code
"""
return jsonify({'msg': 'User claims verification failed'}), 400

View File

@ -0,0 +1,72 @@
class JWTExtendedException(Exception):
"""
Base except which all flask_jwt_extended errors extend
"""
pass
class JWTDecodeError(JWTExtendedException):
"""
An error decoding a JWT
"""
pass
class InvalidHeaderError(JWTExtendedException):
"""
An error getting header information from a request
"""
pass
class NoAuthorizationError(JWTExtendedException):
"""
An error raised when no authorization token was found in a protected endpoint
"""
pass
class CSRFError(JWTExtendedException):
"""
An error with CSRF protection
"""
pass
class WrongTokenError(JWTExtendedException):
"""
Error raised when attempting to use a refresh token to access an endpoint
or vice versa
"""
pass
class RevokedTokenError(JWTExtendedException):
"""
Error raised when a revoked token attempt to access a protected endpoint
"""
pass
class FreshTokenRequired(JWTExtendedException):
"""
Error raised when a valid, non-fresh JWT attempt to access an endpoint
protected by fresh_jwt_required
"""
pass
class UserLoadError(JWTExtendedException):
"""
Error raised when a user_loader callback function returns None, indicating
that it cannot or will not load a user for the given identity.
"""
pass
class UserClaimsVerificationError(JWTExtendedException):
"""
Error raised when the claims_verification_callback function returns False,
indicating that the expected user claims are invalid
"""
pass

View File

@ -0,0 +1,403 @@
import datetime
from jwt import ExpiredSignatureError, InvalidTokenError
from flask_jwt_extended.config import config
from flask_jwt_extended.exceptions import (
JWTDecodeError, NoAuthorizationError, InvalidHeaderError, WrongTokenError,
RevokedTokenError, FreshTokenRequired, CSRFError, UserLoadError,
UserClaimsVerificationError
)
from flask_jwt_extended.default_callbacks import (
default_expired_token_callback, default_user_claims_callback,
default_user_identity_callback, default_invalid_token_callback,
default_unauthorized_callback, default_needs_fresh_token_callback,
default_revoked_token_callback, default_user_loader_error_callback,
default_claims_verification_callback,
default_claims_verification_failed_callback
)
from flask_jwt_extended.tokens import (
encode_refresh_token, encode_access_token
)
from flask_jwt_extended.utils import get_jwt_identity
class JWTManager(object):
"""
An object used to hold JWT settings and callback functions for the
Flask-JWT-Extended extension.
Instances of :class:`JWTManager` are *not* bound to specific apps, so
you can create one in the main body of your code and then bind it
to your app in a factory function.
"""
def __init__(self, app=None):
"""
Create the JWTManager instance. You can either pass a flask application
in directly here to register this extension with the flask app, or
call init_app after creating this object (in a factory pattern).
:param app: A flask application
"""
# Register the default error handler callback methods. These can be
# overridden with the appropriate loader decorators
self._user_claims_callback = default_user_claims_callback
self._user_identity_callback = default_user_identity_callback
self._expired_token_callback = default_expired_token_callback
self._invalid_token_callback = default_invalid_token_callback
self._unauthorized_callback = default_unauthorized_callback
self._needs_fresh_token_callback = default_needs_fresh_token_callback
self._revoked_token_callback = default_revoked_token_callback
self._user_loader_callback = None
self._user_loader_error_callback = default_user_loader_error_callback
self._token_in_blacklist_callback = None
self._claims_verification_callback = default_claims_verification_callback
self._claims_verification_failed_callback = default_claims_verification_failed_callback
# Register this extension with the flask app now (if it is provided)
if app is not None:
self.init_app(app)
def init_app(self, app):
"""
Register this extension with the flask app.
:param app: A flask application
"""
# Save this so we can use it later in the extension
if not hasattr(app, 'extensions'): # pragma: no cover
app.extensions = {}
app.extensions['flask-jwt-extended'] = self
# Set all the default configurations for this extension
self._set_default_configuration_options(app)
self._set_error_handler_callbacks(app)
def _set_error_handler_callbacks(self, app):
"""
Sets the error handler callbacks used by this extension
"""
@app.errorhandler(NoAuthorizationError)
def handle_auth_error(e):
return self._unauthorized_callback(str(e))
@app.errorhandler(CSRFError)
def handle_auth_error(e):
return self._unauthorized_callback(str(e))
@app.errorhandler(ExpiredSignatureError)
def handle_expired_error(e):
return self._expired_token_callback()
@app.errorhandler(InvalidHeaderError)
def handle_invalid_header_error(e):
return self._invalid_token_callback(str(e))
@app.errorhandler(InvalidTokenError)
def handle_invalid_token_error(e):
return self._invalid_token_callback(str(e))
@app.errorhandler(JWTDecodeError)
def handle_jwt_decode_error(e):
return self._invalid_token_callback(str(e))
@app.errorhandler(WrongTokenError)
def handle_wrong_token_error(e):
return self._invalid_token_callback(str(e))
@app.errorhandler(RevokedTokenError)
def handle_revoked_token_error(e):
return self._revoked_token_callback()
@app.errorhandler(FreshTokenRequired)
def handle_fresh_token_required(e):
return self._needs_fresh_token_callback()
@app.errorhandler(UserLoadError)
def handler_user_load_error(e):
# The identity is already saved before this exception was raised,
# otherwise a different exception would be raised, which is why we
# can safely call get_jwt_identity() here
identity = get_jwt_identity()
return self._user_loader_error_callback(identity)
@app.errorhandler(UserClaimsVerificationError)
def handle_failed_user_claims_verification(e):
return self._claims_verification_failed_callback()
@staticmethod
def _set_default_configuration_options(app):
"""
Sets the default configuration options used by this extension
"""
# Where to look for the JWT. Available options are cookies or headers
app.config.setdefault('JWT_TOKEN_LOCATION', ['headers'])
# Options for JWTs when the TOKEN_LOCATION is headers
app.config.setdefault('JWT_HEADER_NAME', 'Authorization')
app.config.setdefault('JWT_HEADER_TYPE', 'Bearer')
# Option for JWTs when the TOKEN_LOCATION is cookies
app.config.setdefault('JWT_ACCESS_COOKIE_NAME', 'access_token_cookie')
app.config.setdefault('JWT_REFRESH_COOKIE_NAME', 'refresh_token_cookie')
app.config.setdefault('JWT_ACCESS_COOKIE_PATH', '/')
app.config.setdefault('JWT_REFRESH_COOKIE_PATH', '/')
app.config.setdefault('JWT_COOKIE_SECURE', False)
app.config.setdefault('JWT_COOKIE_DOMAIN', None)
app.config.setdefault('JWT_SESSION_COOKIE', True)
app.config.setdefault('JWT_COOKIE_SAMESITE', None)
# Options for using double submit csrf protection
app.config.setdefault('JWT_COOKIE_CSRF_PROTECT', True)
app.config.setdefault('JWT_CSRF_METHODS', ['POST', 'PUT', 'PATCH', 'DELETE'])
app.config.setdefault('JWT_ACCESS_CSRF_HEADER_NAME', 'X-CSRF-TOKEN')
app.config.setdefault('JWT_REFRESH_CSRF_HEADER_NAME', 'X-CSRF-TOKEN')
app.config.setdefault('JWT_CSRF_IN_COOKIES', True)
app.config.setdefault('JWT_ACCESS_CSRF_COOKIE_NAME', 'csrf_access_token')
app.config.setdefault('JWT_REFRESH_CSRF_COOKIE_NAME', 'csrf_refresh_token')
app.config.setdefault('JWT_ACCESS_CSRF_COOKIE_PATH', '/')
app.config.setdefault('JWT_REFRESH_CSRF_COOKIE_PATH', '/')
# How long an a token will live before they expire.
app.config.setdefault('JWT_ACCESS_TOKEN_EXPIRES', datetime.timedelta(minutes=15))
app.config.setdefault('JWT_REFRESH_TOKEN_EXPIRES', datetime.timedelta(days=30))
# What algorithm to use to sign the token. See here for a list of options:
# https://github.com/jpadilla/pyjwt/blob/master/jwt/api_jwt.py
app.config.setdefault('JWT_ALGORITHM', 'HS256')
# Secret key to sign JWTs with. Only used if a symmetric algorithm is
# used (such as the HS* algorithms). We will use the app secret key
# if this is not set.
app.config.setdefault('JWT_SECRET_KEY', None)
# Keys to sign JWTs with when use when using an asymmetric
# (public/private key) algorithm, such as RS* or EC*
app.config.setdefault('JWT_PRIVATE_KEY', None)
app.config.setdefault('JWT_PUBLIC_KEY', None)
# Options for blacklisting/revoking tokens
app.config.setdefault('JWT_BLACKLIST_ENABLED', False)
app.config.setdefault('JWT_BLACKLIST_TOKEN_CHECKS', ['access', 'refresh'])
app.config.setdefault('JWT_IDENTITY_CLAIM', 'identity')
app.config.setdefault('JWT_USER_CLAIMS', 'user_claims')
def user_claims_loader(self, callback):
"""
This decorator sets the callback function for adding custom claims to an
access token when :func:`~flask_jwt_extended.create_access_token` is
called. By default, no extra user claims will be added to the JWT.
The callback function must be a function that takes only one argument,
which is the object passed into
:func:`~flask_jwt_extended.create_access_token`, and returns the custom
claims you want included in the access tokens. This returned claims
must be JSON serializable.
"""
self._user_claims_callback = callback
return callback
def user_identity_loader(self, callback):
"""
This decorator sets the callback function for getting the JSON
serializable identity out of whatever object is passed into
:func:`~flask_jwt_extended.create_access_token` and
:func:`~flask_jwt_extended.create_refresh_token`. By default, this will
return the unmodified object that is passed in as the `identity` kwarg
to the above functions.
The callback function must be a function that takes only one argument,
which is the object passed into
:func:`~flask_jwt_extended.create_access_token` or
:func:`~flask_jwt_extended.create_refresh_token`, and returns the
JSON serializable identity of this token.
"""
self._user_identity_callback = callback
return callback
def expired_token_loader(self, callback):
"""
This decorator sets the callback function that will be called if an
expired JWT attempts to access a protected endpoint. The default
implementation will return a 401 status code with the JSON:
{"msg": "Token has expired"}
The callback must be a function that takes zero arguments, and returns
a Flask response.
"""
self._expired_token_callback = callback
return callback
def invalid_token_loader(self, callback):
"""
This decorator sets the callback function that will be called if an
invalid JWT attempts to access a protected endpoint. The default
implementation will return a 422 status code with the JSON:
{"msg": "<error description>"}
The callback must be a function that takes only one argument, which is
a string which contains the reason why a token is invalid, and returns
a Flask response.
"""
self._invalid_token_callback = callback
return callback
def unauthorized_loader(self, callback):
"""
This decorator sets the callback function that will be called if an
no JWT can be found when attempting to access a protected endpoint.
The default implementation will return a 401 status code with the JSON:
{"msg": "<error description>"}
The callback must be a function that takes only one argument, which is
a string which contains the reason why a JWT could not be found, and
returns a Flask response.
"""
self._unauthorized_callback = callback
return callback
def needs_fresh_token_loader(self, callback):
"""
This decorator sets the callback function that will be called if a
valid and non-fresh token attempts to access an endpoint protected with
the :func:`~flask_jwt_extended.fresh_jwt_required` decorator. The
default implementation will return a 401 status code with the JSON:
{"msg": "Fresh token required"}
The callback must be a function that takes no arguments, and returns
a Flask response.
"""
self._needs_fresh_token_callback = callback
return callback
def revoked_token_loader(self, callback):
"""
This decorator sets the callback function that will be called if a
revoked token attempts to access a protected endpoint. The default
implementation will return a 401 status code with the JSON:
{"msg": "Token has been revoked"}
The callback must be a function that takes no arguments, and returns
a Flask response.
"""
self._revoked_token_callback = callback
return callback
def user_loader_callback_loader(self, callback):
"""
This decorator sets the callback function that will be called to
automatically load an object when a protected endpoint is accessed.
By default this is not is not used.
The callback must take one argument which is the identity JWT accessing
the protected endpoint, and it must return any object (which can then
be accessed via the :attr:`~flask_jwt_extended.current_user` LocalProxy
in the protected endpoint), or `None` in the case of a user not being
able to be loaded for any reason. If this callback function returns
`None`, the :meth:`~flask_jwt_extended.JWTManager.user_loader_error_loader`
will be called.
"""
self._user_loader_callback = callback
return callback
def user_loader_error_loader(self, callback):
"""
This decorator sets the callback function that will be called if `None`
is returned from the
:meth:`~flask_jwt_extended.JWTManager.user_loader_callback_loader`
callback function. The default implementation will return
a 401 status code with the JSON:
{"msg": "Error loading the user <identity>"}
The callback must be a function that takes one argument, which is the
identity of the user who failed to load, and must return a Flask response.
"""
self._user_loader_error_callback = callback
return callback
def token_in_blacklist_loader(self, callback):
"""
This decorator sets the callback function that will be called when
a protected endpoint is accessed and will check if the JWT has been
been revoked. By default, this callback is not used.
The callback must be a function that takes one argument, which is the
decoded JWT (python dictionary), and returns `True` if the token
has been blacklisted (or is otherwise considered revoked), or `False`
otherwise.
"""
self._token_in_blacklist_callback = callback
return callback
def claims_verification_loader(self, callback):
"""
This decorator sets the callback function that will be called when
a protected endpoint is accessed, and will check if the custom claims
in the JWT are valid. By default, this callback is not used. The
error returned if the claims are invalid can be controlled via the
:meth:`~flask_jwt_extended.JWTManager.claims_verification_failed_loader`
decorator.
This callback must be a function that takes one argument, which is the
custom claims (python dict) present in the JWT, and returns `True` if the
claims are valid, or `False` otherwise.
"""
self._claims_verification_callback = callback
return callback
def claims_verification_failed_loader(self, callback):
"""
This decorator sets the callback function that will be called if
the :meth:`~flask_jwt_extended.JWTManager.claims_verification_loader`
callback returns False, indicating that the user claims are not valid.
The default implementation will return a 400 status code with the JSON:
{"msg": "User claims verification failed"}
This callback must be a function that takes no arguments, and returns
a Flask response.
"""
self._claims_verification_failed_callback = callback
return callback
def _create_refresh_token(self, identity, expires_delta=None):
if expires_delta is None:
expires_delta = config.refresh_expires
refresh_token = encode_refresh_token(
identity=self._user_identity_callback(identity),
secret=config.encode_key,
algorithm=config.algorithm,
expires_delta=expires_delta,
csrf=config.csrf_protect,
identity_claim_key=config.identity_claim_key,
json_encoder=config.json_encoder
)
return refresh_token
def _create_access_token(self, identity, fresh=False, expires_delta=None):
if expires_delta is None:
expires_delta = config.access_expires
access_token = encode_access_token(
identity=self._user_identity_callback(identity),
secret=config.encode_key,
algorithm=config.algorithm,
expires_delta=expires_delta,
fresh=fresh,
user_claims=self._user_claims_callback(identity),
csrf=config.csrf_protect,
identity_claim_key=config.identity_claim_key,
user_claims_key=config.user_claims_key,
json_encoder=config.json_encoder
)
return access_token

View File

@ -0,0 +1,139 @@
import datetime
import uuid
from calendar import timegm
import jwt
from werkzeug.security import safe_str_cmp
from flask_jwt_extended.exceptions import JWTDecodeError, CSRFError
def _create_csrf_token():
return str(uuid.uuid4())
def _encode_jwt(additional_token_data, expires_delta, secret, algorithm,
json_encoder=None):
uid = str(uuid.uuid4())
now = datetime.datetime.utcnow()
token_data = {
'iat': now,
'nbf': now,
'jti': uid,
}
# If expires_delta is False, the JWT should never expire
# and the 'exp' claim is not set.
if expires_delta:
token_data['exp'] = now + expires_delta
token_data.update(additional_token_data)
encoded_token = jwt.encode(token_data, secret, algorithm,
json_encoder=json_encoder).decode('utf-8')
return encoded_token
def encode_access_token(identity, secret, algorithm, expires_delta, fresh,
user_claims, csrf, identity_claim_key, user_claims_key,
json_encoder=None):
"""
Creates a new encoded (utf-8) access token.
:param identity: Identifier for who this token is for (ex, username). This
data must be json serializable
:param secret: Secret key to encode the JWT with
:param algorithm: Which algorithm to encode this JWT with
:param expires_delta: How far in the future this token should expire
(set to False to disable expiration)
:type expires_delta: datetime.timedelta or False
:param fresh: If this should be a 'fresh' token or not. If a
datetime.timedelta is given this will indicate how long this
token will remain fresh.
:param user_claims: Custom claims to include in this token. This data must
be json serializable
:param csrf: Whether to include a csrf double submit claim in this token
(boolean)
:param identity_claim_key: Which key should be used to store the identity
:param user_claims_key: Which key should be used to store the user claims
:return: Encoded access token
"""
if isinstance(fresh, datetime.timedelta):
now = datetime.datetime.utcnow()
fresh = timegm((now + fresh).utctimetuple())
token_data = {
identity_claim_key: identity,
'fresh': fresh,
'type': 'access',
}
# Don't add extra data to the token if user_claims is empty.
if user_claims:
token_data[user_claims_key] = user_claims
if csrf:
token_data['csrf'] = _create_csrf_token()
return _encode_jwt(token_data, expires_delta, secret, algorithm,
json_encoder=json_encoder)
def encode_refresh_token(identity, secret, algorithm, expires_delta, csrf,
identity_claim_key, json_encoder=None):
"""
Creates a new encoded (utf-8) refresh token.
:param identity: Some identifier used to identify the owner of this token
:param secret: Secret key to encode the JWT with
:param algorithm: Which algorithm to use for the toek
:param expires_delta: How far in the future this token should expire
(set to False to disable expiration)
:type expires_delta: datetime.timedelta or False
:param csrf: Whether to include a csrf double submit claim in this token
(boolean)
:param identity_claim_key: Which key should be used to store the identity
:return: Encoded refresh token
"""
token_data = {
identity_claim_key: identity,
'type': 'refresh',
}
if csrf:
token_data['csrf'] = _create_csrf_token()
return _encode_jwt(token_data, expires_delta, secret, algorithm,
json_encoder=json_encoder)
def decode_jwt(encoded_token, secret, algorithm, identity_claim_key,
user_claims_key, csrf_value=None):
"""
Decodes an encoded JWT
:param encoded_token: The encoded JWT string to decode
:param secret: Secret key used to encode the JWT
:param algorithm: Algorithm used to encode the JWT
:param identity_claim_key: expected key that contains the identity
:param user_claims_key: expected key that contains the user claims
:param csrf_value: Expected double submit csrf value
:return: Dictionary containing contents of the JWT
"""
# This call verifies the ext, iat, and nbf claims
data = jwt.decode(encoded_token, secret, algorithms=[algorithm])
# Make sure that any custom claims we expect in the token are present
if 'jti' not in data:
raise JWTDecodeError("Missing claim: jti")
if identity_claim_key not in data:
raise JWTDecodeError("Missing claim: {}".format(identity_claim_key))
if 'type' not in data or data['type'] not in ('refresh', 'access'):
raise JWTDecodeError("Missing or invalid claim: type")
if data['type'] == 'access':
if 'fresh' not in data:
raise JWTDecodeError("Missing claim: fresh")
if user_claims_key not in data:
data[user_claims_key] = {}
if csrf_value:
if 'csrf' not in data:
raise JWTDecodeError("Missing claim: csrf")
if not safe_str_cmp(data['csrf'], csrf_value):
raise CSRFError("CSRF double submit tokens do not match")
return data

View File

@ -0,0 +1,321 @@
from flask import current_app
from werkzeug.local import LocalProxy
try:
from flask import _app_ctx_stack as ctx_stack
except ImportError: # pragma: no cover
from flask import _request_ctx_stack as ctx_stack
from flask_jwt_extended.config import config
from flask_jwt_extended.exceptions import (
RevokedTokenError, UserClaimsVerificationError, WrongTokenError
)
from flask_jwt_extended.tokens import decode_jwt
# Proxy to access the current user
current_user = LocalProxy(lambda: get_current_user())
def get_raw_jwt():
"""
In a protected endpoint, this will return the python dictionary which has
all of the claims of the JWT that is accessing the endpoint. If no
JWT is currently present, an empty dict is returned instead.
"""
return getattr(ctx_stack.top, 'jwt', {})
def get_jwt_identity():
"""
In a protected endpoint, this will return the identity of the JWT that is
accessing this endpoint. If no JWT is present,`None` is returned instead.
"""
return get_raw_jwt().get(config.identity_claim_key, None)
def get_jwt_claims():
"""
In a protected endpoint, this will return the dictionary of custom claims
in the JWT that is accessing the endpoint. If no custom user claims are
present, an empty dict is returned instead.
"""
return get_raw_jwt().get(config.user_claims_key, {})
def get_current_user():
"""
In a protected endpoint, this will return the user object for the JWT that
is accessing this endpoint. This is only present if the
:meth:`~flask_jwt_extended.JWTManager.user_loader_callback_loader` is
being used. If the user loader callback is not being used, this will
return `None`.
"""
return getattr(ctx_stack.top, 'jwt_user', None)
def get_jti(encoded_token):
"""
Returns the JTI (unique identifier) of an encoded JWT
:param encoded_token: The encoded JWT to get the JTI from.
"""
return decode_token(encoded_token).get('jti')
def decode_token(encoded_token, csrf_value=None):
"""
Returns the decoded token (python dict) from an encoded JWT. This does all
the checks to insure that the decoded token is valid before returning it.
:param encoded_token: The encoded JWT to decode into a python dict.
:param csrf_value: Expected CSRF double submit value (optional)
"""
return decode_jwt(
encoded_token=encoded_token,
secret=config.decode_key,
algorithm=config.algorithm,
identity_claim_key=config.identity_claim_key,
user_claims_key=config.user_claims_key,
csrf_value=csrf_value
)
def _get_jwt_manager():
try:
return current_app.extensions['flask-jwt-extended']
except KeyError: # pragma: no cover
raise RuntimeError("You must initialize a JWTManager with this flask "
"application before using this method")
def create_access_token(identity, fresh=False, expires_delta=None):
"""
Create a new access token.
:param identity: The identity of this token, which can be any data that is
json serializable. It can also be a python object, in which
case you can use the
:meth:`~flask_jwt_extended.JWTManager.user_identity_loader`
to define a callback function that will be used to pull a
json serializable identity out of the object.
:param fresh: If this token should be marked as fresh, and can thus access
:func:`~flask_jwt_extended.fresh_jwt_required` endpoints.
Defaults to `False`. This value can also be a
`datetime.timedelta` in which case it will indicate how long
this token will be considered fresh.
:param expires_delta: A `datetime.timedelta` for how long this token should
last before it expires. Set to False to disable
expiration. If this is None, it will use the
'JWT_ACCESS_TOKEN_EXPIRES` config value
(see :ref:`Configuration Options`)
:return: An encoded access token
"""
jwt_manager = _get_jwt_manager()
return jwt_manager._create_access_token(identity, fresh, expires_delta)
def create_refresh_token(identity, expires_delta=None):
"""
Creates a new refresh token.
:param identity: The identity of this token, which can be any data that is
json serializable. It can also be a python object, in which
case you can use the
:meth:`~flask_jwt_extended.JWTManager.user_identity_loader`
to define a callback function that will be used to pull a
json serializable identity out of the object.
:param expires_delta: A `datetime.timedelta` for how long this token should
last before it expires. Set to False to disable
expiration. If this is None, it will use the
'JWT_REFRESH_TOKEN_EXPIRES` config value
(see :ref:`Configuration Options`)
:return: An encoded access token
"""
jwt_manager = _get_jwt_manager()
return jwt_manager._create_refresh_token(identity, expires_delta)
def has_user_loader():
jwt_manager = _get_jwt_manager()
return jwt_manager._user_loader_callback is not None
def user_loader(*args, **kwargs):
jwt_manager = _get_jwt_manager()
return jwt_manager._user_loader_callback(*args, **kwargs)
def has_token_in_blacklist_callback():
jwt_manager = _get_jwt_manager()
return jwt_manager._token_in_blacklist_callback is not None
def token_in_blacklist(*args, **kwargs):
jwt_manager = _get_jwt_manager()
return jwt_manager._token_in_blacklist_callback(*args, **kwargs)
def verify_token_type(decoded_token, expected_type):
if decoded_token['type'] != expected_type:
raise WrongTokenError('Only {} tokens are allowed'.format(expected_type))
def verify_token_not_blacklisted(decoded_token, request_type):
if not config.blacklist_enabled:
return
if not has_token_in_blacklist_callback():
raise RuntimeError("A token_in_blacklist_callback must be provided via "
"the '@token_in_blacklist_loader' if "
"JWT_BLACKLIST_ENABLED is True")
if config.blacklist_access_tokens and request_type == 'access':
if token_in_blacklist(decoded_token):
raise RevokedTokenError('Token has been revoked')
if config.blacklist_refresh_tokens and request_type == 'refresh':
if token_in_blacklist(decoded_token):
raise RevokedTokenError('Token has been revoked')
def verify_token_claims(jwt_data):
jwt_manager = _get_jwt_manager()
user_claims = jwt_data[config.user_claims_key]
if not jwt_manager._claims_verification_callback(user_claims):
raise UserClaimsVerificationError('User claims verification failed')
def get_csrf_token(encoded_token):
"""
Returns the CSRF double submit token from an encoded JWT.
:param encoded_token: The encoded JWT
:return: The CSRF double submit token
"""
token = decode_token(encoded_token)
return token['csrf']
def set_access_cookies(response, encoded_access_token, max_age=None):
"""
Takes a flask response object, and an encoded access token, and configures
the response to set in the access token in a cookie. If `JWT_CSRF_IN_COOKIES`
is `True` (see :ref:`Configuration Options`), this will also set the CSRF
double submit values in a separate cookie.
:param response: The Flask response object to set the access cookies in.
:param encoded_access_token: The encoded access token to set in the cookies.
:param max_age: The max age of the cookie. If this is None, it will use the
`JWT_SESSION_COOKIE` option (see :ref:`Configuration Options`).
Otherwise, it will use this as the cookies `max-age`.
Values should be the number of seconds (as an integer).
"""
if not config.jwt_in_cookies:
raise RuntimeWarning("set_access_cookies() called without "
"'JWT_TOKEN_LOCATION' configured to use cookies")
# Set the access JWT in the cookie
response.set_cookie(config.access_cookie_name,
value=encoded_access_token,
max_age=max_age or config.cookie_max_age,
secure=config.cookie_secure,
httponly=True,
domain=config.cookie_domain,
path=config.access_cookie_path,
samesite=config.cookie_samesite)
# If enabled, set the csrf double submit access cookie
if config.csrf_protect and config.csrf_in_cookies:
response.set_cookie(config.access_csrf_cookie_name,
value=get_csrf_token(encoded_access_token),
max_age=max_age or config.cookie_max_age,
secure=config.cookie_secure,
httponly=False,
domain=config.cookie_domain,
path=config.access_csrf_cookie_path,
samesite=config.cookie_samesite)
def set_refresh_cookies(response, encoded_refresh_token, max_age=None):
"""
Takes a flask response object, and an encoded refresh token, and configures
the response to set in the refresh token in a cookie. If `JWT_CSRF_IN_COOKIES`
is `True` (see :ref:`Configuration Options`), this will also set the CSRF
double submit values in a separate cookie.
:param response: The Flask response object to set the refresh cookies in.
:param encoded_refresh_token: The encoded refresh token to set in the cookies.
:param max_age: The max age of the cookie. If this is None, it will use the
`JWT_SESSION_COOKIE` option (see :ref:`Configuration Options`).
Otherwise, it will use this as the cookies `max-age`.
Values should be the number of seconds (as an integer).
"""
if not config.jwt_in_cookies:
raise RuntimeWarning("set_refresh_cookies() called without "
"'JWT_TOKEN_LOCATION' configured to use cookies")
# Set the refresh JWT in the cookie
response.set_cookie(config.refresh_cookie_name,
value=encoded_refresh_token,
max_age=max_age or config.cookie_max_age,
secure=config.cookie_secure,
httponly=True,
domain=config.cookie_domain,
path=config.refresh_cookie_path,
samesite=config.cookie_samesite)
# If enabled, set the csrf double submit refresh cookie
if config.csrf_protect and config.csrf_in_cookies:
response.set_cookie(config.refresh_csrf_cookie_name,
value=get_csrf_token(encoded_refresh_token),
max_age=max_age or config.cookie_max_age,
secure=config.cookie_secure,
httponly=False,
domain=config.cookie_domain,
path=config.refresh_csrf_cookie_path,
samesite=config.cookie_samesite)
def unset_jwt_cookies(response):
"""
Takes a flask response object, and configures it to unset (delete) JWTs
stored in cookies.
:param response: The Flask response object to delete the JWT cookies in.
"""
if not config.jwt_in_cookies:
raise RuntimeWarning("unset_refresh_cookies() called without "
"'JWT_TOKEN_LOCATION' configured to use cookies")
response.set_cookie(config.refresh_cookie_name,
value='',
expires=0,
secure=config.cookie_secure,
httponly=True,
domain=config.cookie_domain,
path=config.refresh_cookie_path,
samesite=config.cookie_samesite)
response.set_cookie(config.access_cookie_name,
value='',
expires=0,
secure=config.cookie_secure,
httponly=True,
domain=config.cookie_domain,
path=config.access_cookie_path,
samesite=config.cookie_samesite)
if config.csrf_protect and config.csrf_in_cookies:
response.set_cookie(config.refresh_csrf_cookie_name,
value='',
expires=0,
secure=config.cookie_secure,
httponly=False,
domain=config.cookie_domain,
path=config.refresh_csrf_cookie_path,
samesite=config.cookie_samesite)
response.set_cookie(config.access_csrf_cookie_name,
value='',
expires=0,
secure=config.cookie_secure,
httponly=False,
domain=config.cookie_domain,
path=config.access_csrf_cookie_path,
samesite=config.cookie_samesite)

View File

@ -0,0 +1,196 @@
from functools import wraps
from datetime import datetime
from calendar import timegm
from flask import request
try:
from flask import _app_ctx_stack as ctx_stack
except ImportError: # pragma: no cover
from flask import _request_ctx_stack as ctx_stack
from flask_jwt_extended.config import config
from flask_jwt_extended.exceptions import (
CSRFError, FreshTokenRequired, InvalidHeaderError, NoAuthorizationError,
UserLoadError
)
from flask_jwt_extended.utils import (
decode_token, has_user_loader, user_loader, verify_token_claims,
verify_token_not_blacklisted, verify_token_type
)
def jwt_required(fn):
"""
A decorator to protect a Flask endpoint.
If you decorate an endpoint with this, it will ensure that the requester
has a valid access token before allowing the endpoint to be called. This
does not check the freshness of the access token.
See also: :func:`~flask_jwt_extended.fresh_jwt_required`
"""
@wraps(fn)
def wrapper(*args, **kwargs):
if request.method not in config.exempt_methods:
jwt_data = _decode_jwt_from_request(request_type='access')
ctx_stack.top.jwt = jwt_data
verify_token_claims(jwt_data)
_load_user(jwt_data[config.identity_claim_key])
return fn(*args, **kwargs)
return wrapper
def jwt_optional(fn):
"""
A decorator to optionally protect a Flask endpoint
If an access token in present in the request, this will call the endpoint
with :func:`~flask_jwt_extended.get_jwt_identity` having the identity
of the access token. If no access token is present in the request,
this endpoint will still be called, but
:func:`~flask_jwt_extended.get_jwt_identity` will return `None` instead.
If there is an invalid access token in the request (expired, tampered with,
etc), this will still call the appropriate error handler instead of allowing
the endpoint to be called as if there is no access token in the request.
"""
@wraps(fn)
def wrapper(*args, **kwargs):
try:
jwt_data = _decode_jwt_from_request(request_type='access')
ctx_stack.top.jwt = jwt_data
verify_token_claims(jwt_data)
_load_user(jwt_data[config.identity_claim_key])
except (NoAuthorizationError, InvalidHeaderError):
pass
return fn(*args, **kwargs)
return wrapper
def fresh_jwt_required(fn):
"""
A decorator to protect a Flask endpoint.
If you decorate an endpoint with this, it will ensure that the requester
has a valid and fresh access token before allowing the endpoint to be
called.
See also: :func:`~flask_jwt_extended.jwt_required`
"""
@wraps(fn)
def wrapper(*args, **kwargs):
if request.method not in config.exempt_methods:
jwt_data = _decode_jwt_from_request(request_type='access')
ctx_stack.top.jwt = jwt_data
fresh = jwt_data['fresh']
if isinstance(fresh, bool):
if not fresh:
raise FreshTokenRequired('Fresh token required')
else:
now = timegm(datetime.utcnow().utctimetuple())
if fresh < now:
raise FreshTokenRequired('Fresh token required')
verify_token_claims(jwt_data)
_load_user(jwt_data[config.identity_claim_key])
return fn(*args, **kwargs)
return wrapper
def jwt_refresh_token_required(fn):
"""
A decorator to protect a Flask endpoint.
If you decorate an endpoint with this, it will ensure that the requester
has a valid refresh token before allowing the endpoint to be called.
"""
@wraps(fn)
def wrapper(*args, **kwargs):
if request.method not in config.exempt_methods:
jwt_data = _decode_jwt_from_request(request_type='refresh')
ctx_stack.top.jwt = jwt_data
_load_user(jwt_data[config.identity_claim_key])
return fn(*args, **kwargs)
return wrapper
def _load_user(identity):
if has_user_loader():
user = user_loader(identity)
if user is None:
raise UserLoadError("user_loader returned None for {}".format(identity))
else:
ctx_stack.top.jwt_user = user
def _decode_jwt_from_headers():
header_name = config.header_name
header_type = config.header_type
# Verify we have the auth header
jwt_header = request.headers.get(header_name, None)
if not jwt_header:
raise NoAuthorizationError("Missing {} Header".format(header_name))
# Make sure the header is in a valid format that we are expecting, ie
# <HeaderName>: <HeaderType(optional)> <JWT>
parts = jwt_header.split()
if not header_type:
if len(parts) != 1:
msg = "Bad {} header. Expected value '<JWT>'".format(header_name)
raise InvalidHeaderError(msg)
encoded_token = parts[0]
else:
if parts[0] != header_type or len(parts) != 2:
msg = "Bad {} header. Expected value '{} <JWT>'".format(header_name, header_type)
raise InvalidHeaderError(msg)
encoded_token = parts[1]
return decode_token(encoded_token)
def _decode_jwt_from_cookies(request_type):
if request_type == 'access':
cookie_key = config.access_cookie_name
csrf_header_key = config.access_csrf_header_name
else:
cookie_key = config.refresh_cookie_name
csrf_header_key = config.refresh_csrf_header_name
encoded_token = request.cookies.get(cookie_key)
if not encoded_token:
raise NoAuthorizationError('Missing cookie "{}"'.format(cookie_key))
if config.csrf_protect and request.method in config.csrf_request_methods:
csrf_value = request.headers.get(csrf_header_key, None)
if not csrf_value:
raise CSRFError("Missing CSRF token in headers")
else:
csrf_value = None
return decode_token(encoded_token, csrf_value=csrf_value)
def _decode_jwt_from_request(request_type):
# We have three cases here, having jwts in both cookies and headers is
# valid, or the jwt can only be saved in one of cookies or headers. Check
# all cases here.
if config.jwt_in_cookies and config.jwt_in_headers:
try:
decoded_token = _decode_jwt_from_cookies(request_type)
except NoAuthorizationError:
try:
decoded_token = _decode_jwt_from_headers()
except NoAuthorizationError:
raise NoAuthorizationError("Missing JWT in headers and cookies")
elif config.jwt_in_headers:
decoded_token = _decode_jwt_from_headers()
else:
decoded_token = _decode_jwt_from_cookies(request_type)
# Make sure the type of token we received matches the request type we expect
verify_token_type(decoded_token, expected_type=request_type)
# If blacklisting is enabled, see if this token has been revoked
verify_token_not_blacklisted(decoded_token, request_type)
return decoded_token