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