confirm-token-fix

This commit is contained in:
tonyrewin 2022-10-23 12:33:28 +03:00
parent 20d01a49ec
commit 4c3439d241
11 changed files with 90 additions and 45 deletions

View File

@ -11,7 +11,6 @@ from auth.jwtcodec import JWTCodec
from auth.tokenstorage import TokenStorage from auth.tokenstorage import TokenStorage
from base.exceptions import InvalidToken from base.exceptions import InvalidToken
from services.auth.users import UserStorage from services.auth.users import UserStorage
from settings import SESSION_TOKEN_HEADER
class SessionToken: class SessionToken:
@ -49,10 +48,10 @@ class JWTAuthenticate(AuthenticationBackend):
async def authenticate( async def authenticate(
self, request: HTTPConnection self, request: HTTPConnection
) -> Optional[Tuple[AuthCredentials, AuthUser]]: ) -> Optional[Tuple[AuthCredentials, AuthUser]]:
if SESSION_TOKEN_HEADER not in request.headers: if "Auth" not in request.headers:
return AuthCredentials(scopes=[]), AuthUser(user_id=None) return AuthCredentials(scopes=[]), AuthUser(user_id=None)
token = request.headers[SESSION_TOKEN_HEADER] token = request.headers.get("Auth", "")
try: try:
payload = await SessionToken.verify(token) payload = await SessionToken.verify(token)
except Exception as exc: except Exception as exc:
@ -77,6 +76,7 @@ class JWTAuthenticate(AuthenticationBackend):
def login_required(func): def login_required(func):
@wraps(func) @wraps(func)
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs): async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
# print('[auth.authenticate] login required for %r with info %r' % (func, info)) # debug only
auth: AuthCredentials = info.context["request"].auth auth: AuthCredentials = info.context["request"].auth
if not auth.logged_in: if not auth.logged_in:
return {"error": auth.error_message or "Please login"} return {"error": auth.error_message or "Please login"}

View File

@ -2,8 +2,8 @@ import requests
from settings import MAILGUN_API_KEY, MAILGUN_DOMAIN from settings import MAILGUN_API_KEY, MAILGUN_DOMAIN
api_url = "https://api.mailgun.net/v3/%s/messages" % MAILGUN_DOMAIN api_url = "https://api.mailgun.net/v3/%s/messages" % (MAILGUN_DOMAIN or 'discours.io')
noreply = "discours.io <noreply@%s>" % MAILGUN_DOMAIN noreply = "discours.io <noreply@%s>" % (MAILGUN_DOMAIN or 'discours.io')
lang_subject = { lang_subject = {
"ru": "Подтверждение почты", "ru": "Подтверждение почты",
"en": "Confirm email" "en": "Confirm email"

View File

@ -16,14 +16,21 @@ class JWTCodec:
"exp": exp, "exp": exp,
"iat": datetime.utcnow(), "iat": datetime.utcnow(),
} }
return jwt.encode(payload, JWT_SECRET_KEY, JWT_ALGORITHM) try:
r = jwt.encode(payload, JWT_SECRET_KEY, JWT_ALGORITHM)
return r
except Exception as e:
print('[jwtcodec] JWT encode error %r' % e)
@staticmethod @staticmethod
def decode(token: str, verify_exp: bool = True) -> TokenPayload: def decode(token: str, verify_exp: bool = True) -> TokenPayload:
payload = jwt.decode( try:
token, payload = jwt.decode(
key=JWT_SECRET_KEY, token,
options={"verify_exp": verify_exp}, key=JWT_SECRET_KEY,
algorithms=[JWT_ALGORITHM], options={"verify_exp": verify_exp},
) algorithms=[JWT_ALGORITHM],
return TokenPayload(**payload) )
return TokenPayload(**payload)
except Exception as e:
print('[jwtcodec] JWT decode error %r' % e)

View File

