migration, auth, refactoring, formatting
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
from auth.email import load_email_templates
|
||||
|
||||
load_email_templates()
|
@@ -1,21 +1,20 @@
|
||||
from functools import wraps
|
||||
from typing import Optional, Tuple
|
||||
from datetime import datetime, timedelta
|
||||
from graphql import GraphQLResolveInfo
|
||||
|
||||
from graphql.type import GraphQLResolveInfo
|
||||
from jwt import DecodeError, ExpiredSignatureError
|
||||
from starlette.authentication import AuthenticationBackend
|
||||
from starlette.requests import HTTPConnection
|
||||
|
||||
from auth.credentials import AuthCredentials, AuthUser
|
||||
from auth.jwtcodec import JWTCodec
|
||||
from auth.authorize import Authorize, TokenStorage
|
||||
from auth.tokenstorage import TokenStorage
|
||||
from base.exceptions import InvalidToken
|
||||
from orm.user import User
|
||||
from services.auth.users import UserStorage
|
||||
from base.orm import local_session
|
||||
from settings import JWT_AUTH_HEADER, EMAIL_TOKEN_LIFE_SPAN
|
||||
from settings import SESSION_TOKEN_HEADER
|
||||
|
||||
|
||||
class _Authenticate:
|
||||
class SessionToken:
|
||||
@classmethod
|
||||
async def verify(cls, token: str):
|
||||
"""
|
||||
@@ -32,33 +31,30 @@ class _Authenticate:
|
||||
payload = JWTCodec.decode(token)
|
||||
except ExpiredSignatureError:
|
||||
payload = JWTCodec.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
|
||||
if not await cls.get(payload.user_id, token):
|
||||
raise InvalidToken("Session token has expired, please try again")
|
||||
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")
|
||||
if not await cls.get(payload.user_id, token):
|
||||
raise InvalidToken("Session token has expired, please login again")
|
||||
return payload
|
||||
|
||||
@classmethod
|
||||
async def exists(cls, user_id, token):
|
||||
return await TokenStorage.exist(f"{user_id}-{token}")
|
||||
async def get(cls, uid, token):
|
||||
return await TokenStorage.get(f"{uid}-{token}")
|
||||
|
||||
|
||||
class JWTAuthenticate(AuthenticationBackend):
|
||||
async def authenticate(
|
||||
self, request: HTTPConnection
|
||||
) -> Optional[Tuple[AuthCredentials, AuthUser]]:
|
||||
if JWT_AUTH_HEADER not in request.headers:
|
||||
if SESSION_TOKEN_HEADER not in request.headers:
|
||||
return AuthCredentials(scopes=[]), AuthUser(user_id=None)
|
||||
|
||||
token = request.headers[JWT_AUTH_HEADER]
|
||||
token = request.headers[SESSION_TOKEN_HEADER]
|
||||
try:
|
||||
payload = await _Authenticate.verify(token)
|
||||
payload = await SessionToken.verify(token)
|
||||
except Exception as exc:
|
||||
return AuthCredentials(scopes=[], error_message=str(exc)), AuthUser(
|
||||
user_id=None
|
||||
@@ -67,9 +63,6 @@ class JWTAuthenticate(AuthenticationBackend):
|
||||
if payload is None:
|
||||
return AuthCredentials(scopes=[]), AuthUser(user_id=None)
|
||||
|
||||
if payload.device not in ("pc", "mobile"):
|
||||
return AuthCredentials(scopes=[]), AuthUser(user_id=None)
|
||||
|
||||
user = await UserStorage.get_user(payload.user_id)
|
||||
if not user:
|
||||
return AuthCredentials(scopes=[]), AuthUser(user_id=None)
|
||||
@@ -81,55 +74,6 @@ class JWTAuthenticate(AuthenticationBackend):
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
raise InvalidToken("invalid token")
|
||||
if payload.device != "email":
|
||||
raise InvalidToken("invalid token")
|
||||
with local_session() as session:
|
||||
user = session.query(User).filter_by(id=payload.user_id).first()
|
||||
if not user:
|
||||
raise Exception("user not exist")
|
||||
if not user.emailConfirmed:
|
||||
user.emailConfirmed = True
|
||||
session.commit()
|
||||
auth_token = await Authorize.authorize(user)
|
||||
return (auth_token, user)
|
||||
|
||||
|
||||
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")
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
def login_required(func):
|
||||
@wraps(func)
|
||||
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
|
||||
|
@@ -1,45 +0,0 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from auth.jwtcodec import JWTCodec
|
||||
from base.redis import redis
|
||||
from settings import JWT_LIFE_SPAN
|
||||
from auth.validations import User
|
||||
|
||||
|
||||
class TokenStorage:
|
||||
@staticmethod
|
||||
async def save(token_key, life_span, auto_delete=True):
|
||||
await redis.execute("SET", token_key, "True")
|
||||
if auto_delete:
|
||||
expire_at = (datetime.now() + timedelta(seconds=life_span)).timestamp()
|
||||
await redis.execute("EXPIREAT", token_key, int(expire_at))
|
||||
|
||||
@staticmethod
|
||||
async def exist(token_key):
|
||||
return await redis.execute("GET", token_key)
|
||||
|
||||
|
||||
class Authorize:
|
||||
@staticmethod
|
||||
async def authorize(
|
||||
user: User, device: str = "pc", life_span=JWT_LIFE_SPAN, auto_delete=True
|
||||
) -> str:
|
||||
exp = datetime.utcnow() + timedelta(seconds=life_span)
|
||||
token = JWTCodec.encode(user, exp=exp, device=device)
|
||||
await TokenStorage.save(f"{user.id}-{token}", life_span, auto_delete)
|
||||
return token
|
||||
|
||||
@staticmethod
|
||||
async def revoke(token: str) -> bool:
|
||||
try:
|
||||
payload = JWTCodec.decode(token)
|
||||
except: # noqa
|
||||
pass
|
||||
else:
|
||||
await redis.execute("DEL", f"{payload.user_id}-{token}")
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def revoke_all(user: User):
|
||||
tokens = await redis.execute("KEYS", f"{user.id}-*")
|
||||
await redis.execute("DEL", *tokens)
|
@@ -1,6 +1,9 @@
|
||||
from typing import List, Optional, Text
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from base.exceptions import OperationNotAllowed
|
||||
|
||||
|
||||
class Permission(BaseModel):
|
||||
name: Text
|
||||
@@ -17,7 +20,8 @@ class AuthCredentials(BaseModel):
|
||||
return True
|
||||
|
||||
async def permissions(self) -> List[Permission]:
|
||||
assert self.user_id is not None, "Please login first"
|
||||
if self.user_id is not None:
|
||||
raise OperationNotAllowed("Please login first")
|
||||
return NotImplemented()
|
||||
|
||||
|
||||
|
112
auth/email.py
112
auth/email.py
@@ -1,84 +1,28 @@
|
||||
import requests
|
||||
from starlette.responses import RedirectResponse
|
||||
from auth.authenticate import EmailAuthenticate, ResetPassword
|
||||
from base.orm import local_session
|
||||
from settings import (
|
||||
BACKEND_URL,
|
||||
MAILGUN_API_KEY,
|
||||
MAILGUN_DOMAIN,
|
||||
RESET_PWD_URL,
|
||||
CONFIRM_EMAIL_URL,
|
||||
ERROR_URL_ON_FRONTEND,
|
||||
)
|
||||
|
||||
MAILGUN_API_URL = "https://api.mailgun.net/v3/%s/messages" % (MAILGUN_DOMAIN)
|
||||
MAILGUN_FROM = "discours.io <noreply@%s>" % (MAILGUN_DOMAIN)
|
||||
|
||||
AUTH_URL = "%s/email_authorize" % (BACKEND_URL)
|
||||
|
||||
email_templates = {"confirm_email": "", "auth_email": "", "reset_password_email": ""}
|
||||
|
||||
|
||||
def load_email_templates():
|
||||
for name in email_templates:
|
||||
filename = "auth/templates/%s.tmpl" % name
|
||||
with open(filename) as f:
|
||||
email_templates[name] = f.read()
|
||||
print("[auth.email] templates loaded")
|
||||
|
||||
|
||||
async def send_confirm_email(user):
|
||||
text = email_templates["confirm_email"]
|
||||
token = await EmailAuthenticate.get_email_token(user)
|
||||
await send_email(user, AUTH_URL, text, token)
|
||||
|
||||
|
||||
async def send_auth_email(user):
|
||||
text = email_templates["auth_email"]
|
||||
token = await EmailAuthenticate.get_email_token(user)
|
||||
await send_email(user, AUTH_URL, text, token)
|
||||
|
||||
|
||||
async def send_reset_password_email(user):
|
||||
text = email_templates["reset_password_email"]
|
||||
token = await ResetPassword.get_reset_token(user)
|
||||
await send_email(user, RESET_PWD_URL, text, token)
|
||||
|
||||
|
||||
async def send_email(user, url, text, token):
|
||||
to = "%s <%s>" % (user.username, user.email)
|
||||
url_with_token = "%s?token=%s" % (url, token)
|
||||
text = text % (url_with_token)
|
||||
response = requests.post(
|
||||
MAILGUN_API_URL,
|
||||
auth=("api", MAILGUN_API_KEY),
|
||||
data={
|
||||
"from": MAILGUN_FROM,
|
||||
"to": to,
|
||||
"subject": "authorize log in",
|
||||
"html": text,
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
async def email_authorize(request):
|
||||
token = request.query_params.get("token")
|
||||
if not token:
|
||||
url_with_error = "%s?error=%s" % (ERROR_URL_ON_FRONTEND, "INVALID_TOKEN")
|
||||
return RedirectResponse(url=url_with_error)
|
||||
|
||||
try:
|
||||
auth_token, user = await EmailAuthenticate.authenticate(token)
|
||||
except:
|
||||
url_with_error = "%s?error=%s" % (ERROR_URL_ON_FRONTEND, "INVALID_TOKEN")
|
||||
return RedirectResponse(url=url_with_error)
|
||||
|
||||
if not user.emailConfirmed:
|
||||
with local_session() as session:
|
||||
user.emailConfirmed = True
|
||||
session.commit()
|
||||
|
||||
response = RedirectResponse(url=CONFIRM_EMAIL_URL)
|
||||
response.set_cookie("token", auth_token)
|
||||
return response
|
||||
import requests
|
||||
|
||||
from settings import BACKEND_URL, MAILGUN_API_KEY, MAILGUN_DOMAIN
|
||||
|
||||
MAILGUN_API_URL = "https://api.mailgun.net/v3/%s/messages" % MAILGUN_DOMAIN
|
||||
MAILGUN_FROM = "discours.io <noreply@%s>" % MAILGUN_DOMAIN
|
||||
|
||||
|
||||
async def send_auth_email(user, token):
|
||||
text = """<html><body>
|
||||
Follow the <a href='%s'>link</link> to authorize
|
||||
</body></html>
|
||||
"""
|
||||
url = "%s/confirm_email" % BACKEND_URL
|
||||
to = "%s <%s>" % (user.username, user.email)
|
||||
url_with_token = "%s?token=%s" % (url, token)
|
||||
text = text % url_with_token
|
||||
response = requests.post(
|
||||
MAILGUN_API_URL,
|
||||
auth=("api", MAILGUN_API_KEY),
|
||||
data={
|
||||
"from": MAILGUN_FROM,
|
||||
"to": to,
|
||||
"subject": "Confirm email",
|
||||
"html": text,
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
@@ -1,16 +1,30 @@
|
||||
from auth.password import Password
|
||||
from base.exceptions import InvalidPassword
|
||||
from orm import User as OrmUser
|
||||
from base.orm import local_session
|
||||
from auth.validations import User
|
||||
|
||||
from jwt import DecodeError, ExpiredSignatureError
|
||||
from sqlalchemy import or_
|
||||
|
||||
from auth.jwtcodec import JWTCodec
|
||||
from auth.tokenstorage import TokenStorage
|
||||
from validations.auth import AuthInput
|
||||
from base.exceptions import InvalidPassword
|
||||
from base.exceptions import InvalidToken
|
||||
from base.orm import local_session
|
||||
from orm import User
|
||||
from passlib.hash import bcrypt
|
||||
|
||||
|
||||
class Password:
|
||||
@staticmethod
|
||||
def encode(password: str) -> str:
|
||||
return bcrypt.hash(password)
|
||||
|
||||
@staticmethod
|
||||
def verify(password: str, other: str) -> bool:
|
||||
return bcrypt.verify(password, other)
|
||||
|
||||
|
||||
class Identity:
|
||||
@staticmethod
|
||||
def identity(orm_user: OrmUser, password: str) -> User:
|
||||
user = User(**orm_user.dict())
|
||||
def password(orm_user: User, password: str) -> User:
|
||||
user = AuthInput(**orm_user.dict())
|
||||
if not user.password:
|
||||
raise InvalidPassword("User password is empty")
|
||||
if not Password.verify(password, user.password):
|
||||
@@ -18,22 +32,37 @@ class Identity:
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def identity_oauth(input) -> User:
|
||||
def oauth(inp: AuthInput) -> User:
|
||||
with local_session() as session:
|
||||
user = (
|
||||
session.query(OrmUser)
|
||||
.filter(
|
||||
or_(
|
||||
OrmUser.oauth == input["oauth"], OrmUser.email == input["email"]
|
||||
)
|
||||
)
|
||||
session.query(User)
|
||||
.filter(or_(User.oauth == inp["oauth"], User.email == inp["email"]))
|
||||
.first()
|
||||
)
|
||||
if not user:
|
||||
user = OrmUser.create(**input)
|
||||
user = User.create(**inp)
|
||||
if not user.oauth:
|
||||
user.oauth = input["oauth"]
|
||||
user.oauth = inp["oauth"]
|
||||
session.commit()
|
||||
|
||||
user = User(**user.dict())
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
async def onetime(token: str) -> User:
|
||||
try:
|
||||
payload = JWTCodec.decode(token)
|
||||
if not await TokenStorage.exist(f"{payload.user_id}-{token}"):
|
||||
raise InvalidToken("Login token has expired, please login again")
|
||||
except ExpiredSignatureError:
|
||||
raise InvalidToken("Login token has expired, please try again")
|
||||
except DecodeError as e:
|
||||
raise InvalidToken("token format error") from e
|
||||
with local_session() as session:
|
||||
user = session.query(User).filter_by(id=payload.user_id).first()
|
||||
if not user:
|
||||
raise Exception("user not exist")
|
||||
if not user.emailConfirmed:
|
||||
user.emailConfirmed = True
|
||||
session.commit()
|
||||
return user
|
||||
|
@@ -1,26 +1,29 @@
|
||||
from datetime import datetime
|
||||
|
||||
import jwt
|
||||
|
||||
from validations.auth import TokenPayload, AuthInput
|
||||
from settings import JWT_ALGORITHM, JWT_SECRET_KEY
|
||||
from auth.validations import PayLoad, User
|
||||
|
||||
|
||||
class JWTCodec:
|
||||
@staticmethod
|
||||
def encode(user: User, exp: datetime, device: str = "pc") -> str:
|
||||
def encode(user: AuthInput, exp: datetime) -> str:
|
||||
payload = {
|
||||
"user_id": user.id,
|
||||
"device": device,
|
||||
# "user_email": user.email, # less secure
|
||||
# "device": device, # no use cases
|
||||
"exp": exp,
|
||||
"iat": datetime.utcnow(),
|
||||
}
|
||||
return jwt.encode(payload, JWT_SECRET_KEY, JWT_ALGORITHM)
|
||||
|
||||
@staticmethod
|
||||
def decode(token: str, verify_exp: bool = True) -> PayLoad:
|
||||
def decode(token: str, verify_exp: bool = True) -> TokenPayload:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
key=JWT_SECRET_KEY,
|
||||
options={"verify_exp": verify_exp},
|
||||
algorithms=[JWT_ALGORITHM],
|
||||
)
|
||||
return PayLoad(**payload)
|
||||
return TokenPayload(**payload)
|
||||
|
180
auth/oauth.py
180
auth/oauth.py
@@ -1,91 +1,89 @@
|
||||
from authlib.integrations.starlette_client import OAuth
|
||||
from starlette.responses import RedirectResponse
|
||||
from auth.authorize import Authorize
|
||||
from auth.identity import Identity
|
||||
|
||||
from settings import OAUTH_CLIENTS, BACKEND_URL, OAUTH_CALLBACK_URL
|
||||
|
||||
oauth = OAuth()
|
||||
|
||||
oauth.register(
|
||||
name="facebook",
|
||||
client_id=OAUTH_CLIENTS["FACEBOOK"]["id"],
|
||||
client_secret=OAUTH_CLIENTS["FACEBOOK"]["key"],
|
||||
access_token_url="https://graph.facebook.com/v11.0/oauth/access_token",
|
||||
access_token_params=None,
|
||||
authorize_url="https://www.facebook.com/v11.0/dialog/oauth",
|
||||
authorize_params=None,
|
||||
api_base_url="https://graph.facebook.com/",
|
||||
client_kwargs={"scope": "public_profile email"},
|
||||
)
|
||||
|
||||
oauth.register(
|
||||
name="github",
|
||||
client_id=OAUTH_CLIENTS["GITHUB"]["id"],
|
||||
client_secret=OAUTH_CLIENTS["GITHUB"]["key"],
|
||||
access_token_url="https://github.com/login/oauth/access_token",
|
||||
access_token_params=None,
|
||||
authorize_url="https://github.com/login/oauth/authorize",
|
||||
authorize_params=None,
|
||||
api_base_url="https://api.github.com/",
|
||||
client_kwargs={"scope": "user:email"},
|
||||
)
|
||||
|
||||
oauth.register(
|
||||
name="google",
|
||||
client_id=OAUTH_CLIENTS["GOOGLE"]["id"],
|
||||
client_secret=OAUTH_CLIENTS["GOOGLE"]["key"],
|
||||
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
|
||||
client_kwargs={"scope": "openid email profile"},
|
||||
)
|
||||
|
||||
|
||||
async def google_profile(client, request, token):
|
||||
profile = await client.parse_id_token(request, token)
|
||||
profile["id"] = profile["sub"]
|
||||
return profile
|
||||
|
||||
|
||||
async def facebook_profile(client, request, token):
|
||||
profile = await client.get("me?fields=name,id,email", token=token)
|
||||
return profile.json()
|
||||
|
||||
|
||||
async def github_profile(client, request, token):
|
||||
profile = await client.get("user", token=token)
|
||||
return profile.json()
|
||||
|
||||
|
||||
profile_callbacks = {
|
||||
"google": google_profile,
|
||||
"facebook": facebook_profile,
|
||||
"github": github_profile,
|
||||
}
|
||||
|
||||
|
||||
async def oauth_login(request):
|
||||
provider = request.path_params["provider"]
|
||||
request.session["provider"] = provider
|
||||
client = oauth.create_client(provider)
|
||||
redirect_uri = "%s/%s" % (BACKEND_URL, "oauth_authorize")
|
||||
return await client.authorize_redirect(request, redirect_uri)
|
||||
|
||||
|
||||
async def oauth_authorize(request):
|
||||
provider = request.session["provider"]
|
||||
client = oauth.create_client(provider)
|
||||
token = await client.authorize_access_token(request)
|
||||
get_profile = profile_callbacks[provider]
|
||||
profile = await get_profile(client, request, token)
|
||||
user_oauth_info = "%s:%s" % (provider, profile["id"])
|
||||
user_input = {
|
||||
"oauth": user_oauth_info,
|
||||
"email": profile["email"],
|
||||
"username": profile["name"],
|
||||
}
|
||||
user = Identity.identity_oauth(user_input)
|
||||
token = await Authorize.authorize(user, device="pc")
|
||||
|
||||
response = RedirectResponse(url=OAUTH_CALLBACK_URL)
|
||||
response.set_cookie("token", token)
|
||||
return response
|
||||
from authlib.integrations.starlette_client import OAuth
|
||||
from starlette.responses import RedirectResponse
|
||||
from auth.identity import Identity
|
||||
from auth.tokenstorage import TokenStorage
|
||||
from settings import OAUTH_CLIENTS, BACKEND_URL, OAUTH_CALLBACK_URL
|
||||
|
||||
oauth = OAuth()
|
||||
|
||||
oauth.register(
|
||||
name="facebook",
|
||||
client_id=OAUTH_CLIENTS["FACEBOOK"]["id"],
|
||||
client_secret=OAUTH_CLIENTS["FACEBOOK"]["key"],
|
||||
access_token_url="https://graph.facebook.com/v11.0/oauth/access_token",
|
||||
access_token_params=None,
|
||||
authorize_url="https://www.facebook.com/v11.0/dialog/oauth",
|
||||
authorize_params=None,
|
||||
api_base_url="https://graph.facebook.com/",
|
||||
client_kwargs={"scope": "public_profile email"},
|
||||
)
|
||||
|
||||
oauth.register(
|
||||
name="github",
|
||||
client_id=OAUTH_CLIENTS["GITHUB"]["id"],
|
||||
client_secret=OAUTH_CLIENTS["GITHUB"]["key"],
|
||||
access_token_url="https://github.com/login/oauth/access_token",
|
||||
access_token_params=None,
|
||||
authorize_url="https://github.com/login/oauth/authorize",
|
||||
authorize_params=None,
|
||||
api_base_url="https://api.github.com/",
|
||||
client_kwargs={"scope": "user:email"},
|
||||
)
|
||||
|
||||
oauth.register(
|
||||
name="google",
|
||||
client_id=OAUTH_CLIENTS["GOOGLE"]["id"],
|
||||
client_secret=OAUTH_CLIENTS["GOOGLE"]["key"],
|
||||
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
|
||||
client_kwargs={"scope": "openid email profile"},
|
||||
)
|
||||
|
||||
|
||||
async def google_profile(client, request, token):
|
||||
profile = await client.parse_id_token(request, token)
|
||||
profile["id"] = profile["sub"]
|
||||
return profile
|
||||
|
||||
|
||||
async def facebook_profile(client, request, token):
|
||||
profile = await client.get("me?fields=name,id,email", token=token)
|
||||
return profile.json()
|
||||
|
||||
|
||||
async def github_profile(client, request, token):
|
||||
profile = await client.get("user", token=token)
|
||||
return profile.json()
|
||||
|
||||
|
||||
profile_callbacks = {
|
||||
"google": google_profile,
|
||||
"facebook": facebook_profile,
|
||||
"github": github_profile,
|
||||
}
|
||||
|
||||
|
||||
async def oauth_login(request):
|
||||
provider = request.path_params["provider"]
|
||||
request.session["provider"] = provider
|
||||
client = oauth.create_client(provider)
|
||||
redirect_uri = "%s/%s" % (BACKEND_URL, "oauth_authorize")
|
||||
return await client.authorize_redirect(request, redirect_uri)
|
||||
|
||||
|
||||
async def oauth_authorize(request):
|
||||
provider = request.session["provider"]
|
||||
client = oauth.create_client(provider)
|
||||
token = await client.authorize_access_token(request)
|
||||
get_profile = profile_callbacks[provider]
|
||||
profile = await get_profile(client, request, token)
|
||||
user_oauth_info = "%s:%s" % (provider, profile["id"])
|
||||
user_input = {
|
||||
"oauth": user_oauth_info,
|
||||
"email": profile["email"],
|
||||
"username": profile["name"],
|
||||
}
|
||||
user = Identity.oauth(user_input)
|
||||
session_token = await TokenStorage.create_session(user)
|
||||
response = RedirectResponse(url=OAUTH_CALLBACK_URL)
|
||||
response.set_cookie("token", session_token)
|
||||
return response
|
||||
|
@@ -1,11 +0,0 @@
|
||||
from passlib.hash import bcrypt
|
||||
|
||||
|
||||
class Password:
|
||||
@staticmethod
|
||||
def encode(password: str) -> str:
|
||||
return bcrypt.hash(password)
|
||||
|
||||
@staticmethod
|
||||
def verify(password: str, other: str) -> bool:
|
||||
return bcrypt.verify(password, other)
|
@@ -1 +0,0 @@
|
||||
<html><body>To enter the site follow the <a href='%s'>link</link></body></html>
|
@@ -1 +0,0 @@
|
||||
<html><body>To confirm registration follow the <a href='%s'>link</link></body></html>
|
@@ -1 +0,0 @@
|
||||
<html><body>To reset password follow the <a href='%s'>link</link></body></html>
|
50
auth/tokenstorage.py
Normal file
50
auth/tokenstorage.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from auth.jwtcodec import JWTCodec
|
||||
from validations.auth import AuthInput
|
||||
from base.redis import redis
|
||||
from settings import SESSION_TOKEN_LIFE_SPAN, ONETIME_TOKEN_LIFE_SPAN
|
||||
|
||||
|
||||
async def save(token_key, life_span, auto_delete=True):
|
||||
await redis.execute("SET", token_key, "True")
|
||||
if auto_delete:
|
||||
expire_at = (datetime.now() + timedelta(seconds=life_span)).timestamp()
|
||||
await redis.execute("EXPIREAT", token_key, int(expire_at))
|
||||
|
||||
|
||||
class TokenStorage:
|
||||
@staticmethod
|
||||
async def get(token_key):
|
||||
return await redis.execute("GET", token_key)
|
||||
|
||||
@staticmethod
|
||||
async def create_onetime(user: AuthInput) -> str:
|
||||
life_span = ONETIME_TOKEN_LIFE_SPAN
|
||||
exp = datetime.utcnow() + timedelta(seconds=life_span)
|
||||
one_time_token = JWTCodec.encode(user, exp=exp)
|
||||
await save(f"{user.id}-{one_time_token}", life_span)
|
||||
return one_time_token
|
||||
|
||||
@staticmethod
|
||||
async def create_session(user: AuthInput) -> str:
|
||||
life_span = SESSION_TOKEN_LIFE_SPAN
|
||||
exp = datetime.utcnow() + timedelta(seconds=life_span)
|
||||
session_token = JWTCodec.encode(user, exp=exp)
|
||||
await save(f"{user.id}-{session_token}", life_span)
|
||||
return session_token
|
||||
|
||||
@staticmethod
|
||||
async def revoke(token: str) -> bool:
|
||||
try:
|
||||
payload = JWTCodec.decode(token)
|
||||
except: # noqa
|
||||
pass
|
||||
else:
|
||||
await redis.execute("DEL", f"{payload.user_id}-{token}")
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def revoke_all(user: AuthInput):
|
||||
tokens = await redis.execute("KEYS", f"{user.id}-*")
|
||||
await redis.execute("DEL", *tokens)
|
@@ -1,27 +0,0 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional, Text
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
id: Optional[int]
|
||||
# age: Optional[int]
|
||||
username: Optional[Text]
|
||||
# phone: Optional[Text]
|
||||
password: Optional[Text]
|
||||
|
||||
|
||||
class PayLoad(BaseModel):
|
||||
user_id: int
|
||||
device: Text
|
||||
exp: datetime
|
||||
iat: datetime
|
||||
|
||||
|
||||
class CreateUser(BaseModel):
|
||||
email: Text
|
||||
username: Optional[Text]
|
||||
# age: Optional[int]
|
||||
# phone: Optional[Text]
|
||||
password: Optional[Text]
|
Reference in New Issue
Block a user