From 805db9814a4ffca3b4d7bf054292151e8ffdb1e8 Mon Sep 17 00:00:00 2001 From: knst-kotov Date: Wed, 25 Aug 2021 11:31:51 +0300 Subject: [PATCH] auth via email --- Pipfile | 1 + auth/authenticate.py | 22 +++++++++++++++++++++- auth/authorize.py | 14 ++------------ auth/email.py | 27 +++++++++++++++++++++++++++ auth/oauth.py | 2 +- resolvers/auth.py | 8 +++----- schema.graphql | 2 +- settings.py | 4 ++++ 8 files changed, 60 insertions(+), 20 deletions(-) create mode 100644 auth/email.py diff --git a/Pipfile b/Pipfile index bd7795f9..f2c6b603 100644 --- a/Pipfile +++ b/Pipfile @@ -20,6 +20,7 @@ bson = "*" python-frontmatter = "*" bs4 = "*" psycopg2 = "*" +requests = "*" [dev-packages] diff --git a/auth/authenticate.py b/auth/authenticate.py index 5c4764c8..84591a16 100644 --- a/auth/authenticate.py +++ b/auth/authenticate.py @@ -8,10 +8,11 @@ from starlette.requests import HTTPConnection from auth.credentials import AuthCredentials, AuthUser from auth.token import Token +from auth.authorize import Authorize from exceptions import InvalidToken, OperationNotAllowed from orm import User from redis import redis -from settings import JWT_AUTH_HEADER +from settings import JWT_AUTH_HEADER, EMAIL_TOKEN_LIFE_SPAN class _Authenticate: @@ -68,6 +69,25 @@ class JWTAuthenticate(AuthenticationBackend): scopes = User.get_permission(user_id=payload.user_id) return AuthCredentials(user_id=payload.user_id, scopes=scopes, logged_in=True), AuthUser(user_id=payload.user_id) +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: + return + if payload.device != "email": + return; + auth_token = Authorize.authorize(payload.user) + return (auth_token, payload.user) def login_required(func): @wraps(func) diff --git a/auth/authorize.py b/auth/authorize.py index 46b8a324..682876fe 100644 --- a/auth/authorize.py +++ b/auth/authorize.py @@ -8,14 +8,14 @@ from auth.validations import User class Authorize: @staticmethod - async def authorize(user: User, device: str = "pc", auto_delete=True) -> str: + async def authorize(user: User, device: str = "pc", life_span = JWT_LIFE_SPAN, auto_delete=True) -> str: """ :param user: :param device: :param auto_delete: Whether the expiration is automatically deleted, the default is True :return: """ - exp = datetime.utcnow() + timedelta(seconds=JWT_LIFE_SPAN) + exp = datetime.utcnow() + timedelta(seconds=life_span) token = Token.encode(user, exp=exp, device=device) await redis.execute("SET", f"{user.id}-{token}", "True") if auto_delete: @@ -37,13 +37,3 @@ class Authorize: async def revoke_all(user: User): tokens = await redis.execute("KEYS", f"{user.id}-*") await redis.execute("DEL", *tokens) - - @staticmethod - async def confirm(token: str): - try: - # NOTE: auth_token and email_token are different - payload = Token.decode(token) # TODO: check to decode here the proper way - auth_token = self.authorize(payload.user) - return auth_token, payload.user - except: - pass diff --git a/auth/email.py b/auth/email.py new file mode 100644 index 00000000..d314e06c --- /dev/null +++ b/auth/email.py @@ -0,0 +1,27 @@ +import requests + +from auth.authenticate import EmailAuthenticate + +from settings import MAILGUN_API_KEY, MAILGUN_DOMAIN + +MAILGUN_API_URL = "https://api.mailgun.net/v3/%s/messages" % (MAILGUN_DOMAIN) +MAILGUN_FROM = "postmaster " % (MAILGUN_DOMAIN) + +AUTH_URL = "https://localhost:8080/auth" + +async def send_auth_email(user): + token = await EmailAuthenticate.get_email_token(user) + + to = "%s <%s>" % (user.username, user.email) + text = "%s&token=%s" % (AUTH_URL, token) + response = requests.post( + MAILGUN_API_URL, + auth = ("api", MAILGUN_API_KEY), + data = { + "from": MAILGUN_FROM, + "to": to, + "subject": "authorize log in", + "text": text + } + ) + response.raise_for_status() diff --git a/auth/oauth.py b/auth/oauth.py index ea70792b..987ced3a 100644 --- a/auth/oauth.py +++ b/auth/oauth.py @@ -64,5 +64,5 @@ async def oauth_authorize(request): "username" : profile["name"] } user = Identity.identity_oauth(user_input) - token = await Authorize.authorize(user, device="pc", auto_delete=False) + token = await Authorize.authorize(user, device="pc") return PlainTextResponse(token) diff --git a/resolvers/auth.py b/resolvers/auth.py index b9341eb5..6eeab110 100644 --- a/resolvers/auth.py +++ b/resolvers/auth.py @@ -5,6 +5,7 @@ from auth.authorize import Authorize from auth.identity import Identity from auth.password import Password from auth.validations import CreateUser +from auth.email import send_auth_email from orm import User from orm.base import local_session from resolvers.base import mutation, query @@ -29,11 +30,8 @@ async def register(*_, email: str, password: str = ""): create_user = CreateUser(**inp) create_user.username = email.split('@')[0] if not password: - # NOTE: 1 hour confirm_token expire - confirm_token = Token.encode(create_user, datetime.now() + timedelta(hours = 1) , "email") - # TODO: sendAuthEmail(confirm_token) - # без пароля не возвращаем, а высылаем токен на почту - # + user = User.create(**create_user.dict()) + await send_auth_email(user) return { "user": user } else: create_user.password = Password.encode(create_user.password) diff --git a/schema.graphql b/schema.graphql index 2acdb4fb..2d445498 100644 --- a/schema.graphql +++ b/schema.graphql @@ -58,7 +58,7 @@ type Mutation { confirmEmail(token: String!): AuthResult! requestPasswordReset(email: String!): Boolean! confirmPasswordReset(token: String!): Boolean! - registerUser(email: String!, password: String!): AuthResult! + registerUser(email: String!, password: String): AuthResult! # updatePassword(password: String!, token: String!): Token! # invalidateAllTokens: Boolean! # invalidateTokenById(id: Int!): Boolean! diff --git a/settings.py b/settings.py index 5660bdc6..70a806d0 100644 --- a/settings.py +++ b/settings.py @@ -8,8 +8,12 @@ JWT_ALGORITHM = "HS256" JWT_SECRET_KEY = "8f1bd7696ffb482d8486dfbc6e7d16dd-secret-key" JWT_LIFE_SPAN = 24 * 60 * 60 # seconds JWT_AUTH_HEADER = "Auth" +EMAIL_TOKEN_LIFE_SPAN = 1 * 60 * 60 # seconds REDIS_URL = environ.get("REDIS_URL") or "redis://127.0.0.1" +MAILGUN_API_KEY = environ.get("MAILGUN_API_KEY") +MAILGUN_DOMAIN = "sandbox6afe2b71cd354c8fa59e0b868c20a23b.mailgun.org" + OAUTH_PROVIDERS = ("GITHUB", "FACEBOOK", "GOOGLE") OAUTH_CLIENTS = {} for provider in OAUTH_PROVIDERS: