parent
12a0af0862
commit
6802b216b7
6 changed files with 69 additions and 221 deletions
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue