auth via email

This commit is contained in:
knst-kotov 2021-08-25 11:31:51 +03:00
parent 3c538d3724
commit 805db9814a
8 changed files with 60 additions and 20 deletions

View File

@ -20,6 +20,7 @@ bson = "*"
python-frontmatter = "*" python-frontmatter = "*"
bs4 = "*" bs4 = "*"
psycopg2 = "*" psycopg2 = "*"
requests = "*"
[dev-packages] [dev-packages]

View File

@ -8,10 +8,11 @@ from starlette.requests import HTTPConnection
from auth.credentials import AuthCredentials, AuthUser from auth.credentials import AuthCredentials, AuthUser
from auth.token import Token from auth.token import Token
from auth.authorize import Authorize
from exceptions import InvalidToken, OperationNotAllowed from exceptions import InvalidToken, OperationNotAllowed
from orm import User from orm import User
from redis import redis from redis import redis
from settings import JWT_AUTH_HEADER from settings import JWT_AUTH_HEADER, EMAIL_TOKEN_LIFE_SPAN
class _Authenticate: class _Authenticate:
@ -68,6 +69,25 @@ class JWTAuthenticate(AuthenticationBackend):
scopes = User.get_permission(user_id=payload.user_id) 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) 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): def login_required(func):
@wraps(func) @wraps(func)

View File

@ -8,14 +8,14 @@ from auth.validations import User
class Authorize: class Authorize:
@staticmethod @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 user:
:param device: :param device:
:param auto_delete: Whether the expiration is automatically deleted, the default is True :param auto_delete: Whether the expiration is automatically deleted, the default is True
:return: :return:
""" """
exp = datetime.utcnow() + timedelta(seconds=JWT_LIFE_SPAN) exp = datetime.utcnow() + timedelta(seconds=life_span)
token = Token.encode(user, exp=exp, device=device) token = Token.encode(user, exp=exp, device=device)
await redis.execute("SET", f"{user.id}-{token}", "True") await redis.execute("SET", f"{user.id}-{token}", "True")
if auto_delete: if auto_delete:
@ -37,13 +37,3 @@ class Authorize:
async def revoke_all(user: User): async def revoke_all(user: User):
tokens = await redis.execute("KEYS", f"{user.id}-*") tokens = await redis.execute("KEYS", f"{user.id}-*")
await redis.execute("DEL", *tokens) 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

27
auth/email.py Normal file
View File

@ -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 <postmaster@%s>" % (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()

View File

@ -64,5 +64,5 @@ async def oauth_authorize(request):
"username" : profile["name"] "username" : profile["name"]
} }
user = Identity.identity_oauth(user_input) 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) return PlainTextResponse(token)

View File

@ -5,6 +5,7 @@ from auth.authorize import Authorize
from auth.identity import Identity from auth.identity import Identity
from auth.password import Password from auth.password import Password
from auth.validations import CreateUser from auth.validations import CreateUser
from auth.email import send_auth_email
from orm import User from orm import User
from orm.base import local_session from orm.base import local_session
from resolvers.base import mutation, query from resolvers.base import mutation, query
@ -29,11 +30,8 @@ async def register(*_, email: str, password: str = ""):
create_user = CreateUser(**inp) create_user = CreateUser(**inp)
create_user.username = email.split('@')[0] create_user.username = email.split('@')[0]
if not password: if not password:
# NOTE: 1 hour confirm_token expire user = User.create(**create_user.dict())
confirm_token = Token.encode(create_user, datetime.now() + timedelta(hours = 1) , "email") await send_auth_email(user)
# TODO: sendAuthEmail(confirm_token)
# без пароля не возвращаем, а высылаем токен на почту
#
return { "user": user } return { "user": user }
else: else:
create_user.password = Password.encode(create_user.password) create_user.password = Password.encode(create_user.password)

View File

@ -58,7 +58,7 @@ type Mutation {
confirmEmail(token: String!): AuthResult! confirmEmail(token: String!): AuthResult!
requestPasswordReset(email: String!): Boolean! requestPasswordReset(email: String!): Boolean!
confirmPasswordReset(token: String!): Boolean! confirmPasswordReset(token: String!): Boolean!
registerUser(email: String!, password: String!): AuthResult! registerUser(email: String!, password: String): AuthResult!
# updatePassword(password: String!, token: String!): Token! # updatePassword(password: String!, token: String!): Token!
# invalidateAllTokens: Boolean! # invalidateAllTokens: Boolean!
# invalidateTokenById(id: Int!): Boolean! # invalidateTokenById(id: Int!): Boolean!

View File

@ -8,8 +8,12 @@ JWT_ALGORITHM = "HS256"
JWT_SECRET_KEY = "8f1bd7696ffb482d8486dfbc6e7d16dd-secret-key" JWT_SECRET_KEY = "8f1bd7696ffb482d8486dfbc6e7d16dd-secret-key"
JWT_LIFE_SPAN = 24 * 60 * 60 # seconds JWT_LIFE_SPAN = 24 * 60 * 60 # seconds
JWT_AUTH_HEADER = "Auth" JWT_AUTH_HEADER = "Auth"
EMAIL_TOKEN_LIFE_SPAN = 1 * 60 * 60 # seconds
REDIS_URL = environ.get("REDIS_URL") or "redis://127.0.0.1" 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_PROVIDERS = ("GITHUB", "FACEBOOK", "GOOGLE")
OAUTH_CLIENTS = {} OAUTH_CLIENTS = {}
for provider in OAUTH_PROVIDERS: for provider in OAUTH_PROVIDERS: