diff --git a/api/auth/auth_jwt.py b/api/auth/auth_jwt.py index 1831a5d20..89d8f5bda 100644 --- a/api/auth/auth_jwt.py +++ b/api/auth/auth_jwt.py @@ -1,4 +1,3 @@ -import datetime from typing import Optional from fastapi import Request @@ -6,20 +5,8 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from starlette import status from starlette.exceptions import HTTPException -import schemas from chalicelib.core import authorizers, users - - -def _get_current_auth_context(request: Request, jwt_payload: dict) -> schemas.CurrentContext: - user = users.get(user_id=jwt_payload.get("userId", -1), tenant_id=jwt_payload.get("tenantId", -1)) - if user is None: - print("JWTAuth: User not found.") - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User not found.") - request.state.authorizer_identity = "jwt" - request.state.currentContext = schemas.CurrentContext(tenant_id=jwt_payload.get("tenantId", -1), - user_id=jwt_payload.get("userId", -1), - email=user["email"]) - return request.state.currentContext +import schemas class JWTAuth(HTTPBearer): @@ -27,60 +14,40 @@ class JWTAuth(HTTPBearer): super(JWTAuth, self).__init__(auto_error=auto_error) async def __call__(self, request: Request) -> Optional[schemas.CurrentContext]: - if request.url.path == "/refresh": - refresh_token = request.cookies.get("refreshToken") - jwt_payload = authorizers.jwt_refresh_authorizer(scheme="Bearer", token=refresh_token) - if jwt_payload is None or jwt_payload.get("jti") is None: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid token or expired token.") - auth_exists = users.auth_exists(user_id=jwt_payload.get("userId", -1), - tenant_id=jwt_payload.get("tenantId", -1), - jwt_iat=jwt_payload.get("iat", 100), - jwt_aud=jwt_payload.get("aud", ""), - jwt_jti=jwt_payload["jti"]) - if not auth_exists: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid token or expired token.") + credentials: HTTPAuthorizationCredentials = await super(JWTAuth, self).__call__(request) + if credentials: + if not credentials.scheme == "Bearer": + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid authentication scheme.") + jwt_payload = authorizers.jwt_authorizer(scheme=credentials.scheme, token=credentials.credentials) + auth_exists = jwt_payload is not None \ + and users.auth_exists(user_id=jwt_payload.get("userId", -1), + tenant_id=jwt_payload.get("tenantId", -1), + jwt_iat=jwt_payload.get("iat", 100), + jwt_aud=jwt_payload.get("aud", "")) + if jwt_payload is None \ + or jwt_payload.get("iat") is None or jwt_payload.get("aud") is None \ + or not auth_exists: + if jwt_payload is not None: + print(jwt_payload) + if jwt_payload.get("iat") is None: + print("JWTAuth: iat is None") + if jwt_payload.get("aud") is None: + print("JWTAuth: aud is None") + if not auth_exists: + print("JWTAuth: not users.auth_exists") - credentials: HTTPAuthorizationCredentials = await super(JWTAuth, self).__call__(request) - if credentials: - if not credentials.scheme == "Bearer": - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, - detail="Invalid authentication scheme.") - old_jwt_payload = authorizers.jwt_authorizer(scheme=credentials.scheme, token=credentials.credentials, - leeway=datetime.timedelta(days=3)) - if old_jwt_payload is None \ - or old_jwt_payload.get("userId") is None \ - or old_jwt_payload.get("userId") != jwt_payload.get("userId"): - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid token or expired token.") - - return _get_current_auth_context(request=request, jwt_payload=jwt_payload) + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid token or expired token.") + user = users.get(user_id=jwt_payload.get("userId", -1), tenant_id=jwt_payload.get("tenantId", -1)) + if user is None: + print("JWTAuth: User not found.") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User not found.") + jwt_payload["authorizer_identity"] = "jwt" + request.state.authorizer_identity = "jwt" + request.state.currentContext = schemas.CurrentContext(tenantId=jwt_payload.get("tenantId", -1), + userId=jwt_payload.get("userId", -1), + email=user["email"]) + return request.state.currentContext else: - credentials: HTTPAuthorizationCredentials = await super(JWTAuth, self).__call__(request) - if credentials: - if not credentials.scheme == "Bearer": - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, - detail="Invalid authentication scheme.") - jwt_payload = authorizers.jwt_authorizer(scheme=credentials.scheme, token=credentials.credentials) - auth_exists = jwt_payload is not None \ - and users.auth_exists(user_id=jwt_payload.get("userId", -1), - tenant_id=jwt_payload.get("tenantId", -1), - jwt_iat=jwt_payload.get("iat", 100), - jwt_aud=jwt_payload.get("aud", "")) - if jwt_payload is None \ - or jwt_payload.get("iat") is None or jwt_payload.get("aud") is None \ - or not auth_exists: - if jwt_payload is not None: - print(jwt_payload) - if jwt_payload.get("iat") is None: - print("JWTAuth: iat is None") - if jwt_payload.get("aud") is None: - print("JWTAuth: aud is None") - if not auth_exists: - print("JWTAuth: not users.auth_exists") - - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid token or expired token.") - - return _get_current_auth_context(request=request, jwt_payload=jwt_payload) - - print("JWTAuth: Invalid authorization code.") - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid authorization code.") + print("JWTAuth: Invalid authorization code.") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid authorization code.") diff --git a/api/chalicelib/core/authorizers.py b/api/chalicelib/core/authorizers.py index 04c3291ee..55b16d94f 100644 --- a/api/chalicelib/core/authorizers.py +++ b/api/chalicelib/core/authorizers.py @@ -6,7 +6,7 @@ from chalicelib.core import tenants from chalicelib.core import users -def jwt_authorizer(scheme: str, token: str, leeway=0): +def jwt_authorizer(scheme: str, token: str): if scheme.lower() != "bearer": return None try: @@ -14,8 +14,7 @@ def jwt_authorizer(scheme: str, token: str, leeway=0): token, config("jwt_secret"), algorithms=config("jwt_algorithm"), - audience=[f"front:{helper.get_stage_name()}"], - leeway=leeway + audience=[f"front:{helper.get_stage_name()}"] ) except jwt.ExpiredSignatureError: print("! JWT Expired signature") @@ -27,26 +26,6 @@ def jwt_authorizer(scheme: str, token: str, leeway=0): return payload -def jwt_refresh_authorizer(scheme: str, token: str): - if scheme.lower() != "bearer": - return None - try: - payload = jwt.decode( - token, - config("JWT_REFRESH_SECRET"), - algorithms=config("jwt_algorithm"), - audience=[f"front:{helper.get_stage_name()}"] - ) - except jwt.ExpiredSignatureError: - print("! JWT-refresh Expired signature") - return None - except BaseException as e: - print("! JWT-refresh Base Exception") - print(e) - return None - return payload - - def jwt_context(context): user = users.get(user_id=context["userId"], tenant_id=context["tenantId"]) if user is None: @@ -62,14 +41,10 @@ def get_jwt_exp(iat): return iat // 1000 + config("JWT_EXPIRATION", cast=int) + TimeUTC.get_utc_offset() // 1000 -def get_jwt_refresh_exp(iat): - return iat // 1000 + config("JWT_REFRESH_EXPIRATION", cast=int) + TimeUTC.get_utc_offset() // 1000 - - -def generate_jwt(user_id, tenant_id, iat, aud): +def generate_jwt(id, tenant_id, iat, aud): token = jwt.encode( payload={ - "userId": user_id, + "userId": id, "tenantId": tenant_id, "exp": get_jwt_exp(iat), "iss": config("JWT_ISSUER"), @@ -82,23 +57,6 @@ def generate_jwt(user_id, tenant_id, iat, aud): return token -def generate_jwt_refresh(user_id, tenant_id, iat, aud, jwt_jti): - token = jwt.encode( - payload={ - "userId": user_id, - "tenantId": tenant_id, - "exp": get_jwt_refresh_exp(iat), - "iss": config("JWT_ISSUER"), - "iat": iat // 1000, - "aud": aud, - "jti": jwt_jti - }, - key=config("JWT_REFRESH_SECRET"), - algorithm=config("jwt_algorithm") - ) - return token - - def api_key_authorizer(token): t = tenants.get_by_api_key(token) if t is not None: diff --git a/api/chalicelib/core/users.py b/api/chalicelib/core/users.py index b97882682..08ab84d9a 100644 --- a/api/chalicelib/core/users.py +++ b/api/chalicelib/core/users.py @@ -593,19 +593,16 @@ def get_by_invitation_token(token, pass_token=None): return helper.dict_to_camel_case(r) -def auth_exists(user_id, tenant_id, jwt_iat, jwt_aud, jwt_jti=None): - extra_condition = "" - if jwt_jti is not None: - extra_condition = " AND jwt_jti = %(jwt_jti)s" +def auth_exists(user_id, tenant_id, jwt_iat, jwt_aud): with pg_client.PostgresClient() as cur: cur.execute( - cur.mogrify(f"""SELECT user_id,jwt_iat - FROM public.users + cur.mogrify(f"""SELECT user_id,jwt_iat, changed_at + FROM public.users + INNER JOIN public.basic_authentication USING(user_id) WHERE user_id = %(userId)s - AND deleted_at IS NULL - {extra_condition} + AND deleted_at IS NULL LIMIT 1;""", - {"userId": user_id, "jwt_jti": jwt_jti}) + {"userId": user_id}) ) r = cur.fetchone() return r is not None \ @@ -613,21 +610,18 @@ def auth_exists(user_id, tenant_id, jwt_iat, jwt_aud, jwt_jti=None): and abs(jwt_iat - TimeUTC.datetime_to_timestamp(r["jwt_iat"]) // 1000) <= 1 -def change_jwt_iat_jti(user_id): +def change_jwt_iat(user_id): with pg_client.PostgresClient() as cur: query = cur.mogrify(f"""UPDATE public.users - SET jwt_iat = timezone('utc'::text, now()), - jwt_refresh_jti = coalesce(jwt_refresh_jti,0) + 1, - jwt_refresh_iat = coalesce(jwt_refresh_iat,timezone('utc'::text, now())) + SET jwt_iat = timezone('utc'::text, now()) WHERE user_id = %(user_id)s - RETURNING jwt_iat, jwt_refresh_jti, jwt_refresh_iat;""", + RETURNING jwt_iat;""", {"user_id": user_id}) cur.execute(query) - row = cur.fetchone() - return row.get("jwt_iat"), row.get("jwt_refresh_jti"), row.get("jwt_refresh_iat") + return cur.fetchone().get("jwt_iat") -def authenticate(email, password, for_change_password=False) -> dict | bool | None: +def authenticate(email, password, for_change_password=False) -> dict | None: with pg_client.PostgresClient() as cur: query = cur.mogrify( f"""SELECT @@ -652,65 +646,17 @@ def authenticate(email, password, for_change_password=False) -> dict | bool | No if for_change_password: return True r = helper.dict_to_camel_case(r) - jwt_iat, jwt_r_jti, jwt_r_iat = change_jwt_iat_jti(user_id=r['userId']) - jwt_iat = TimeUTC.datetime_to_timestamp(jwt_iat) - jwt_r_iat = TimeUTC.datetime_to_timestamp(jwt_r_iat) + jwt_iat = change_jwt_iat(r['userId']) + iat = TimeUTC.datetime_to_timestamp(jwt_iat) return { - "jwt": authorizers.generate_jwt(user_id=r['userId'], tenant_id=r['tenantId'], iat=jwt_iat, + "jwt": authorizers.generate_jwt(r['userId'], r['tenantId'], iat=iat, aud=f"front:{helper.get_stage_name()}"), - "refreshToken": authorizers.generate_jwt_refresh(user_id=r['userId'], tenant_id=r['tenantId'], - iat=jwt_r_iat, aud=f"front:{helper.get_stage_name()}", - jwt_jti=jwt_r_jti), - "refreshTokenMaxAge": config("JWT_REFRESH_EXPIRATION", cast=int), "email": email, **r } return None -def logout(user_id: int): - with pg_client.PostgresClient() as cur: - query = cur.mogrify( - """UPDATE public.users - SET jwt_iat = NULL, jwt_refresh_jti = NULL, jwt_refresh_iat = NULL - WHERE user_id = %(user_id)s;""", - {"user_id": user_id}) - cur.execute(query) - - -def refresh(user_id: int) -> dict: - with pg_client.PostgresClient() as cur: - query = cur.mogrify( - f"""SELECT - users.user_id, - 1 AS tenant_id, - users.jwt_refresh_jti, - users.jwt_refresh_iat, - FROM public.users - WHERE users.user_id = %(user_id)s - AND deleted_at IS NULL - LIMIT 1;""", - {"user_id": user_id}) - - cur.execute(query) - r = cur.fetchone() - - if r is not None: - r = helper.dict_to_camel_case(r) - jwt_iat, jwt_r_jti, jwt_r_iat = change_jwt_iat_jti(user_id=r['userId']) - jwt_iat = TimeUTC.datetime_to_timestamp(jwt_iat) - jwt_r_iat = TimeUTC.datetime_to_timestamp(jwt_r_iat) - return { - "jwt": authorizers.generate_jwt(user_id=r['userId'], tenant_id=r['tenantId'], iat=jwt_iat, - aud=f"front:{helper.get_stage_name()}"), - "refreshToken": authorizers.generate_jwt_refresh(user_id=r['userId'], tenant_id=r['tenantId'], - iat=jwt_r_iat, aud=f"front:{helper.get_stage_name()}", - jwt_jti=jwt_r_jti), - "refreshTokenMaxAge": config("JWT_REFRESH_EXPIRATION", cast=int) - } - return {} - - def get_user_role(tenant_id, user_id): with pg_client.PostgresClient() as cur: cur.execute( diff --git a/api/routers/core_dynamic.py b/api/routers/core_dynamic.py index 1d3fcd75b..d34244d68 100644 --- a/api/routers/core_dynamic.py +++ b/api/routers/core_dynamic.py @@ -3,7 +3,7 @@ from typing import Optional, Union from decouple import config from fastapi import Body, Depends, BackgroundTasks from fastapi import HTTPException, status -from starlette.responses import RedirectResponse, FileResponse, JSONResponse, Response +from starlette.responses import RedirectResponse, FileResponse import schemas from chalicelib.core import sessions, errors, errors_viewed, errors_favorite, sessions_assignments, heatmaps, \ @@ -38,7 +38,7 @@ if not tenants.tenants_exists(use_pool=False): @public_app.post('/login', tags=["authentication"]) -def login_user(response: JSONResponse, data: schemas.UserLoginSchema = Body(...)): +def login_user(data: schemas.UserLoginSchema = Body(...)): if helper.allow_captcha() and not captcha.is_valid(data.g_recaptcha_response): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -58,38 +58,21 @@ def login_user(response: JSONResponse, data: schemas.UserLoginSchema = Body(...) ) r["smtp"] = smtp.has_smtp() - refresh_token = r.pop("refreshToken") - refresh_token_max_age = r.pop("refreshTokenMaxAge") content = { 'jwt': r.pop('jwt'), 'data': { "user": r } } - response = JSONResponse(content=content) - response.set_cookie(key="refreshToken", value=refresh_token, - max_age=refresh_token_max_age, secure=True, httponly=True) - return response + + return content -@app.get('/logout', tags=["login"]) -def logout_user(response: Response, context: schemas.CurrentContext = Depends(OR_context)): - users.logout(user_id=context.user_id) - response.delete_cookie(key="refreshToken") +@app.get('/logout', tags=["login", "logout"]) +def logout_user(context: schemas.CurrentContext = Depends(OR_context)): return {"data": "success"} -@app.get('/refresh', tags=["login"]) -def refresh_login(context: schemas.CurrentContext = Depends(OR_context)): - r = users.refresh(user_id=context.user_id) - content = {"jwt": r.get("jwt")} - response = JSONResponse(content=content) - response.set_cookie(key="refreshToken", value=r.get("refreshToken"), - max_age=r.pop("refreshTokenMaxAge"), - secure=True, httponly=True) - return response - - @app.get('/account', tags=['accounts']) def get_account(context: schemas.CurrentContext = Depends(OR_context)): r = users.get(tenant_id=context.tenant_id, user_id=context.user_id) diff --git a/scripts/schema/db/init_dbs/postgresql/1.14.0/1.14.0.sql b/scripts/schema/db/init_dbs/postgresql/1.14.0/1.14.0.sql index 35002869a..7dba13f6a 100644 --- a/scripts/schema/db/init_dbs/postgresql/1.14.0/1.14.0.sql +++ b/scripts/schema/db/init_dbs/postgresql/1.14.0/1.14.0.sql @@ -67,10 +67,6 @@ CREATE TABLE IF NOT EXISTS public.sessions_feature_flags condition_id integer NULL REFERENCES feature_flags_conditions (condition_id) ON DELETE SET NULL ); -ALTER TABLE IF EXISTS public.users - ADD COLUMN IF NOT EXISTS jwt_refresh_jti integer NULL DEFAULT NULL, - ADD COLUMN IF NOT EXISTS jwt_refresh_iat timestamp without time zone NULL DEFAULT NULL; - COMMIT; \elif :is_next diff --git a/scripts/schema/db/init_dbs/postgresql/init_schema.sql b/scripts/schema/db/init_dbs/postgresql/init_schema.sql index e536d08fc..91b76a986 100644 --- a/scripts/schema/db/init_dbs/postgresql/init_schema.sql +++ b/scripts/schema/db/init_dbs/postgresql/init_schema.sql @@ -129,18 +129,16 @@ $$ CREATE TABLE users ( - user_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY, - email text NOT NULL UNIQUE, - role user_role NOT NULL DEFAULT 'member', - name text NOT NULL, - created_at timestamp without time zone NOT NULL DEFAULT (now() at time zone 'utc'), - deleted_at timestamp without time zone NULL DEFAULT NULL, - api_key text UNIQUE DEFAULT generate_api_key(20) NOT NULL, - jwt_iat timestamp without time zone NULL DEFAULT NULL, - jwt_refresh_jti integer NULL DEFAULT NULL, - jwt_refresh_iat timestamp without time zone NULL DEFAULT NULL, - data jsonb NOT NULL DEFAULT '{}'::jsonb, - weekly_report boolean NOT NULL DEFAULT TRUE + user_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY, + email text NOT NULL UNIQUE, + role user_role NOT NULL DEFAULT 'member', + name text NOT NULL, + created_at timestamp without time zone NOT NULL DEFAULT (now() at time zone 'utc'), + deleted_at timestamp without time zone NULL DEFAULT NULL, + api_key text UNIQUE DEFAULT generate_api_key(20) NOT NULL, + jwt_iat timestamp without time zone NULL DEFAULT NULL, + data jsonb NOT NULL DEFAULT '{}'::jsonb, + weekly_report boolean NOT NULL DEFAULT TRUE ); CREATE TABLE basic_authentication