[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 #
|
# 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 werkzeug.local import LocalProxy
|
||||||
|
|
||||||
|
from flaschengeist import logger
|
||||||
from flaschengeist.system.decorator import login_required
|
from flaschengeist.system.decorator import login_required
|
||||||
from flaschengeist.system.exceptions import PermissionDenied
|
|
||||||
from flaschengeist.system.controller import mainController as mc
|
from flaschengeist.system.controller import mainController as mc
|
||||||
import flaschengeist.system.controller.accessTokenController as ac
|
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())
|
access_controller = LocalProxy(lambda: ac.AccessTokenController())
|
||||||
|
|
||||||
auth_bp = Blueprint('auth', __name__)
|
auth_bp = Blueprint('auth', __name__)
|
||||||
|
@ -23,13 +21,19 @@ auth_bp = Blueprint('auth', __name__)
|
||||||
def register():
|
def register():
|
||||||
return auth_bp
|
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'])
|
@auth_bp.route("/auth", methods=['POST'])
|
||||||
def _login():
|
def _create_token():
|
||||||
""" Login User
|
""" Login User
|
||||||
|
|
||||||
Login in User and create an AccessToken for the User.
|
Login in User and create an AccessToken for the User.
|
||||||
|
@ -38,95 +42,70 @@ def _login():
|
||||||
"""
|
"""
|
||||||
logger.debug("Start log in.")
|
logger.debug("Start log in.")
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
logger.info(request)
|
|
||||||
username = data['username']
|
username = data['username']
|
||||||
password = data['password']
|
password = data['password']
|
||||||
logger.debug("username is {{ {} }}".format(username))
|
|
||||||
try:
|
logger.debug("search user {{ {} }} in database".format(username))
|
||||||
logger.debug("search {{ {} }} in database".format(username))
|
main_controller = mc.MainController()
|
||||||
main_controller = mc.MainController()
|
user = main_controller.login_user(username, password)
|
||||||
user = main_controller.login_user(username, password)
|
logger.debug("user is {{ {} }}".format(user))
|
||||||
logger.debug("user is {{ {} }}".format(user))
|
token = access_controller.create(user, user_agent=request.user_agent)
|
||||||
token = access_controller.create(user, user_agent=request.user_agent)
|
logger.debug("access token is {{ {} }}".format(token))
|
||||||
logger.debug("access token is {{ {} }}".format(token))
|
dic = user.serialize()
|
||||||
logger.debug("validate access token")
|
dic["access_token"] = token.token
|
||||||
dic = user.default()
|
logger.info("User {{ {} }} success login.".format(username))
|
||||||
dic["accessToken"] = token.token
|
|
||||||
logger.info("User {{ {} }} success login.".format(username))
|
# Lets cleanup the DB
|
||||||
logger.debug("return login {{ {} }}".format(dic))
|
access_controller.clear_expired()
|
||||||
return jsonify(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
|
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route("/logout", methods=['GET'])
|
@auth_bp.route("/auth", methods=['GET'])
|
||||||
@login_required()
|
@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:
|
try:
|
||||||
logger.debug("logout user")
|
lifetime = request.get_json()['value']
|
||||||
token = kwargs['accToken']
|
logger.debug("set lifetime {{ {} }} to access token {{ {} }}".format(lifetime, token))
|
||||||
logger.debug("access token is {{ {} }}".format(token))
|
access_controller.set_lifetime(token, lifetime)
|
||||||
logger.debug("delete access token")
|
|
||||||
access_controller.deleteAccessToken(token)
|
|
||||||
access_controller.clearExpired()
|
|
||||||
logger.info("return ok logout user")
|
|
||||||
return jsonify({"ok": "ok"})
|
return jsonify({"ok": "ok"})
|
||||||
except Exception as err:
|
except (KeyError, TypeError):
|
||||||
logger.warning("exception in logout user.", exc_info=True)
|
raise BadRequest
|
||||||
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
|
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
|
import secrets
|
||||||
from ..models.accessToken import AccessToken
|
from ..models.accessToken import AccessToken
|
||||||
from flaschengeist.system.database import db
|
from flaschengeist.system.database import db
|
||||||
|
from flaschengeist import logger
|
||||||
|
from werkzeug.exceptions import Forbidden
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import secrets
|
|
||||||
from . import Singleton
|
from . import Singleton
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger("flaschenpost")
|
|
||||||
|
|
||||||
|
|
||||||
class AccessTokenController(metaclass=Singleton):
|
class AccessTokenController(metaclass=Singleton):
|
||||||
|
@ -17,99 +15,111 @@ class AccessTokenController(metaclass=Singleton):
|
||||||
Attributes:
|
Attributes:
|
||||||
lifetime: Variable for the Lifetime of one AccessToken in seconds.
|
lifetime: Variable for the Lifetime of one AccessToken in seconds.
|
||||||
"""
|
"""
|
||||||
instance = None
|
|
||||||
tokenList = None
|
|
||||||
|
|
||||||
def __init__(self, lifetime=1800):
|
def __init__(self, lifetime=1800):
|
||||||
""" Initialize AccessTokenController
|
|
||||||
|
|
||||||
Initialize Thread and set tokenList empty.
|
|
||||||
"""
|
|
||||||
logger.debug("init access token controller")
|
|
||||||
self.lifetime = lifetime
|
self.lifetime = lifetime
|
||||||
|
|
||||||
def validate_token(self, token, roles):
|
def validate_token(self, token, user_agent, roles):
|
||||||
""" Verify access token
|
""" 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
|
Retrieves the access token if valid else retrieves False
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
token: Token to verify.
|
token: Token to verify.
|
||||||
|
user_agent: User agent of browser to check
|
||||||
roles: Roles needed to access restricted routes
|
roles: Roles needed to access restricted routes
|
||||||
Returns:
|
Returns:
|
||||||
An the AccessToken for this given Token or False.
|
An the AccessToken for this given Token or False.
|
||||||
"""
|
"""
|
||||||
logger.debug("check token {{ {} }} is valid".format(token))
|
logger.debug("check token {{ {} }} is valid".format(token))
|
||||||
for access_token in AccessToken.query.filter_by(token=token):
|
access_token = AccessToken.query.filter_by(token=token).one_or_none()
|
||||||
time_end = access_token.timestamp + timedelta(seconds=access_token.lifetime)
|
if access_token:
|
||||||
now = datetime.utcnow()
|
logger.debug("token found, check if expired or invalid user agent differs")
|
||||||
logger.debug("now is {{ {} }}, endtime is {{ {} }}".format(now, time_end))
|
if access_token.expires >= datetime.utcnow() and (
|
||||||
if now <= time_end:
|
access_token.browser == user_agent.browser and
|
||||||
logger.debug("check if token {{ {} }} is same as {{ {} }}".format(token, access_token))
|
access_token.platform == user_agent.platform):
|
||||||
if not roles or (roles and self.userHasRole(access_token.user, roles)):
|
if not roles or (roles and self.user_has_role(access_token.user, roles)):
|
||||||
access_token.updateTimestamp()
|
access_token.refresh()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return access_token
|
return access_token
|
||||||
else:
|
else:
|
||||||
logger.debug("access token is {{ {} }} out of date".format(access_token))
|
logger.debug("access token is out of date or invalid client used")
|
||||||
db.session.delete(access_token)
|
self.delete_token(access_token)
|
||||||
db.session.commit()
|
logger.debug("no valid access token with token: {{ {} }} and roles: {{ {} }}".format(token, roles))
|
||||||
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
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def create(self, user, user_agent=None) -> AccessToken:
|
def create(self, user, user_agent=None) -> AccessToken:
|
||||||
""" Create an 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:
|
Returns:
|
||||||
user: For which User is to create an AccessToken
|
|
||||||
user_agent: User agent to identify session
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
AccessToken: A created Token for User
|
AccessToken: A created Token for User
|
||||||
"""
|
"""
|
||||||
logger.debug("create access token")
|
logger.debug("create access token")
|
||||||
token_str = secrets.token_hex(16)
|
token_str = secrets.token_hex(16)
|
||||||
token = AccessToken(token=token_str, user=user, lifetime=self.lifetime,
|
token = AccessToken(token=token_str, user=user, lifetime=self.lifetime,
|
||||||
browser=user_agent.browser, platform=user_agent.platform)
|
browser=user_agent.browser, platform=user_agent.platform)
|
||||||
|
token.refresh()
|
||||||
db.session.add(token)
|
db.session.add(token)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
logger.debug("access token is {{ {} }}".format(token))
|
logger.debug("access token is {{ {} }}".format(token))
|
||||||
return 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)
|
return AccessToken.query.filter(AccessToken.user == user)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_token(token):
|
def delete_token(token: AccessToken):
|
||||||
if token is isinstance(token, AccessToken):
|
"""Deletes given AccessToken
|
||||||
db.session.delete(token)
|
|
||||||
else:
|
Args:
|
||||||
AccessToken.query.filter_by(token=token).delete()
|
token (AccessToken): Token to delete
|
||||||
|
"""
|
||||||
|
db.session.delete(token)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_token(token):
|
def update_token(token):
|
||||||
token.update_timestamp()
|
token.refresh()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
def set_lifetime(self, token, lifetime):
|
||||||
|
token.lifetime = lifetime
|
||||||
|
self.update_token(token)
|
||||||
|
|
||||||
def clear_expired(self):
|
def clear_expired(self):
|
||||||
|
"""Remove expired tokens from database"""
|
||||||
logger.debug("Clear expired AccessToken")
|
logger.debug("Clear expired AccessToken")
|
||||||
might_expired = datetime.utcnow() - timedelta(seconds=self.lifetime)
|
deleted = AccessToken.query.filter(AccessToken.expires < datetime.utcnow()).delete()
|
||||||
tokens = AccessToken.query.filter(AccessToken.timestamp < might_expired)
|
logger.debug("{} tokens have been removed".format(deleted))
|
||||||
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)
|
|
||||||
db.session.commit()
|
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):
|
class PermissionDenied(Exception):
|
||||||
pass
|
def __init__(self, message=None):
|
||||||
|
if not message:
|
||||||
|
message = "PermissionDenied"
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
|
||||||
class UsernameExistDB(Exception):
|
class UsernameExistDB(Exception):
|
||||||
|
|
|
@ -1,69 +1,53 @@
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from ..database import db
|
from ..database import db
|
||||||
from flask import current_app
|
|
||||||
from werkzeug.local import LocalProxy
|
|
||||||
from secrets import compare_digest
|
from secrets import compare_digest
|
||||||
|
from flaschengeist import logger
|
||||||
logger = LocalProxy(lambda: current_app.logger)
|
|
||||||
|
|
||||||
|
|
||||||
class AccessToken(db.Model):
|
class AccessToken(db.Model):
|
||||||
""" Model for an AccessToken
|
""" Model for an AccessToken
|
||||||
|
|
||||||
Attributes:
|
Args:
|
||||||
timestamp: Is a Datetime from current Time.
|
expires: Is a Datetime from current Time.
|
||||||
user: Is an User.
|
user: Is an User.
|
||||||
token: String to verify access later.
|
token: String to verify access later.
|
||||||
"""
|
"""
|
||||||
__tablename__ = 'session'
|
__tablename__ = 'session'
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
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_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
user = db.relationship("User", back_populates="sessions")
|
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)
|
lifetime = db.Column(db.Integer)
|
||||||
browser = db.Column(db.String(30))
|
browser = db.Column(db.String(30))
|
||||||
platform = db.Column(db.String(30))
|
platform = db.Column(db.String(30))
|
||||||
|
|
||||||
def update_timestamp(self):
|
def refresh(self):
|
||||||
""" Update the Timestamp
|
""" Update the Timestamp
|
||||||
|
|
||||||
Update the Timestamp to the current Time.
|
Update the Timestamp to the current Time.
|
||||||
"""
|
"""
|
||||||
logger.debug("update timestamp from access token {{ {} }}".format(self))
|
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
|
""" Create Dic to dump in JSON
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A Dic with static Attributes.
|
A Dic with static Attributes.
|
||||||
"""
|
"""
|
||||||
dic = {
|
return {
|
||||||
"id": self.id,
|
"token": self.token,
|
||||||
"timestamp": {'year': self.timestamp.year,
|
"expires": self.expires,
|
||||||
'month': self.timestamp.month,
|
|
||||||
'day': self.timestamp.day,
|
|
||||||
'hour': self.timestamp.hour,
|
|
||||||
'minute': self.timestamp.minute,
|
|
||||||
'second': self.timestamp.second
|
|
||||||
},
|
|
||||||
"lifetime": self.lifetime,
|
"lifetime": self.lifetime,
|
||||||
"browser": self.browser,
|
"browser": self.browser,
|
||||||
"platform": self.platform
|
"platform": self.platform
|
||||||
}
|
}
|
||||||
return dic
|
|
||||||
|
|
||||||
def __eq__(self, token):
|
def __eq__(self, token):
|
||||||
return compare_digest(self.token, token)
|
return compare_digest(self.token, token)
|
||||||
|
|
||||||
def __sub__(self, other):
|
|
||||||
return other - self.timestamp
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "AccessToken(user={}, token={}, timestamp={}, lifetime={}".format(
|
return "AccessToken(user={}, token={}, expires={}, lifetime={})".format(
|
||||||
self.user, self.token, self.timestamp, self.lifetime)
|
self.user, self.token, self.expires, self.lifetime)
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "AccessToken(user={}, token={}, timestamp={}, lifetime={}".format(
|
|
||||||
self.user, self.token, self.timestamp, self.lifetime)
|
|
||||||
|
|
Loading…
Reference in New Issue