lemur/lemur/auth/service.py

203 lines
6.1 KiB
Python
Raw Normal View History

2015-06-22 22:47:27 +02:00
"""
.. module: lemur.auth.service
:platform: Unix
:synopsis: This module contains all of the authentication duties for
lemur
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
2015-06-22 22:47:27 +02:00
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import jwt
import json
import binascii
2015-08-04 06:07:28 +02:00
2015-06-22 22:47:27 +02:00
from functools import wraps
from datetime import datetime, timedelta
from flask import g, current_app, jsonify, request
2016-11-23 06:11:20 +01:00
from flask_restful import Resource
from flask_principal import identity_loaded, RoleNeed, UserNeed
2015-06-22 22:47:27 +02:00
2016-11-23 06:11:20 +01:00
from flask_principal import Identity, identity_changed
2015-06-22 22:47:27 +02:00
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
from lemur.users import service as user_service
from lemur.api_keys import service as api_key_service
from lemur.auth.permissions import AuthorityCreatorNeed, RoleMemberNeed
2015-06-22 22:47:27 +02:00
def get_rsa_public_key(n, e):
"""
Retrieve an RSA public key based on a module and exponent as provided by the JWKS format.
:param n:
:param e:
:return: a RSA Public Key in PEM format
"""
2019-05-16 16:57:02 +02:00
n = int(binascii.hexlify(jwt.utils.base64url_decode(bytes(n, "utf-8"))), 16)
e = int(binascii.hexlify(jwt.utils.base64url_decode(bytes(e, "utf-8"))), 16)
2015-06-22 22:47:27 +02:00
pub = RSAPublicNumbers(e, n).public_key(default_backend())
return pub.public_bytes(
encoding=serialization.Encoding.PEM,
2019-05-16 16:57:02 +02:00
format=serialization.PublicFormat.SubjectPublicKeyInfo,
2015-06-22 22:47:27 +02:00
)
def create_token(user, aid=None, ttl=None):
2015-06-22 22:47:27 +02:00
"""
Create a valid JWT for a given user/api key, this token is then used to authenticate
2015-06-22 22:47:27 +02:00
sessions until the token expires.
:param user:
:return:
"""
2019-05-16 16:57:02 +02:00
expiration_delta = timedelta(
days=int(current_app.config.get("LEMUR_TOKEN_EXPIRATION", 1))
)
payload = {"iat": datetime.utcnow(), "exp": datetime.utcnow() + expiration_delta}
# Handle Just a User ID & User Object.
if isinstance(user, int):
2019-05-16 16:57:02 +02:00
payload["sub"] = user
else:
2019-05-16 16:57:02 +02:00
payload["sub"] = user.id
if aid is not None:
2019-05-16 16:57:02 +02:00
payload["aid"] = aid
# Custom TTLs are only supported on Access Keys.
if ttl is not None and aid is not None:
# Tokens that are forever until revoked.
if ttl == -1:
2019-05-16 16:57:02 +02:00
del payload["exp"]
else:
2019-05-16 16:57:02 +02:00
payload["exp"] = ttl
token = jwt.encode(payload, current_app.config["LEMUR_TOKEN_SECRET"])
return token.decode("unicode_escape")
2015-06-22 22:47:27 +02:00
def login_required(f):
"""
Validates the JWT and ensures that is has not expired and the user is still active.
2015-06-22 22:47:27 +02:00
:param f:
:return:
"""
2019-05-16 16:57:02 +02:00
2015-06-22 22:47:27 +02:00
@wraps(f)
def decorated_function(*args, **kwargs):
2019-05-16 16:57:02 +02:00
if not request.headers.get("Authorization"):
response = jsonify(message="Missing authorization header")
2015-06-22 22:47:27 +02:00
response.status_code = 401
return response
try:
2019-05-16 16:57:02 +02:00
token = request.headers.get("Authorization").split()[1]
2015-06-26 03:08:04 +02:00
except Exception as e:
2019-05-16 16:57:02 +02:00
return dict(message="Token is invalid"), 403
2015-06-26 03:08:04 +02:00
try:
2019-05-16 16:57:02 +02:00
payload = jwt.decode(token, current_app.config["LEMUR_TOKEN_SECRET"])
2015-06-22 22:47:27 +02:00
except jwt.DecodeError:
2019-05-16 16:57:02 +02:00
return dict(message="Token is invalid"), 403
2015-06-22 22:47:27 +02:00
except jwt.ExpiredSignatureError:
2019-05-16 16:57:02 +02:00
return dict(message="Token has expired"), 403
2015-06-22 22:47:27 +02:00
except jwt.InvalidTokenError:
2019-05-16 16:57:02 +02:00
return dict(message="Token is invalid"), 403
2015-06-22 22:47:27 +02:00
2019-05-16 16:57:02 +02:00
if "aid" in payload:
access_key = api_key_service.get(payload["aid"])
if access_key.revoked:
2019-05-16 16:57:02 +02:00
return dict(message="Token has been revoked"), 403
if access_key.ttl != -1:
current_time = datetime.utcnow()
2019-05-16 16:57:02 +02:00
expired_time = datetime.fromtimestamp(
access_key.issued_at + access_key.ttl
)
if current_time >= expired_time:
2019-05-16 16:57:02 +02:00
return dict(message="Token has expired"), 403
2019-05-16 16:57:02 +02:00
user = user_service.get(payload["sub"])
if not user.active:
2019-05-16 16:57:02 +02:00
return dict(message="User is not currently active"), 403
g.current_user = user
2015-06-22 22:47:27 +02:00
2015-06-26 03:08:04 +02:00
if not g.current_user:
2019-05-16 16:57:02 +02:00
return dict(message="You are not logged in"), 403
2015-06-22 22:47:27 +02:00
# Tell Flask-Principal the identity changed
2019-05-16 16:57:02 +02:00
identity_changed.send(
current_app._get_current_object(), identity=Identity(g.current_user.id)
)
2015-06-22 22:47:27 +02:00
return f(*args, **kwargs)
return decorated_function
def fetch_token_header(token):
"""
Fetch the header out of the JWT token.
:param token:
:return: :raise jwt.DecodeError:
"""
2019-05-16 16:57:02 +02:00
token = token.encode("utf-8")
2015-06-22 22:47:27 +02:00
try:
2019-05-16 16:57:02 +02:00
signing_input, crypto_segment = token.rsplit(b".", 1)
header_segment, payload_segment = signing_input.split(b".", 1)
2015-06-22 22:47:27 +02:00
except ValueError:
2019-05-16 16:57:02 +02:00
raise jwt.DecodeError("Not enough segments")
2015-06-22 22:47:27 +02:00
try:
2019-05-16 16:57:02 +02:00
return json.loads(jwt.utils.base64url_decode(header_segment).decode("utf-8"))
except TypeError as e:
current_app.logger.exception(e)
2019-05-16 16:57:02 +02:00
raise jwt.DecodeError("Invalid header padding")
2015-06-22 22:47:27 +02:00
@identity_loaded.connect
def on_identity_loaded(sender, identity):
"""
Sets the identity of a given option, assigns additional permissions based on
the role that the user is a part of.
:param sender:
:param identity:
"""
# load the user
user = user_service.get(identity.id)
# add the UserNeed to the identity
identity.provides.add(UserNeed(identity.id))
# identity with the roles that the user provides
2019-05-16 16:57:02 +02:00
if hasattr(user, "roles"):
2015-06-22 22:47:27 +02:00
for role in user.roles:
identity.provides.add(RoleNeed(role.name))
identity.provides.add(RoleMemberNeed(role.id))
2015-06-22 22:47:27 +02:00
# apply ownership for authorities
2019-05-16 16:57:02 +02:00
if hasattr(user, "authorities"):
2015-06-22 22:47:27 +02:00
for authority in user.authorities:
2015-08-04 06:07:28 +02:00
identity.provides.add(AuthorityCreatorNeed(authority.id))
2015-06-22 22:47:27 +02:00
g.user = user
class AuthenticatedResource(Resource):
"""
Inherited by all resources that need to be protected by authentication.
"""
2019-05-16 16:57:02 +02:00
2015-06-22 22:47:27 +02:00
method_decorators = [login_required]
def __init__(self):
super(AuthenticatedResource, self).__init__()