Coverage for mcpgateway/utils/verify_credentials.py: 95%
45 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-22 16:21 +0100
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-22 16:21 +0100
1# -*- coding: utf-8 -*-
2"""
4Copyright 2025
5SPDX-License-Identifier: Apache-2.0
6Authors: Mihai Criveti
8"""
10from typing import Optional
12import jwt
13from fastapi import Cookie, Depends, HTTPException, status
14from fastapi.security import (
15 HTTPAuthorizationCredentials,
16 HTTPBasic,
17 HTTPBasicCredentials,
18 HTTPBearer,
19)
20from fastapi.security.utils import get_authorization_scheme_param
21from jwt import PyJWTError
23from mcpgateway.config import settings
25basic_security = HTTPBasic(auto_error=False)
26security = HTTPBearer(auto_error=False)
29async def verify_jwt_token(token: str) -> dict:
30 """Verify and decode a JWT token.
32 Args:
33 token: The JWT token to verify.
35 Returns:
36 dict: The decoded token payload containing claims.
38 Raises:
39 HTTPException: If the token has expired or is invalid.
40 """
41 try:
42 # Decode and validate token
43 payload = jwt.decode(
44 token,
45 settings.jwt_secret_key,
46 algorithms=[settings.jwt_algorithm],
47 # options={"require": ["exp"]}, # Require expiration
48 )
49 return payload # Contains the claims (e.g., user info)
50 except jwt.ExpiredSignatureError:
51 raise HTTPException(
52 status_code=status.HTTP_401_UNAUTHORIZED,
53 detail="Token has expired",
54 headers={"WWW-Authenticate": "Bearer"},
55 )
56 except PyJWTError:
57 raise HTTPException(
58 status_code=status.HTTP_401_UNAUTHORIZED,
59 detail="Invalid token",
60 headers={"WWW-Authenticate": "Bearer"},
61 )
64async def verify_credentials(token: str) -> dict:
65 """Verify credentials using a JWT token.
67 This function uses verify_jwt_token internally which may raise exceptions.
69 Args:
70 token: The JWT token to verify.
72 Returns:
73 dict: The validated token payload with the original token added.
74 """
75 payload = await verify_jwt_token(token)
76 payload["token"] = token
77 return payload
80async def require_auth(credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), jwt_token: Optional[str] = Cookie(None)) -> str | dict:
81 """Require authentication via JWT token.
83 Checks for a JWT token either in the Authorization header or as a cookie.
85 Args:
86 credentials: HTTP Authorization credentials from the request header.
87 jwt_token: JWT token from cookies.
89 Returns:
90 str or dict: The verified credentials payload or "anonymous" if authentication is not required.
92 Raises:
93 HTTPException: If authentication is required but no valid token is provided.
94 """
95 token = credentials.credentials if credentials else jwt_token
97 if settings.auth_required and not token:
98 raise HTTPException(
99 status_code=status.HTTP_401_UNAUTHORIZED,
100 detail="Not authenticated",
101 headers={"WWW-Authenticate": "Bearer"},
102 )
103 return await verify_credentials(token) if token else "anonymous"
106async def verify_basic_credentials(credentials: HTTPBasicCredentials) -> str:
107 """Verify provided credentials.
109 Args:
110 credentials: HTTP Basic credentials.
112 Returns:
113 The username if credentials are valid.
115 Raises:
116 HTTPException: If credentials are invalid.
117 """
118 is_valid_user = credentials.username == settings.basic_auth_user
119 is_valid_pass = credentials.password == settings.basic_auth_password
121 if not (is_valid_user and is_valid_pass):
122 raise HTTPException(
123 status_code=status.HTTP_401_UNAUTHORIZED,
124 detail="Invalid credentials",
125 headers={"WWW-Authenticate": "Basic"},
126 )
127 return credentials.username
130async def require_basic_auth(credentials: HTTPBasicCredentials = Depends(basic_security)) -> str:
131 """Require valid authentication.
133 Args:
134 credentials: HTTP Basic credentials provided by the client.
136 Returns:
137 str: The authenticated username or "anonymous" if auth is not required.
139 Raises:
140 HTTPException: If authentication is required but no valid credentials are provided.
141 """
142 if settings.auth_required:
143 if not credentials: 143 ↛ 144line 143 didn't jump to line 144 because the condition on line 143 was never true
144 raise HTTPException(
145 status_code=status.HTTP_401_UNAUTHORIZED,
146 detail="Not authenticated",
147 headers={"WWW-Authenticate": "Basic"},
148 )
149 return await verify_basic_credentials(credentials)
150 return "anonymous"
153async def require_auth_override(
154 auth_header: str | None = None,
155 jwt_token: str | None = None,
156) -> str | dict:
157 """
158 Call :func:`require_auth` manually from middleware, without FastAPI
159 dependency injection.
161 Args:
162 auth_header: Raw ``Authorization`` header value
163 (e.g. ``"Bearer eyJhbGciOi..."``).
164 jwt_token: JWT taken from a cookie. If both header and cookie are
165 supplied, the header wins.
167 Returns:
168 str or dict: Whatever :func:`require_auth` returns
169 (decoded JWT payload or the string ``"anonymous"``).
171 Note:
172 This wrapper may propagate :class:`fastapi.HTTPException` raised by
173 :func:`require_auth`, but it does not raise anything on its own, so
174 we omit a formal *Raises* section to satisfy pydocstyle.
175 """
176 credentials = None
177 if auth_header:
178 scheme, param = get_authorization_scheme_param(auth_header)
179 if scheme.lower() == "bearer" and param: 179 ↛ 182line 179 didn't jump to line 182 because the condition on line 179 was always true
180 credentials = HTTPAuthorizationCredentials(scheme=scheme, credentials=param)
182 return await require_auth(credentials=credentials, jwt_token=jwt_token)