[API BREAK] Changed authentication routes
Authentication is now on /auth/... and using REST pathes and methods. AccessToken are now having a expires field instead of timestamp, more usefull for automatic removal of expired ones.
This commit is contained in:
parent
b6157f4953
commit
7fbff30214
|
@ -4,17 +4,15 @@
|
|||
# authentication, login, logout, etc #
|
||||
#############################################
|
||||
|
||||
from flask import Blueprint, current_app, request, jsonify
|
||||
from flask import Blueprint, request, jsonify
|
||||
from werkzeug.exceptions import Forbidden, BadRequest
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
from flaschengeist import logger
|
||||
from flaschengeist.system.decorator import login_required
|
||||
from flaschengeist.system.exceptions import PermissionDenied
|
||||
from flaschengeist.system.controller import mainController as mc
|
||||
import flaschengeist.system.controller.accessTokenController as ac
|
||||
|
||||
from flaschengeist.system.models.accessToken import AccessToken
|
||||
|
||||
logger = LocalProxy(lambda: current_app.logger)
|
||||
access_controller = LocalProxy(lambda: ac.AccessTokenController())
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
|
@ -23,13 +21,19 @@ auth_bp = Blueprint('auth', __name__)
|
|||
def register():
|
||||
return auth_bp
|
||||
|
||||
############################################
|
||||
# Routes #
|
||||
############################################
|
||||
#################################################
|
||||
# Routes #
|
||||
# #
|
||||
# /auth POST: login (new token) #
|
||||
# GET: get all tokens for user #
|
||||
# /auth/<token> GET: get lifetime of token #
|
||||
# PUT: set new lifetime #
|
||||
# DELETE: logout / delete token #
|
||||
#################################################
|
||||
|
||||
|
||||
@auth_bp.route("/login", methods=['POST'])
|
||||
def _login():
|
||||
@auth_bp.route("/auth", methods=['POST'])
|
||||
def _create_token():
|
||||
""" Login User
|
||||
|
||||
Login in User and create an AccessToken for the User.
|
||||
|
@ -38,95 +42,70 @@ def _login():
|
|||
"""
|
||||
logger.debug("Start log in.")
|
||||
data = request.get_json()
|
||||
logger.info(request)
|
||||
username = data['username']
|
||||
password = data['password']
|
||||
logger.debug("username is {{ {} }}".format(username))
|
||||
try:
|
||||
logger.debug("search {{ {} }} in database".format(username))
|
||||
main_controller = mc.MainController()
|
||||
user = main_controller.login_user(username, password)
|
||||
logger.debug("user is {{ {} }}".format(user))
|
||||
token = access_controller.create(user, user_agent=request.user_agent)
|
||||
logger.debug("access token is {{ {} }}".format(token))
|
||||
logger.debug("validate access token")
|
||||
dic = user.default()
|
||||
dic["accessToken"] = token.token
|
||||
logger.info("User {{ {} }} success login.".format(username))
|
||||
logger.debug("return login {{ {} }}".format(dic))
|
||||
return jsonify(dic)
|
||||
except PermissionDenied as err:
|
||||
logger.debug("permission denied exception in login", exc_info=True)
|
||||
return jsonify({"error": str(err)}), 401
|
||||
except Exception as err:
|
||||
logger.error("exception in login.", exc_info=True)
|
||||
return jsonify({"error": "permission denied"}), 401
|
||||
|
||||
logger.debug("search user {{ {} }} in database".format(username))
|
||||
main_controller = mc.MainController()
|
||||
user = main_controller.login_user(username, password)
|
||||
logger.debug("user is {{ {} }}".format(user))
|
||||
token = access_controller.create(user, user_agent=request.user_agent)
|
||||
logger.debug("access token is {{ {} }}".format(token))
|
||||
dic = user.serialize()
|
||||
dic["access_token"] = token.token
|
||||
logger.info("User {{ {} }} success login.".format(username))
|
||||
|
||||
# Lets cleanup the DB
|
||||
access_controller.clear_expired()
|
||||
return jsonify(dic)
|
||||
|
||||
|
||||
@auth_bp.route("/logout", methods=['GET'])
|
||||
@auth_bp.route("/auth", methods=['GET'])
|
||||
@login_required()
|
||||
def _logout(**kwargs):
|
||||
def _get_tokens(access_token, **kwargs):
|
||||
tokens = access_controller.get_users_tokens(access_token.user)
|
||||
return jsonify(tokens)
|
||||
|
||||
|
||||
@auth_bp.route("/auth/<token>", methods=['DELETE'])
|
||||
@login_required()
|
||||
def _delete_token(token, access_token, **kwargs):
|
||||
logger.debug("Try to delete access token {{ {} }}".format(token))
|
||||
token = access_controller.get_token(token, access_token.user)
|
||||
if not token:
|
||||
logger.debug("Token not found in database!")
|
||||
# Return 403 error, so that users can not bruteforce tokens
|
||||
# Valid tokens from other users and invalid tokens now are looking the same
|
||||
raise Forbidden
|
||||
access_controller.delete_token(token)
|
||||
access_controller.clear_expired()
|
||||
return jsonify({"ok": "ok"})
|
||||
|
||||
|
||||
@auth_bp.route("/auth/<token>", methods=['GET'])
|
||||
@login_required()
|
||||
def _get_token(token, access_token, **kwargs):
|
||||
logger.debug("get token {{ {} }}".format(token))
|
||||
token = access_controller.get_token(token, access_token.user)
|
||||
if not token:
|
||||
# Return 403 error, so that users can not bruteforce tokens
|
||||
# Valid tokens from other users and invalid tokens now are looking the same
|
||||
raise Forbidden
|
||||
return jsonify(token)
|
||||
|
||||
|
||||
@auth_bp.route("/auth/<token>", methods=['PUT'])
|
||||
@login_required()
|
||||
def _set_lifetime(token, access_token, **kwargs):
|
||||
token = access_controller.get_token(token, access_token.user)
|
||||
if not token:
|
||||
# Return 403 error, so that users can not bruteforce tokens
|
||||
# Valid tokens from other users and invalid tokens now are looking the same
|
||||
raise Forbidden
|
||||
try:
|
||||
logger.debug("logout user")
|
||||
token = kwargs['accToken']
|
||||
logger.debug("access token is {{ {} }}".format(token))
|
||||
logger.debug("delete access token")
|
||||
access_controller.deleteAccessToken(token)
|
||||
access_controller.clearExpired()
|
||||
logger.info("return ok logout user")
|
||||
lifetime = request.get_json()['value']
|
||||
logger.debug("set lifetime {{ {} }} to access token {{ {} }}".format(lifetime, token))
|
||||
access_controller.set_lifetime(token, lifetime)
|
||||
return jsonify({"ok": "ok"})
|
||||
except Exception as err:
|
||||
logger.warning("exception in logout user.", exc_info=True)
|
||||
return jsonify({"error": str(err)}), 500
|
||||
|
||||
|
||||
@auth_bp.route("/user/getAccessTokens", methods=['GET', 'POST'])
|
||||
# @auth_bp.route("/accessTokens", methods=['GET', 'POST'])
|
||||
@login_required()
|
||||
def _getAccessTokens(**kwargs):
|
||||
try:
|
||||
if request.method == 'POST':
|
||||
data = request.get_json()
|
||||
token = AccessToken(data['id'], kwargs['accToken'].user, None, None, None)
|
||||
access_controller.delete_token(token)
|
||||
tokens = access_controller.getAccessTokensFromUser(kwargs['accToken'].user)
|
||||
r = [t.toJSON() for t in tokens]
|
||||
logger.debug("return {{ {} }}".format(r))
|
||||
return jsonify(r)
|
||||
except Exception as err:
|
||||
logger.debug("exception", exc_info=True)
|
||||
return jsonify({"error": str(err)}), 500
|
||||
|
||||
|
||||
@auth_bp.route("/getLifetime", methods=['GET'])
|
||||
@login_required()
|
||||
def _getLifeTime(**kwargs):
|
||||
try:
|
||||
logger.debug("get lifetime of access token")
|
||||
token = kwargs['accToken']
|
||||
logger.debug("accessToken is {{ {} }}".format(token))
|
||||
return jsonify({"value": token.lifetime})
|
||||
except Exception as err:
|
||||
logger.warning("exception in get lifetime of access token.", exc_info=True)
|
||||
return jsonify({"error": str(err)}), 500
|
||||
|
||||
|
||||
@auth_bp.route("/setLifetime", methods=['POST'])
|
||||
@login_required()
|
||||
def _saveLifeTime(**kwargs):
|
||||
try:
|
||||
token = kwargs['accToken']
|
||||
logger.debug("save lifetime for access token {{ {} }}".format(token))
|
||||
data = request.get_json()
|
||||
lifetime = data['value']
|
||||
logger.debug("lifetime is {{ {} }}".format(lifetime))
|
||||
logger.info("set lifetime {{ {} }} to access token {{ {} }}".format(
|
||||
lifetime, token))
|
||||
token.lifetime = lifetime
|
||||
logger.info("update access token timestamp")
|
||||
token = access_controller.update(token)
|
||||
return jsonify({"value": token.lifetime})
|
||||
except Exception as err:
|
||||
logger.warning(
|
||||
"exception in save lifetime for access token.", exc_info=True)
|
||||
return jsonify({"error": str(err)}), 500
|
||||
except (KeyError, TypeError):
|
||||
raise BadRequest
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import secrets
|
||||
from ..models.accessToken import AccessToken
|
||||
from flaschengeist.system.database import db
|
||||
|
||||
from flaschengeist import logger
|
||||
from werkzeug.exceptions import Forbidden
|
||||
from datetime import datetime, timedelta
|
||||
import secrets
|
||||
from . import Singleton
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("flaschenpost")
|
||||
|
||||
|
||||
class AccessTokenController(metaclass=Singleton):
|
||||
|
@ -17,99 +15,111 @@ class AccessTokenController(metaclass=Singleton):
|
|||
Attributes:
|
||||
lifetime: Variable for the Lifetime of one AccessToken in seconds.
|
||||
"""
|
||||
instance = None
|
||||
tokenList = None
|
||||
|
||||
def __init__(self, lifetime=1800):
|
||||
""" Initialize AccessTokenController
|
||||
|
||||
Initialize Thread and set tokenList empty.
|
||||
"""
|
||||
logger.debug("init access token controller")
|
||||
self.lifetime = lifetime
|
||||
|
||||
def validate_token(self, token, roles):
|
||||
def validate_token(self, token, user_agent, roles):
|
||||
""" Verify access token
|
||||
|
||||
Verify an AccessToken and Group so if the User has permission or not.
|
||||
Verify an AccessToken and Roles so if the User has permission or not.
|
||||
Retrieves the access token if valid else retrieves False
|
||||
|
||||
Args:
|
||||
token: Token to verify.
|
||||
user_agent: User agent of browser to check
|
||||
roles: Roles needed to access restricted routes
|
||||
Returns:
|
||||
An the AccessToken for this given Token or False.
|
||||
"""
|
||||
logger.debug("check token {{ {} }} is valid".format(token))
|
||||
for access_token in AccessToken.query.filter_by(token=token):
|
||||
time_end = access_token.timestamp + timedelta(seconds=access_token.lifetime)
|
||||
now = datetime.utcnow()
|
||||
logger.debug("now is {{ {} }}, endtime is {{ {} }}".format(now, time_end))
|
||||
if now <= time_end:
|
||||
logger.debug("check if token {{ {} }} is same as {{ {} }}".format(token, access_token))
|
||||
if not roles or (roles and self.userHasRole(access_token.user, roles)):
|
||||
access_token.updateTimestamp()
|
||||
access_token = AccessToken.query.filter_by(token=token).one_or_none()
|
||||
if access_token:
|
||||
logger.debug("token found, check if expired or invalid user agent differs")
|
||||
if access_token.expires >= datetime.utcnow() and (
|
||||
access_token.browser == user_agent.browser and
|
||||
access_token.platform == user_agent.platform):
|
||||
if not roles or (roles and self.user_has_role(access_token.user, roles)):
|
||||
access_token.refresh()
|
||||
db.session.commit()
|
||||
return access_token
|
||||
else:
|
||||
logger.debug("access token is {{ {} }} out of date".format(access_token))
|
||||
db.session.delete(access_token)
|
||||
db.session.commit()
|
||||
logger.debug("no valid access token with token: {{ {} }} and group: {{ {} }}".format(token, roles))
|
||||
return False
|
||||
|
||||
def userHasRole(self, user, roles):
|
||||
for group in user.groups:
|
||||
for role in group.roles:
|
||||
if role.name in roles:
|
||||
return True
|
||||
logger.debug("access token is out of date or invalid client used")
|
||||
self.delete_token(access_token)
|
||||
logger.debug("no valid access token with token: {{ {} }} and roles: {{ {} }}".format(token, roles))
|
||||
return False
|
||||
|
||||
def create(self, user, user_agent=None) -> AccessToken:
|
||||
""" Create an AccessToken
|
||||
|
||||
Create an AccessToken for an User and add it to the tokenList.
|
||||
Args:
|
||||
user: For which User is to create an AccessToken
|
||||
user_agent: User agent to identify session
|
||||
|
||||
Args:
|
||||
user: For which User is to create an AccessToken
|
||||
user_agent: User agent to identify session
|
||||
|
||||
Returns:
|
||||
Returns:
|
||||
AccessToken: A created Token for User
|
||||
"""
|
||||
logger.debug("create access token")
|
||||
token_str = secrets.token_hex(16)
|
||||
token = AccessToken(token=token_str, user=user, lifetime=self.lifetime,
|
||||
browser=user_agent.browser, platform=user_agent.platform)
|
||||
token.refresh()
|
||||
db.session.add(token)
|
||||
db.session.commit()
|
||||
|
||||
logger.debug("access token is {{ {} }}".format(token))
|
||||
return token
|
||||
|
||||
def getAccessTokensFromUser(self, user):
|
||||
def get_token(self, token, owner=None):
|
||||
"""Retrieves AccessToken from token string
|
||||
|
||||
Args:
|
||||
token (str): Token string
|
||||
owner (User, optional): User owning the token
|
||||
|
||||
Raises:
|
||||
Forbidden: Raised if owner is set but does not match
|
||||
Returns:
|
||||
AccessToken: Token object identified by given token string
|
||||
"""
|
||||
access_token = AccessToken.query.filter(AccessToken.token == token).one_or_none()
|
||||
if access_token and (owner and owner != access_token.user):
|
||||
raise Forbidden
|
||||
return access_token
|
||||
|
||||
def get_users_tokens(self, user):
|
||||
return AccessToken.query.filter(AccessToken.user == user)
|
||||
|
||||
@staticmethod
|
||||
def delete_token(token):
|
||||
if token is isinstance(token, AccessToken):
|
||||
db.session.delete(token)
|
||||
else:
|
||||
AccessToken.query.filter_by(token=token).delete()
|
||||
def delete_token(token: AccessToken):
|
||||
"""Deletes given AccessToken
|
||||
|
||||
Args:
|
||||
token (AccessToken): Token to delete
|
||||
"""
|
||||
db.session.delete(token)
|
||||
db.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def update_token(token):
|
||||
token.update_timestamp()
|
||||
token.refresh()
|
||||
db.session.commit()
|
||||
|
||||
def set_lifetime(self, token, lifetime):
|
||||
token.lifetime = lifetime
|
||||
self.update_token(token)
|
||||
|
||||
def clear_expired(self):
|
||||
"""Remove expired tokens from database"""
|
||||
logger.debug("Clear expired AccessToken")
|
||||
might_expired = datetime.utcnow() - timedelta(seconds=self.lifetime)
|
||||
tokens = AccessToken.query.filter(AccessToken.timestamp < might_expired)
|
||||
logger.debug(tokens)
|
||||
for token in tokens:
|
||||
if token.timestamp < datetime.utcnow() - timedelta(seconds=token.lifetime):
|
||||
logger.debug("Delete token %s", token.token)
|
||||
db.session.delete(token)
|
||||
deleted = AccessToken.query.filter(AccessToken.expires < datetime.utcnow()).delete()
|
||||
logger.debug("{} tokens have been removed".format(deleted))
|
||||
db.session.commit()
|
||||
|
||||
# TODO: is this needed?
|
||||
def user_has_role(self, user, roles):
|
||||
for group in user.groups:
|
||||
for role in group.roles:
|
||||
if role.name in roles:
|
||||
return True
|
||||
return False
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
class PermissionDenied(Exception):
|
||||
pass
|
||||
def __init__(self, message=None):
|
||||
if not message:
|
||||
message = "PermissionDenied"
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class UsernameExistDB(Exception):
|
||||
|
|
|
@ -1,69 +1,53 @@
|
|||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from ..database import db
|
||||
from flask import current_app
|
||||
from werkzeug.local import LocalProxy
|
||||
from secrets import compare_digest
|
||||
|
||||
logger = LocalProxy(lambda: current_app.logger)
|
||||
from flaschengeist import logger
|
||||
|
||||
|
||||
class AccessToken(db.Model):
|
||||
""" Model for an AccessToken
|
||||
|
||||
Attributes:
|
||||
timestamp: Is a Datetime from current Time.
|
||||
Args:
|
||||
expires: Is a Datetime from current Time.
|
||||
user: Is an User.
|
||||
token: String to verify access later.
|
||||
"""
|
||||
__tablename__ = 'session'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||
user = db.relationship("User", back_populates="sessions")
|
||||
token = db.Column(db.String(30))
|
||||
|
||||
expires = db.Column(db.DateTime)
|
||||
token = db.Column(db.String(30), unique=True)
|
||||
lifetime = db.Column(db.Integer)
|
||||
browser = db.Column(db.String(30))
|
||||
platform = db.Column(db.String(30))
|
||||
|
||||
def update_timestamp(self):
|
||||
def refresh(self):
|
||||
""" Update the Timestamp
|
||||
|
||||
Update the Timestamp to the current Time.
|
||||
"""
|
||||
logger.debug("update timestamp from access token {{ {} }}".format(self))
|
||||
self.timestamp = datetime.utcnow()
|
||||
self.expires = datetime.utcnow() + timedelta(seconds=self.lifetime)
|
||||
|
||||
def default(self):
|
||||
def serialize(self):
|
||||
""" Create Dic to dump in JSON
|
||||
|
||||
Returns:
|
||||
A Dic with static Attributes.
|
||||
"""
|
||||
dic = {
|
||||
"id": self.id,
|
||||
"timestamp": {'year': self.timestamp.year,
|
||||
'month': self.timestamp.month,
|
||||
'day': self.timestamp.day,
|
||||
'hour': self.timestamp.hour,
|
||||
'minute': self.timestamp.minute,
|
||||
'second': self.timestamp.second
|
||||
},
|
||||
return {
|
||||
"token": self.token,
|
||||
"expires": self.expires,
|
||||
"lifetime": self.lifetime,
|
||||
"browser": self.browser,
|
||||
"platform": self.platform
|
||||
}
|
||||
return dic
|
||||
|
||||
def __eq__(self, token):
|
||||
return compare_digest(self.token, token)
|
||||
|
||||
def __sub__(self, other):
|
||||
return other - self.timestamp
|
||||
|
||||
def __str__(self):
|
||||
return "AccessToken(user={}, token={}, timestamp={}, lifetime={}".format(
|
||||
self.user, self.token, self.timestamp, self.lifetime)
|
||||
|
||||
def __repr__(self):
|
||||
return "AccessToken(user={}, token={}, timestamp={}, lifetime={}".format(
|
||||
self.user, self.token, self.timestamp, self.lifetime)
|
||||
return "AccessToken(user={}, token={}, expires={}, lifetime={})".format(
|
||||
self.user, self.token, self.expires, self.lifetime)
|
||||
|
|
Loading…
Reference in New Issue