core/auth/authenticate.py

135 lines
4.4 KiB
Python
Raw Normal View History

2021-07-13 10:14:48 +00:00
from functools import wraps
from typing import Optional, Tuple
2022-01-13 12:16:35 +00:00
from datetime import datetime, timedelta
2021-07-13 10:14:48 +00:00
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.jwtcodec import JWTCodec
2022-01-13 12:16:35 +00:00
from auth.authorize import Authorize, TokenStorage
2022-08-11 05:53:14 +00:00
from base.exceptions import InvalidToken
from orm.user import User
2022-08-11 09:09:57 +00:00
from services.auth.users import UserStorage
2022-08-11 05:53:14 +00:00
from base.orm import local_session
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 = JWTCodec.decode(token)
2021-08-18 16:53:55 +00:00
except ExpiredSignatureError:
payload = JWTCodec.decode(token, verify_exp=False)
2021-08-18 16:53:55 +00:00
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):
2022-01-13 12:16:35 +00:00
return await TokenStorage.exist(f"{user_id}-{token}")
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
2022-01-13 12:16:35 +00:00
class ResetPassword:
@staticmethod
async def get_reset_token(user):
exp = datetime.utcnow() + timedelta(seconds=EMAIL_TOKEN_LIFE_SPAN)
token = JWTCodec.encode(user, exp=exp, device="pc")
2022-01-13 12:16:35 +00:00
await TokenStorage.save(f"{user.id}-reset-{token}", EMAIL_TOKEN_LIFE_SPAN, True)
return token
@staticmethod
async def verify(token):
try:
payload = JWTCodec.decode(token)
2022-01-13 12:16:35 +00:00
except ExpiredSignatureError:
raise InvalidToken("Login expired, please login again")
except DecodeError as e:
raise InvalidToken("token format error") from e
else:
if not await TokenStorage.exist(f"{payload.user_id}-reset-{token}"):
raise InvalidToken("Login expired, please login again")
return payload.user_id
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