@ -55,7 +55,7 @@ async def shutdown():
routes = [ routes = [
Route("/oauth/{provider}", endpoint=oauth_login), Route("/oauth/{provider}", endpoint=oauth_login),
Route("/oauth-authorize", endpoint=oauth_authorize), Route("/oauth-authorize", endpoint=oauth_authorize),
Route("/confirm/{token}", endpoint=confirm_email_handler), # should be called on client Route("/confirm/{token}", endpoint=confirm_email_handler)
] ]
app = Starlette( app = Starlette(

View File

@ -119,7 +119,7 @@ server {
# #
# Custom headers and headers various browsers *should* be OK with but aren't # Custom headers and headers various browsers *should* be OK with but aren't
# #
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,auth'; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Auth';
add_header 'Access-Control-Allow-Credentials' 'true'; add_header 'Access-Control-Allow-Credentials' 'true';
# #
# Tell client that this pre-flight info is valid for 20 days # Tell client that this pre-flight info is valid for 20 days
@ -133,7 +133,7 @@ server {
if ($request_method = 'POST') { if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' '$allow_origin' always; add_header 'Access-Control-Allow-Origin' '$allow_origin' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,auth' always; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Auth' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
add_header 'Access-Control-Allow-Credentials' 'true' always; add_header 'Access-Control-Allow-Credentials' 'true' always;
} }
@ -141,7 +141,7 @@ server {
if ($request_method = 'GET') { if ($request_method = 'GET') {
add_header 'Access-Control-Allow-Origin' '$allow_origin' always; add_header 'Access-Control-Allow-Origin' '$allow_origin' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,auth' always; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Auth' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
add_header 'Access-Control-Allow-Credentials' 'true' always; add_header 'Access-Control-Allow-Credentials' 'true' always;
} }

View File

@ -13,6 +13,7 @@ from auth.authenticate import login_required
from auth.email import send_auth_email from auth.email import send_auth_email
from auth.identity import Identity, Password from auth.identity import Identity, Password
from base.exceptions import ( from base.exceptions import (
BaseHttpException,
InvalidPassword, InvalidPassword,
InvalidToken, InvalidToken,
ObjectNotExist, ObjectNotExist,
@ -21,13 +22,13 @@ from base.exceptions import (
from base.orm import local_session from base.orm import local_session
from base.resolvers import mutation, query from base.resolvers import mutation, query
from orm import User, Role from orm import User, Role
from resolvers.profile import get_user_subscriptions from resolvers.profile import user_subscriptions
from settings import SESSION_TOKEN_HEADER
@mutation.field("refreshSession") @mutation.field("refreshSession")
@login_required @login_required
async def get_current_user(_, info): async def get_current_user(_, info):
print('[resolvers.auth] get current user %r' % info)
user = info.context["request"].user user = info.context["request"].user
user.lastSeen = datetime.now() user.lastSeen = datetime.now()
with local_session() as session: with local_session() as session:
@ -37,17 +38,17 @@ async def get_current_user(_, info):
return { return {
"token": token, "token": token,
"user": user, "user": user,
"news": await get_user_subscriptions(user.slug), "news": await user_subscriptions(user.slug),
} }
@mutation.field("confirmEmail") @mutation.field("confirmEmail")
async def confirm_email(_, _info, code): async def confirm_email(_, info, token):
"""confirm owning email address""" """confirm owning email address"""
try: try:
payload = JWTCodec.decode(code) payload = JWTCodec.decode(token)
user_id = payload.user_id user_id = payload.user_id
await TokenStorage.get(f"{user_id}-{code}") await TokenStorage.get(f"{user_id}-{token}")
with local_session() as session: with local_session() as session:
user = session.query(User).where(User.id == user_id).first() user = session.query(User).where(User.id == user_id).first()
session_token = await TokenStorage.create_session(user) session_token = await TokenStorage.create_session(user)
@ -58,7 +59,7 @@ async def confirm_email(_, _info, code):
return { return {
"token": session_token, "token": session_token,
"user": user, "user": user,
"news": await get_user_subscriptions(user.slug) "news": await user_subscriptions(user.slug)
} }
except InvalidToken as e: except InvalidToken as e:
raise InvalidToken(e.message) raise InvalidToken(e.message)
@ -70,10 +71,14 @@ async def confirm_email(_, _info, code):
async def confirm_email_handler(request): async def confirm_email_handler(request):
token = request.path_params["token"] # one time token = request.path_params["token"] # one time
request.session["token"] = token request.session["token"] = token
res = await confirm_email(None, token) res = await confirm_email(None, {}, token)
response = RedirectResponse(url="https://new.discours.io/confirm") # print('[resolvers.auth] confirm_email response: %r' % res)
response.set_cookie("token", res["token"]) # session token if "error" in res:
return response raise BaseHttpException(res['error'])
else:
response = RedirectResponse(url="https://new.discours.io/confirm")
response.set_cookie("token", res["token"]) # session token
return response
def create_user(user_dict): def create_user(user_dict):
@ -90,8 +95,8 @@ def generate_unique_slug(src):
slug = translit(src, "ru", reversed=True).replace(".", "-").lower() slug = translit(src, "ru", reversed=True).replace(".", "-").lower()
if slug != src: if slug != src:
print('[resolvers.auth] translited name: ' + slug) print('[resolvers.auth] translited name: ' + slug)
c = 1
with local_session() as session: with local_session() as session:
c = 1
user = session.query(User).where(User.slug == slug).first() user = session.query(User).where(User.slug == slug).first()
while user: while user:
user = session.query(User).where(User.slug == slug).first() user = session.query(User).where(User.slug == slug).first()
@ -111,7 +116,10 @@ async def register_by_email(_, _info, email: str, password: str = "", name: str
if user: if user:
raise OperationNotAllowed("User already exist") raise OperationNotAllowed("User already exist")
else: else:
slug = generate_unique_slug(name or email.split('@')[0]) slug = generate_unique_slug(name)
user = session.query(User).where(User.slug == slug).first()
if user:
slug = generate_unique_slug(email.split('@')[0])
user_dict = { user_dict = {
"email": email, "email": email,
"username": email, # will be used to store phone number or some messenger network id "username": email, # will be used to store phone number or some messenger network id
@ -139,7 +147,7 @@ async def auth_send_link(_, _info, email, lang="ru"):
@query.field("signIn") @query.field("signIn")
async def login(_, _info, email: str, password: str = "", lang: str = "ru"): async def login(_, info, email: str, password: str = "", lang: str = "ru"):
with local_session() as session: with local_session() as session:
orm_user = session.query(User).filter(User.email == email).first() orm_user = session.query(User).filter(User.email == email).first()
@ -168,7 +176,7 @@ async def login(_, _info, email: str, password: str = "", lang: str = "ru"):
return { return {
"token": session_token, "token": session_token,
"user": user, "user": user,
"news": await get_user_subscriptions(user.slug), "news": await user_subscriptions(user.slug),
} }
except InvalidPassword: except InvalidPassword:
print(f"[auth] {email}: invalid password") print(f"[auth] {email}: invalid password")
@ -179,7 +187,7 @@ async def login(_, _info, email: str, password: str = "", lang: str = "ru"):
@query.field("signOut") @query.field("signOut")
@login_required @login_required
async def sign_out(_, info: GraphQLResolveInfo): async def sign_out(_, info: GraphQLResolveInfo):
token = info.context["request"].headers[SESSION_TOKEN_HEADER] token = info.context["request"].headers.get("Auth", "")
status = await TokenStorage.revoke(token) status = await TokenStorage.revoke(token)
return status return status

View File

@ -118,7 +118,11 @@ def community_unfollow(user, slug):
@query.field("userFollowedCommunities") @query.field("userFollowedCommunities")
def get_followed_communities(_, user_slug) -> List[Community]: def get_followed_communities(_, _info, user_slug) -> List[Community]:
return followed_communities(user_slug)
def followed_communities(user_slug) -> List[Community]:
ccc = [] ccc = []
with local_session() as session: with local_session() as session:
ccc = ( ccc = (

View File

@ -10,21 +10,21 @@ from orm.reaction import Reaction
from orm.shout import Shout from orm.shout import Shout
from orm.topic import Topic, TopicFollower from orm.topic import Topic, TopicFollower
from orm.user import User, UserRole, Role, UserRating, AuthorFollower from orm.user import User, UserRole, Role, UserRating, AuthorFollower
from .community import get_followed_communities from .community import followed_communities
from .inbox import get_unread_counter from .inbox import get_unread_counter
from .reactions import get_reactions_for_shouts
from .topics import get_topic_stat from .topics import get_topic_stat
from services.auth.users import UserStorage from services.auth.users import UserStorage
from services.zine.shoutauthor import ShoutAuthorStorage from services.zine.shoutauthor import ShoutAuthorStorage
from services.stat.reacted import ReactedStorage
async def get_user_subscriptions(slug): async def user_subscriptions(slug: str):
return { return {
"unread": await get_unread_counter(slug), # unread inbox messages counter "unread": await get_unread_counter(slug), # unread inbox messages counter
"topics": [t.slug for t in await get_followed_topics(0, slug)], # followed topics slugs "topics": [t.slug for t in await followed_topics(slug)], # followed topics slugs
"authors": [a.slug for a in await get_followed_authors(0, slug)], # followed authors slugs "authors": [a.slug for a in await followed_authors(slug)], # followed authors slugs
"reactions": [r.shout for r in await get_reactions_for_shouts(0, [slug, ])], # followed reacted shout "reactions": len(await ReactedStorage.get_shout(slug)),
"communities": [c.slug for c in get_followed_communities(0, slug)], # followed communities slugs "communities": [c.slug for c in followed_communities(slug)], # communities
} }
@ -59,6 +59,10 @@ async def get_user_reacted_shouts(_, slug, offset, limit) -> List[Shout]:
@query.field("userFollowedTopics") @query.field("userFollowedTopics")
@login_required @login_required
async def get_followed_topics(_, info, slug) -> List[Topic]: async def get_followed_topics(_, info, slug) -> List[Topic]:
return await followed_topics(slug)
async def followed_topics(slug):
topics = [] topics = []
with local_session() as session: with local_session() as session:
topics = ( topics = (
@ -74,6 +78,10 @@ async def get_followed_topics(_, info, slug) -> List[Topic]:
@query.field("userFollowedAuthors") @query.field("userFollowedAuthors")
async def get_followed_authors(_, _info, slug) -> List[User]: async def get_followed_authors(_, _info, slug) -> List[User]:
return await followed_authors(slug)
async def followed_authors(slug) -> List[User]:
authors = [] authors = []
with local_session() as session: with local_session() as session:
authors = ( authors = (

View File

@ -124,6 +124,26 @@ async def delete_reaction(_, info, rid):
@query.field("reactionsForShouts") @query.field("reactionsForShouts")
async def get_reactions_for_shouts(_, info, shouts, offset, limit): async def get_reactions_for_shouts(_, info, shouts, offset, limit):
return await reactions_for_shouts(shouts, offset, limit)
async def reactions_for_shouts(shouts, offset, limit):
reactions = []
with local_session() as session:
for slug in shouts:
reactions += (
session.query(Reaction)
.filter(Reaction.shout == slug)
.where(Reaction.deletedAt.is_not(None))
.order_by(desc("createdAt"))
.offset(offset)
.limit(limit)
.all()
)
for r in reactions:
r.stat = await get_reaction_stat(r.id)
r.createdBy = await UserStorage.get_user(r.createdBy or "discours")
return reactions
reactions = [] reactions = []
with local_session() as session: with local_session() as session:
for slug in shouts: for slug in shouts:

View File

@ -13,7 +13,7 @@ if __name__ == "__main__":
("Access-Control-Allow-Origin", "http://localhost:3000"), ("Access-Control-Allow-Origin", "http://localhost:3000"),
( (
"Access-Control-Allow-Headers", "Access-Control-Allow-Headers",
"DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,auth", "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Auth",
), ),
("Access-Control-Expose-Headers", "Content-Length,Content-Range"), ("Access-Control-Expose-Headers", "Content-Length,Content-Range"),
("Access-Control-Allow-Credentials", "true"), ("Access-Control-Allow-Credentials", "true"),

View File

@ -1,15 +1,13 @@
from os import environ from os import environ
PORT = 8080 PORT = 8080
INBOX_SERVICE_PORT = 8081
DB_URL = ( DB_URL = (
environ.get("DATABASE_URL") or environ.get("DB_URL") or environ.get("DATABASE_URL") or environ.get("DB_URL") or
"postgresql://postgres@localhost:5432/discoursio" or "sqlite:///db.sqlite3" "postgresql://postgres@localhost:5432/discoursio" or "sqlite:///db.sqlite3"
) )
JWT_ALGORITHM = "HS256" JWT_ALGORITHM = "HS256"
JWT_SECRET_KEY = "8f1bd7696ffb482d8486dfbc6e7d16dd-secret-key" JWT_SECRET_KEY = environ.get("JWT_SECRET_KEY") or "8f1bd7696ffb482d8486dfbc6e7d16dd-secret-key"
SESSION_TOKEN_HEADER = "Auth"
SESSION_TOKEN_LIFE_SPAN = 24 * 60 * 60 # seconds SESSION_TOKEN_LIFE_SPAN = 24 * 60 * 60 # seconds
ONETIME_TOKEN_LIFE_SPAN = 1 * 60 * 60 # seconds ONETIME_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"