Coverage for chalicelib/core/mcp/authorizers.py: 32%
55 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:56 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:56 +0000
1import logging
3import jwt
4from decouple import config
5from fastapi import HTTPException
7from chalicelib.utils import pg_client
8from chalicelib.utils.TimeUTC import TimeUTC
9from chalicelib.utils.log import sanitize
10from schemas import schemas, MCP
12logger = logging.getLogger(__name__)
14AUDIENCE = "mcp:OpenReplay"
15MCP_LOGIN_TIMEOUT_S = config("MCP_LOGIN_TIMEOUT_S", cast=int, default=0)
18def get_supported_audience():
19 return [AUDIENCE]
22def is_mcp_token(token: str) -> bool:
23 try:
24 if len(token) < 5 or "." not in token:
25 return False
26 decoded_token = jwt.decode(token, options={"verify_signature": False, "verify_exp": False})
27 audience = decoded_token.get("aud")
28 return audience == AUDIENCE
29 except jwt.InvalidTokenError:
30 logger.error(f"Invalid token for is_spot_token: {sanitize(token, max_length=16)}...")
31 raise
34def jwt_authorizer(scheme: str, token: str, leeway=0) -> dict | None:
35 if scheme.lower() != "bearer" or len(token) < 5:
36 return None
37 try:
38 payload = jwt.decode(jwt=token,
39 key=config("JWT_MCP_SECRET"),
40 algorithms=config("JWT_MCP_ALGORITHM"),
41 audience=get_supported_audience(),
42 leeway=leeway)
43 except jwt.ExpiredSignatureError:
44 logger.debug("! JWT Expired signature")
45 return None
46 except jwt.exceptions.InvalidSignatureError:
47 logger.warning("! JWT Signature verification failed")
48 return None
49 except BaseException as e:
50 logger.warning("! JWT Base Exception", exc_info=e)
51 return None
52 return payload
55def generate_jwt(user_id, tenant_id, iat, jti, client_id):
56 token = jwt.encode(
57 payload={
58 "userId": user_id,
59 "tenantId": tenant_id,
60 "exp": iat + config("JWT_MCP_EXPIRATION", cast=int),
61 "iss": config("JWT_ISSUER"),
62 "iat": iat,
63 "jti": jti,
64 "aud": AUDIENCE,
65 "clientId": client_id
66 },
67 key=config("JWT_MCP_SECRET"),
68 algorithm=config("JWT_MCP_ALGORITHM")
69 )
70 return token
73def store_token_request(data: MCP.AuthorizeSchema, cotext: schemas.CurrentContext):
74 with pg_client.PostgresClient() as cur:
75 query = cur.mogrify(
76 """
77 INSERT INTO public.mcp_authentication_tokens(user_id, client_id, state, iat)
78 VALUES (%(user_id)s, %(client_id)s, %(state)s,
79 timezone('utc'::text, now() - INTERVAL '10s')) ON CONFLICT DO NOTHING;""",
80 {"client_id": data.client_id, "state": data.state,
81 "user_id": cotext.user_id, },
82 )
83 cur.execute(query=query)
86def get_token_by_state(client_id, state):
87 with pg_client.PostgresClient() as cur:
88 query = cur.mogrify(
89 """
90 UPDATE public.mcp_authentication_tokens
91 SET generated= TRUE
92 WHERE client_id = %(client_id)s
93 AND state = %(state)s
94 AND NOT generated
95 RETURNING *,EXTRACT(epoch FROM iat)::BIGINT AS iat,
96 (SELECT tenant_id
97 FROM public.users
98 WHERE users.user_id = mcp_authentication_tokens.user_id
99 AND users.deleted_at IS NULL) AS tenant_id;""",
100 {"client_id": client_id, "state": state},
101 )
102 cur.execute(query=query)
103 row = cur.fetchone()
105 if row is None:
106 raise HTTPException(status_code=404, detail="State not found or token already generated")
107 if MCP_LOGIN_TIMEOUT_S > 0 and (row["iat"] + MCP_LOGIN_TIMEOUT_S) >= TimeUTC.now():
108 raise HTTPException(status_code=408, detail="Login timed out")
110 return generate_jwt(user_id=row["user_id"], tenant_id=row["tenant_id"], iat=row["iat"], client_id=client_id, jti=row["jti"])