core/auth/authenticate.py

115 lines
3.6 KiB
Python
Raw Normal View History

2021-07-13 10:14:48 +00:00
from functools import wraps
from typing import Optional, Tuple
from graphql import GraphQLResolveInfo
2021-07-14 14:45:31 +00:00
from jwt import DecodeError, ExpiredSignatureError
2021-07-13 10:14:48 +00:00
from starlette.authentication import AuthenticationBackend
from starlette.requests import HTTPConnection
from auth.credentials import AuthCredentials, AuthUser
from auth.token import Token
2021-08-25 08:31:51 +00:00
from auth.authorize import Authorize
2021-07-13 10:14:48 +00:00
from exceptions import InvalidToken, OperationNotAllowed
2021-11-24 12:56:09 +00:00
from orm import User, UserStorage
2021-08-25 13:39:24 +00:00
from orm.base import local_session
2021-07-13 10:14:48 +00:00
from redis import redis
2021-08-25 08:31:51 +00:00
from settings import JWT_AUTH_HEADER, EMAIL_TOKEN_LIFE_SPAN
2021-07-13 10:14:48 +00:00
class _Authenticate:
2021-08-18 16:53:55 +00:00
@classmethod
async def verify(cls, token: str):
"""
Rules for a token to be valid.
1. token format is legal &&
token exists in redis database &&
token is not expired
2. token format is legal &&
token exists in redis database &&
token is expired &&
token is of specified type
"""
try:
payload = Token.decode(token)
except ExpiredSignatureError:
payload = Token.decode(token, verify_exp=False)
if not await cls.exists(payload.user_id, token):
raise InvalidToken("Login expired, please login again")
if payload.device == "mobile": # noqa
"we cat set mobile token to be valid forever"
return payload
except DecodeError as e:
raise InvalidToken("token format error") from e
else:
if not await cls.exists(payload.user_id, token):
raise InvalidToken("Login expired, please login again")
return payload
2021-07-13 10:14:48 +00:00
2021-08-18 16:53:55 +00:00
@classmethod
async def exists(cls, user_id, token):
token = await redis.execute("GET", f"{user_id}-{token}")
return token is not None
2021-07-13 10:14:48 +00:00
class JWTAuthenticate(AuthenticationBackend):
2021-08-18 16:53:55 +00:00
async def authenticate(
self, request: HTTPConnection
) -> Optional[Tuple[AuthCredentials, AuthUser]]:
if JWT_AUTH_HEADER not in request.headers:
return AuthCredentials(scopes=[]), AuthUser(user_id=None)
2021-07-13 10:14:48 +00:00
2021-08-18 16:53:55 +00:00
token = request.headers[JWT_AUTH_HEADER]
try:
payload = await _Authenticate.verify(token)
except Exception as exc:
return AuthCredentials(scopes=[], error_message=str(exc)), AuthUser(user_id=None)
if payload is None:
return AuthCredentials(scopes=[]), AuthUser(user_id=None)
2021-07-13 10:14:48 +00:00
2021-08-26 09:24:46 +00:00
if not payload.device in ("pc", "mobile"):
return AuthCredentials(scopes=[]), AuthUser(user_id=None)
2021-11-24 12:56:09 +00:00
user = await UserStorage.get_user(payload.user_id)
if not user:
return AuthCredentials(scopes=[]), AuthUser(user_id=None)
scopes = await user.get_permission()
2021-11-24 12:56:09 +00:00
return AuthCredentials(user_id=payload.user_id, scopes=scopes, logged_in=True), user
2021-07-13 10:14:48 +00:00
2021-08-25 08:31:51 +00:00
class EmailAuthenticate:
@staticmethod
async def get_email_token(user):
token = await Authorize.authorize(
user,
device="email",
life_span=EMAIL_TOKEN_LIFE_SPAN
)
return token
@staticmethod
async def authenticate(token):
payload = await _Authenticate.verify(token)
if payload is None:
2021-08-25 13:39:24 +00:00
raise InvalidToken("invalid token")
2021-08-25 08:31:51 +00:00
if payload.device != "email":
2021-08-25 13:39:24 +00:00
raise InvalidToken("invalid token")
with local_session() as session:
user = session.query(User).filter_by(id=payload.user_id).first()
2021-08-26 09:24:46 +00:00
if not user:
raise Exception("user not exist")
if not user.emailConfirmed:
user.emailConfirmed = True
session.commit()
2021-08-25 13:39:24 +00:00
auth_token = await Authorize.authorize(user)
return (auth_token, user)
2021-07-13 10:14:48 +00:00
def login_required(func):
2021-08-18 16:53:55 +00:00
@wraps(func)
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
auth: AuthCredentials = info.context["request"].auth
if not auth.logged_in:
return {"error" : auth.error_message or "Please login"}
return await func(parent, info, *args, **kwargs)
return wrap