diff --git a/api/auth/auth_jwt.py b/api/auth/auth_jwt.py index 41239590a..ddee4d3d5 100644 --- a/api/auth/auth_jwt.py +++ b/api/auth/auth_jwt.py @@ -9,7 +9,7 @@ from starlette import status from starlette.exceptions import HTTPException import schemas -from chalicelib.core import authorizers, users +from chalicelib.core import authorizers, users, spot logger = logging.getLogger(__name__) @@ -67,6 +67,42 @@ class JWTAuth(HTTPBearer): return _get_current_auth_context(request=request, jwt_payload=jwt_payload) + elif request.url.path in ["/spot/refresh", "/spot/api/refresh"]: + if "refreshToken" not in request.cookies: + logger.warning("Missing sopt-refreshToken cookie.") + jwt_payload = None + else: + jwt_payload = authorizers.jwt_refresh_authorizer(scheme="Bearer", token=request.cookies["refreshToken"]) + + if jwt_payload is None or jwt_payload.get("jti") is None: + logger.warning("Null spot-refreshToken's payload, or null JTI.") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, + detail="Invalid spot-refresh-token or expired refresh-token.") + auth_exists = spot.refresh_auth_exists(user_id=jwt_payload.get("userId", -1), + jwt_jti=jwt_payload["jti"]) + if not auth_exists: + logger.warning("spot-refreshToken's user not found.") + logger.warning(jwt_payload) + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, + detail="Invalid spot-refresh-token or expired refresh-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 spot-authentication scheme.") + old_jwt_payload = authorizers.jwt_authorizer(scheme=credentials.scheme, token=credentials.credentials, + leeway=datetime.timedelta( + days=config("JWT_LEEWAY_DAYS", cast=int, default=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 spot-token or expired token.") + + return _get_current_auth_context(request=request, jwt_payload=jwt_payload) + else: credentials: HTTPAuthorizationCredentials = await super(JWTAuth, self).__call__(request) if credentials: @@ -91,6 +127,11 @@ class JWTAuth(HTTPBearer): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid token or expired token.") + if jwt_payload.get("aud", "").startswith("spot") and not request.url.path.startswith("/spot"): + # Allow access to spot endpoints only + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, + detail="Unauthorized access (spot).") + return _get_current_auth_context(request=request, jwt_payload=jwt_payload) logger.warning("Invalid authorization code.") diff --git a/api/chalicelib/core/authorizers.py b/api/chalicelib/core/authorizers.py index 9073bb7fc..804b09663 100644 --- a/api/chalicelib/core/authorizers.py +++ b/api/chalicelib/core/authorizers.py @@ -2,16 +2,20 @@ import logging import jwt from decouple import config - +from typing import Optional from chalicelib.core import tenants -from chalicelib.core import users -from chalicelib.utils import helper +from chalicelib.core import users, spot + from chalicelib.utils.TimeUTC import TimeUTC logger = logging.getLogger(__name__) -def jwt_authorizer(scheme: str, token: str, leeway=0): +def get_supported_audience(): + return [users.AUDIENCE, spot.AUDIENCE] + + +def jwt_authorizer(scheme: str, token: str, leeway=0) -> Optional[dict]: if scheme.lower() != "bearer": return None try: @@ -19,7 +23,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()}",f"spot:{helper.get_stage_name()}"], + audience=get_supported_audience(), leeway=leeway ) except jwt.ExpiredSignatureError: @@ -40,7 +44,7 @@ def jwt_refresh_authorizer(scheme: str, token: str): token, config("JWT_REFRESH_SECRET"), algorithms=config("jwt_algorithm"), - audience=[f"front:{helper.get_stage_name()}"] + audience=get_supported_audience() ) except jwt.ExpiredSignatureError: logger.debug("! JWT-refresh Expired signature") @@ -52,18 +56,7 @@ def jwt_refresh_authorizer(scheme: str, token: str): return payload -def jwt_context(context): - user = users.get(user_id=context["userId"], tenant_id=context["tenantId"]) - if user is None: - return None - return { - "tenantId": context["tenantId"], - "userId": context["userId"], - **user - } - - -def generate_spot_jwt(user_id, tenant_id, iat, aud): +def generate_jwt(user_id, tenant_id, iat, aud): token = jwt.encode( payload={ "userId": user_id, @@ -79,7 +72,7 @@ def generate_spot_jwt(user_id, tenant_id, iat, aud): return token -def generate_spot_jwt_refresh(user_id, tenant_id, iat, aud, jwt_jti): +def generate_jwt_refresh(user_id, tenant_id, iat, aud, jwt_jti): token = jwt.encode( payload={ "userId": user_id, diff --git a/api/chalicelib/core/spot.py b/api/chalicelib/core/spot.py index 44db90151..bcf00a67f 100644 --- a/api/chalicelib/core/spot.py +++ b/api/chalicelib/core/spot.py @@ -4,6 +4,8 @@ from chalicelib.core import authorizers, users from chalicelib.utils import helper from chalicelib.utils import pg_client +AUDIENCE = "spot:OpenReplay" + def change_spot_jwt_iat_jti(user_id): with pg_client.PostgresClient() as cur: @@ -58,11 +60,11 @@ def authenticate(email, password) -> dict | None: r = helper.dict_to_camel_case(r) spot_jwt_iat, spot_jwt_r_jti, spot_jwt_r_iat = change_spot_jwt_iat_jti(user_id=r['userId']) return { - "jwt": authorizers.generate_spot_jwt(user_id=r['userId'], tenant_id=r['tenantId'], iat=spot_jwt_iat, - aud=f"spot:{helper.get_stage_name()}"), - "refreshToken": authorizers.generate_spot_jwt_refresh(user_id=r['userId'], tenant_id=r['tenantId'], - iat=spot_jwt_r_iat, aud=f"spot:{helper.get_stage_name()}", - jwt_jti=spot_jwt_r_jti), + "jwt": authorizers.generate_jwt(user_id=r['userId'], tenant_id=r['tenantId'], iat=spot_jwt_iat, + aud=AUDIENCE), + "refreshToken": authorizers.generate_jwt_refresh(user_id=r['userId'], tenant_id=r['tenantId'], + iat=spot_jwt_r_iat, aud=AUDIENCE, + jwt_jti=spot_jwt_r_jti), "refreshTokenMaxAge": config("JWT_REFRESH_EXPIRATION", cast=int), "email": email, **r @@ -77,10 +79,24 @@ def logout(user_id: int): def refresh(user_id: int, tenant_id: int = -1) -> dict: spot_jwt_iat, spot_jwt_r_jti, spot_jwt_r_iat = refresh_spot_jwt_iat_jti(user_id=user_id) return { - "jwt": authorizers.generate_spot_jwt(user_id=user_id, tenant_id=tenant_id, iat=spot_jwt_iat, - aud=f"spot:{helper.get_stage_name()}"), - "refreshToken": authorizers.generate_spot_jwt_refresh(user_id=user_id, tenant_id=tenant_id, iat=spot_jwt_r_iat, - aud=f"spot:{helper.get_stage_name()}", - jwt_jti=spot_jwt_r_jti), + "jwt": authorizers.generate_jwt(user_id=user_id, tenant_id=tenant_id, iat=spot_jwt_iat, + aud=AUDIENCE), + "refreshToken": authorizers.generate_jwt_refresh(user_id=user_id, tenant_id=tenant_id, iat=spot_jwt_r_iat, + aud=AUDIENCE, jwt_jti=spot_jwt_r_jti), "refreshTokenMaxAge": config("JWT_REFRESH_EXPIRATION", cast=int) - (spot_jwt_iat - spot_jwt_r_iat) } + + +def refresh_auth_exists(user_id, jwt_jti=None): + with pg_client.PostgresClient() as cur: + cur.execute( + cur.mogrify(f"""SELECT user_id + FROM public.users + WHERE user_id = %(userId)s + AND deleted_at IS NULL + AND spot_jwt_refresh_jti = %(jwt_jti)s + LIMIT 1;""", + {"userId": user_id, "jwt_jti": jwt_jti}) + ) + r = cur.fetchone() + return r is not None diff --git a/api/chalicelib/core/users.py b/api/chalicelib/core/users.py index d8711e4a6..865ac5022 100644 --- a/api/chalicelib/core/users.py +++ b/api/chalicelib/core/users.py @@ -12,6 +12,8 @@ from chalicelib.utils import helper from chalicelib.utils import pg_client from chalicelib.utils.TimeUTC import TimeUTC +AUDIENCE = "front:OpenReplay" + def __generate_invitation_token(): return secrets.token_urlsafe(64) @@ -611,11 +613,11 @@ def authenticate(email, password, for_change_password=False) -> dict | bool | No r = helper.dict_to_camel_case(r) jwt_iat, jwt_r_jti, jwt_r_iat = change_jwt_iat_jti(user_id=r['userId']) return { - "jwt": authorizers.generate_spot_jwt(user_id=r['userId'], tenant_id=r['tenantId'], iat=jwt_iat, - aud=f"front:{helper.get_stage_name()}"), - "refreshToken": authorizers.generate_spot_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), + "jwt": authorizers.generate_jwt(user_id=r['userId'], tenant_id=r['tenantId'], iat=jwt_iat, + aud=AUDIENCE), + "refreshToken": authorizers.generate_jwt_refresh(user_id=r['userId'], tenant_id=r['tenantId'], + iat=jwt_r_iat, aud=AUDIENCE, + jwt_jti=jwt_r_jti), "refreshTokenMaxAge": config("JWT_REFRESH_EXPIRATION", cast=int), "email": email, **r @@ -637,11 +639,10 @@ def logout(user_id: int): def refresh(user_id: int, tenant_id: int = -1) -> dict: jwt_iat, jwt_r_jti, jwt_r_iat = refresh_jwt_iat_jti(user_id=user_id) return { - "jwt": authorizers.generate_spot_jwt(user_id=user_id, tenant_id=tenant_id, iat=jwt_iat, - aud=f"front:{helper.get_stage_name()}"), - "refreshToken": authorizers.generate_spot_jwt_refresh(user_id=user_id, tenant_id=tenant_id, iat=jwt_r_iat, - aud=f"front:{helper.get_stage_name()}", - jwt_jti=jwt_r_jti), + "jwt": authorizers.generate_jwt(user_id=user_id, tenant_id=tenant_id, iat=jwt_iat, + aud=AUDIENCE), + "refreshToken": authorizers.generate_jwt_refresh(user_id=user_id, tenant_id=tenant_id, iat=jwt_r_iat, + aud=AUDIENCE, jwt_jti=jwt_r_jti), "refreshTokenMaxAge": config("JWT_REFRESH_EXPIRATION", cast=int) - (jwt_iat - jwt_r_iat) } diff --git a/api/chalicelib/utils/helper.py b/api/chalicelib/utils/helper.py index a3eb41f63..ba89a8529 100644 --- a/api/chalicelib/utils/helper.py +++ b/api/chalicelib/utils/helper.py @@ -11,10 +11,6 @@ import schemas from chalicelib.utils.TimeUTC import TimeUTC -def get_stage_name(): - return "OpenReplay" - - def random_string(length=36): return "".join(random.choices(string.hexdigits, k=length)) diff --git a/api/routers/subs/spot.py b/api/routers/subs/spot.py index 6158860f3..0ece2b260 100644 --- a/api/routers/subs/spot.py +++ b/api/routers/subs/spot.py @@ -3,7 +3,7 @@ from fastapi import HTTPException, status from starlette.responses import JSONResponse, Response import schemas -from chalicelib.core import spot +from chalicelib.core import spot, webhook from chalicelib.utils import captcha from chalicelib.utils import helper from or_dependencies import OR_context @@ -12,7 +12,7 @@ from routers.base import get_routers public_app, app, app_apikey = get_routers(prefix="/spot", tags=["spot"]) -@public_app.post('/login', tags=["authentication"]) +@public_app.post('/login') def login_spot(response: JSONResponse, data: schemas.UserLoginSchema = Body(...)): if helper.allow_captcha() and not captcha.is_valid(data.g_recaptcha_response): raise HTTPException( @@ -46,14 +46,14 @@ def login_spot(response: JSONResponse, data: schemas.UserLoginSchema = Body(...) return response -@app.get('/logout', tags=["login"]) +@app.get('/logout') def logout_spot(response: Response, context: schemas.CurrentContext = Depends(OR_context)): spot.logout(user_id=context.user_id) response.delete_cookie(key="refreshToken", path="/api/refresh") return {"data": "success"} -@app.get('/refresh', tags=["login"]) +@app.get('/refresh') def refresh_spot_login(context: schemas.CurrentContext = Depends(OR_context)): r = spot.refresh(user_id=context.user_id) content = {"jwt": r.get("jwt")} @@ -61,3 +61,8 @@ def refresh_spot_login(context: schemas.CurrentContext = Depends(OR_context)): response.set_cookie(key="refreshToken", value=r.get("refreshToken"), path="/api/refresh", max_age=r.pop("refreshTokenMaxAge"), secure=True, httponly=True) return response + + +@app.get('/integrations/slack/channels', tags=["integrations"]) +def get_slack_channels(context: schemas.CurrentContext = Depends(OR_context)): + return {"data": webhook.get_by_type(tenant_id=context.tenant_id, webhook_type=schemas.WebhookType.SLACK)}