vibrant-auth-middleware-fastapi 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.4
2
+ Name: vibrant-auth-middleware-fastapi
3
+ Version: 0.1.0
4
+ Summary: FastAPI authentication middleware with JWT verification and Azure integration
5
+ Author-email: Vibrant Engineering <engineering@vibrant.com>
6
+ License: ISC
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: azure-appconfiguration<2.0.0,>=1.7.0
9
+ Requires-Dist: azure-identity<2.0.0,>=1.15.0
10
+ Requires-Dist: azure-keyvault-secrets<5.0.0,>=4.8.0
11
+ Requires-Dist: fastapi>=0.100.0
12
+ Requires-Dist: python-jose[cryptography]<4.0.0,>=3.3.0
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest-asyncio<0.20.0,>=0.19.0; extra == 'dev'
15
+ Requires-Dist: pytest<8.0.0,>=7.1.2; extra == 'dev'
16
+ Requires-Dist: ruff<1.0.0,>=0.9.0; extra == 'dev'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # Vibrant Auth Middleware for FastAPI
20
+
21
+ JWT authentication middleware with Azure integration for FastAPI applications.
22
+
23
+ ## Features
24
+
25
+ - JWT verification with automatic algorithm detection (HS256/RS256)
26
+ - Azure Key Vault integration for HS256 secrets
27
+ - Azure App Configuration integration for RS256 public keys
28
+ - FastAPI dependency injection support
29
+ - Cookie-based authentication support (access_token + token_type)
30
+ - Automatic fallback from Authorization header to cookies
31
+ - Caching for improved performance
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install vibrant-auth-middleware
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ ```python
42
+ from fastapi import FastAPI, Depends
43
+ from vibrant_auth_middleware import get_user_id
44
+
45
+ app = FastAPI()
46
+
47
+ @app.get("/protected")
48
+ def protected_route(user_id: str = Depends(get_user_id)):
49
+ return {"user_id": user_id}
50
+ ```
51
+
52
+ ## Configuration
53
+
54
+ Configure via environment variables:
55
+
56
+ ### HS256 (Symmetric Key)
57
+
58
+ ```bash
59
+ # Option 1: Direct secret
60
+ JWT_SECRET_KEY=your-secret-key
61
+
62
+ # Option 2: Azure Key Vault
63
+ AZURE_KEY_VAULT_URI=https://your-vault.vault.azure.net/
64
+ AZURE_KEY_VAULT_JWT_SECRET=jwt-secret-key # optional, default: "jwt-secret-key"
65
+ ```
66
+
67
+ ### RS256 (Asymmetric Key)
68
+
69
+ ```bash
70
+ # Option 1: Direct public key
71
+ JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
72
+
73
+ # Option 2: Azure App Configuration
74
+ AZURE_APP_CONFIG_ENDPOINT=https://your-config.azconfig.io
75
+ # or
76
+ AZURE_APP_CONFIG_CONNECTION_STRING=Endpoint=...
77
+ AZURE_APP_CONFIG_JWT_KEY=jwt-public-key # optional, default: "jwt-public-key"
78
+ ```
79
+
80
+ ### JWT Verification Options
81
+
82
+ ```bash
83
+ JWT_AUDIENCE=your-audience # optional
84
+ JWT_ISSUER=your-issuer # optional
85
+ JWT_LEEWAY=0 # optional, seconds for time validation
86
+ ```
87
+
88
+ ### Azure Authentication
89
+
90
+ ```bash
91
+ APP_ENV=production # Uses WorkloadIdentityCredential
92
+ APP_ENV=dev # Uses DefaultAzureCredential (default)
93
+ ```
94
+
95
+ ## API Reference
96
+
97
+ ### `get_user_id`
98
+
99
+ FastAPI dependency that extracts and validates user_id from JWT token. Supports both Authorization header and cookie-based authentication with automatic fallback.
100
+
101
+ **Authentication methods (in order of priority):**
102
+ 1. Authorization header: `Authorization: Bearer <token>`
103
+ 2. Cookies: `access_token` + `token_type` (token_type must be "Bearer")
104
+
105
+ ```python
106
+ @app.get("/me")
107
+ def get_me(user_id: str = Depends(get_user_id)):
108
+ return {"user_id": user_id}
109
+ ```
110
+
111
+ ### `get_user`
112
+
113
+ FastAPI dependency that extracts and validates the full JWT payload. Supports both Authorization header and cookie-based authentication with automatic fallback.
114
+
115
+ ```python
116
+ from vibrant_auth_middleware import get_user
117
+
118
+ @app.get("/me")
119
+ def get_me(user: dict = Depends(get_user)):
120
+ return {"user": user}
121
+ ```
122
+
123
+ ### `get_user_id_from_cookie`
124
+
125
+ FastAPI dependency that extracts and validates user_id exclusively from cookies. Requires both `access_token` and `token_type` cookies, where `token_type` must be "Bearer".
126
+
127
+ ```python
128
+ from vibrant_auth_middleware import get_user_id_from_cookie
129
+
130
+ @app.get("/me")
131
+ def get_me(user_id: str = Depends(get_user_id_from_cookie)):
132
+ return {"user_id": user_id}
133
+ ```
134
+
135
+ **Expected cookies:**
136
+ - `access_token`: The JWT token
137
+ - `token_type`: Must be "Bearer"
138
+
139
+ ### `verify_jwt_token`
140
+
141
+ Verify a JWT token and return its payload.
142
+
143
+ ```python
144
+ from vibrant_auth_middleware import verify_jwt_token
145
+
146
+ payload = verify_jwt_token(token)
147
+ ```
148
+
149
+ ### `get_user_id_from_token`
150
+
151
+ Extract user_id from a verified JWT token.
152
+
153
+ ```python
154
+ from vibrant_auth_middleware import get_user_id_from_token
155
+
156
+ user_id = get_user_id_from_token(token)
157
+ ```
158
+
159
+ ### `get_token_payload`
160
+
161
+ Get the full verified token payload.
162
+
163
+ ```python
164
+ from vibrant_auth_middleware import get_token_payload
165
+
166
+ payload = get_token_payload(token)
167
+ ```
168
+
169
+ ## License
170
+
171
+ ISC
@@ -0,0 +1,153 @@
1
+ # Vibrant Auth Middleware for FastAPI
2
+
3
+ JWT authentication middleware with Azure integration for FastAPI applications.
4
+
5
+ ## Features
6
+
7
+ - JWT verification with automatic algorithm detection (HS256/RS256)
8
+ - Azure Key Vault integration for HS256 secrets
9
+ - Azure App Configuration integration for RS256 public keys
10
+ - FastAPI dependency injection support
11
+ - Cookie-based authentication support (access_token + token_type)
12
+ - Automatic fallback from Authorization header to cookies
13
+ - Caching for improved performance
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install vibrant-auth-middleware
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```python
24
+ from fastapi import FastAPI, Depends
25
+ from vibrant_auth_middleware import get_user_id
26
+
27
+ app = FastAPI()
28
+
29
+ @app.get("/protected")
30
+ def protected_route(user_id: str = Depends(get_user_id)):
31
+ return {"user_id": user_id}
32
+ ```
33
+
34
+ ## Configuration
35
+
36
+ Configure via environment variables:
37
+
38
+ ### HS256 (Symmetric Key)
39
+
40
+ ```bash
41
+ # Option 1: Direct secret
42
+ JWT_SECRET_KEY=your-secret-key
43
+
44
+ # Option 2: Azure Key Vault
45
+ AZURE_KEY_VAULT_URI=https://your-vault.vault.azure.net/
46
+ AZURE_KEY_VAULT_JWT_SECRET=jwt-secret-key # optional, default: "jwt-secret-key"
47
+ ```
48
+
49
+ ### RS256 (Asymmetric Key)
50
+
51
+ ```bash
52
+ # Option 1: Direct public key
53
+ JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
54
+
55
+ # Option 2: Azure App Configuration
56
+ AZURE_APP_CONFIG_ENDPOINT=https://your-config.azconfig.io
57
+ # or
58
+ AZURE_APP_CONFIG_CONNECTION_STRING=Endpoint=...
59
+ AZURE_APP_CONFIG_JWT_KEY=jwt-public-key # optional, default: "jwt-public-key"
60
+ ```
61
+
62
+ ### JWT Verification Options
63
+
64
+ ```bash
65
+ JWT_AUDIENCE=your-audience # optional
66
+ JWT_ISSUER=your-issuer # optional
67
+ JWT_LEEWAY=0 # optional, seconds for time validation
68
+ ```
69
+
70
+ ### Azure Authentication
71
+
72
+ ```bash
73
+ APP_ENV=production # Uses WorkloadIdentityCredential
74
+ APP_ENV=dev # Uses DefaultAzureCredential (default)
75
+ ```
76
+
77
+ ## API Reference
78
+
79
+ ### `get_user_id`
80
+
81
+ FastAPI dependency that extracts and validates user_id from JWT token. Supports both Authorization header and cookie-based authentication with automatic fallback.
82
+
83
+ **Authentication methods (in order of priority):**
84
+ 1. Authorization header: `Authorization: Bearer <token>`
85
+ 2. Cookies: `access_token` + `token_type` (token_type must be "Bearer")
86
+
87
+ ```python
88
+ @app.get("/me")
89
+ def get_me(user_id: str = Depends(get_user_id)):
90
+ return {"user_id": user_id}
91
+ ```
92
+
93
+ ### `get_user`
94
+
95
+ FastAPI dependency that extracts and validates the full JWT payload. Supports both Authorization header and cookie-based authentication with automatic fallback.
96
+
97
+ ```python
98
+ from vibrant_auth_middleware import get_user
99
+
100
+ @app.get("/me")
101
+ def get_me(user: dict = Depends(get_user)):
102
+ return {"user": user}
103
+ ```
104
+
105
+ ### `get_user_id_from_cookie`
106
+
107
+ FastAPI dependency that extracts and validates user_id exclusively from cookies. Requires both `access_token` and `token_type` cookies, where `token_type` must be "Bearer".
108
+
109
+ ```python
110
+ from vibrant_auth_middleware import get_user_id_from_cookie
111
+
112
+ @app.get("/me")
113
+ def get_me(user_id: str = Depends(get_user_id_from_cookie)):
114
+ return {"user_id": user_id}
115
+ ```
116
+
117
+ **Expected cookies:**
118
+ - `access_token`: The JWT token
119
+ - `token_type`: Must be "Bearer"
120
+
121
+ ### `verify_jwt_token`
122
+
123
+ Verify a JWT token and return its payload.
124
+
125
+ ```python
126
+ from vibrant_auth_middleware import verify_jwt_token
127
+
128
+ payload = verify_jwt_token(token)
129
+ ```
130
+
131
+ ### `get_user_id_from_token`
132
+
133
+ Extract user_id from a verified JWT token.
134
+
135
+ ```python
136
+ from vibrant_auth_middleware import get_user_id_from_token
137
+
138
+ user_id = get_user_id_from_token(token)
139
+ ```
140
+
141
+ ### `get_token_payload`
142
+
143
+ Get the full verified token payload.
144
+
145
+ ```python
146
+ from vibrant_auth_middleware import get_token_payload
147
+
148
+ payload = get_token_payload(token)
149
+ ```
150
+
151
+ ## License
152
+
153
+ ISC
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "vibrant-auth-middleware-fastapi"
3
+ version = "0.1.0"
4
+ description = "FastAPI authentication middleware with JWT verification and Azure integration"
5
+ authors = [{ name = "Vibrant Engineering", email = "engineering@vibrant.com" }]
6
+ license = { text = "ISC" }
7
+ requires-python = ">=3.11"
8
+ readme = "README.md"
9
+ dependencies = [
10
+ "fastapi>=0.100.0",
11
+ "python-jose[cryptography]>=3.3.0,<4.0.0",
12
+ "azure-appconfiguration>=1.7.0,<2.0.0",
13
+ "azure-identity>=1.15.0,<2.0.0",
14
+ "azure-keyvault-secrets>=4.8.0,<5.0.0",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ dev = [
19
+ "pytest>=7.1.2,<8.0.0",
20
+ "ruff>=0.9.0,<1.0.0",
21
+ "pytest-asyncio>=0.19.0,<0.20.0",
22
+ ]
23
+
24
+ [build-system]
25
+ requires = ["hatchling>=1.18"]
26
+ build-backend = "hatchling.build"
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["src/vibrant_auth_middleware"]
30
+
31
+ [tool.ruff]
32
+ line-length = 100
33
+ target-version = "py311"
@@ -0,0 +1,63 @@
1
+ """
2
+ Vibrant Auth Middleware for FastAPI.
3
+
4
+ A library for JWT authentication with Azure integration, supporting both
5
+ HS256 (symmetric) and RS256 (asymmetric) algorithms.
6
+
7
+ Usage:
8
+ ```python
9
+ from fastapi import FastAPI, Depends
10
+ from vibrant_auth_middleware import get_user_id
11
+
12
+ app = FastAPI()
13
+
14
+ @app.get("/protected")
15
+ def protected_route(user_id: str = Depends(get_user_id)):
16
+ return {"user_id": user_id}
17
+ ```
18
+
19
+ Environment Variables:
20
+ JWT_SECRET_KEY: HS256 secret key (or use Azure Key Vault)
21
+ JWT_PUBLIC_KEY: RS256 public key (or use Azure App Configuration)
22
+ AZURE_KEY_VAULT_URI: Azure Key Vault URI for HS256 secret
23
+ AZURE_KEY_VAULT_JWT_SECRET: Secret name in Key Vault (default: "jwt-secret-key")
24
+ AZURE_APP_CONFIG_ENDPOINT: Azure App Configuration endpoint for RS256 public key
25
+ AZURE_APP_CONFIG_CONNECTION_STRING: App Configuration connection string
26
+ AZURE_APP_CONFIG_JWT_KEY: Key name in App Configuration (default: "jwt-public-key")
27
+ JWT_AUDIENCE: Optional audience claim for token verification
28
+ JWT_ISSUER: Optional issuer claim for token verification
29
+ JWT_LEEWAY: Leeway in seconds for time validation (default: 0)
30
+ APP_ENV: "production" for WorkloadIdentityCredential, else DefaultAzureCredential
31
+ """
32
+
33
+ from .config import JWTConfig, jwt_config
34
+ from .dependencies import (
35
+ OAUTH2_SCHEME,
36
+ OAuth2PasswordToken,
37
+ get_user,
38
+ get_user_id,
39
+ get_user_id_from_cookie,
40
+ )
41
+ from .jwt_auth import (
42
+ get_token_payload,
43
+ get_user_id_from_token,
44
+ verify_jwt_token,
45
+ )
46
+
47
+ __all__ = [
48
+ # Configuration
49
+ "JWTConfig",
50
+ "jwt_config",
51
+ # FastAPI dependencies
52
+ "get_user",
53
+ "get_user_id",
54
+ "get_user_id_from_cookie",
55
+ "OAUTH2_SCHEME",
56
+ "OAuth2PasswordToken",
57
+ # Core functions
58
+ "verify_jwt_token",
59
+ "get_user_id_from_token",
60
+ "get_token_payload",
61
+ ]
62
+
63
+ __version__ = "0.1.0"
@@ -0,0 +1,41 @@
1
+ """
2
+ Configuration for JWT authentication.
3
+
4
+ Loads settings from environment variables for JWT verification,
5
+ supporting both HS256 (symmetric) and RS256 (asymmetric) algorithms.
6
+ """
7
+
8
+ import os
9
+ from typing import Optional
10
+
11
+
12
+ class JWTConfig:
13
+ """JWT configuration settings loaded from environment variables."""
14
+
15
+ def __init__(self):
16
+ # HS256 Secret Key (from Azure Key Vault or env var)
17
+ self.jwt_secret_key: Optional[str] = os.getenv("JWT_SECRET_KEY")
18
+ self.azure_key_vault_secret_name: str = os.getenv(
19
+ "AZURE_KEY_VAULT_JWT_SECRET", "jwt-secret-key"
20
+ )
21
+ self.azure_key_vault_uri: Optional[str] = os.getenv("AZURE_KEY_VAULT_URI")
22
+
23
+ # RS256 Public Key (from Azure App Configuration or env var)
24
+ self.jwt_public_key: Optional[str] = os.getenv("JWT_PUBLIC_KEY")
25
+ self.azure_app_config_jwt_key: str = os.getenv(
26
+ "AZURE_APP_CONFIG_JWT_KEY", "jwt-public-key"
27
+ )
28
+ self.azure_app_config_endpoint: Optional[str] = os.getenv(
29
+ "AZURE_APP_CONFIG_ENDPOINT"
30
+ )
31
+ self.azure_app_config_connection_string: Optional[str] = os.getenv(
32
+ "AZURE_APP_CONFIG_CONNECTION_STRING"
33
+ )
34
+
35
+ # JWT verification options
36
+ self.jwt_audience: Optional[str] = os.getenv("JWT_AUDIENCE")
37
+ self.jwt_issuer: Optional[str] = os.getenv("JWT_ISSUER")
38
+ self.jwt_leeway: int = int(os.getenv("JWT_LEEWAY", "0"))
39
+
40
+
41
+ jwt_config = JWTConfig()
@@ -0,0 +1,234 @@
1
+ """
2
+ FastAPI dependencies for authentication.
3
+
4
+ Provides injectable dependencies for use with FastAPI's Depends system.
5
+ """
6
+
7
+ from typing import Any, Dict, Optional, cast
8
+
9
+ from fastapi import Depends, HTTPException, Request, status
10
+ from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
11
+ from fastapi.security import OAuth2
12
+ from fastapi.security.utils import get_authorization_scheme_param
13
+
14
+ from .jwt_auth import get_token_payload, get_user_id_from_token
15
+
16
+
17
+ class OAuth2PasswordToken(OAuth2):
18
+ """OAuth2 scheme for extracting Bearer token from Authorization header."""
19
+
20
+ def __init__(
21
+ self,
22
+ tokenUrl: str = "/auth/token",
23
+ scheme_name: Optional[str] = None,
24
+ scopes: Optional[dict] = None,
25
+ ):
26
+ if not scopes:
27
+ scopes = {}
28
+ flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes})
29
+ super().__init__(flows=flows, scheme_name=scheme_name, auto_error=False)
30
+
31
+ async def __call__(self, request: Request) -> Optional[str]:
32
+ authorization: str = request.headers.get("Authorization")
33
+ scheme, param = get_authorization_scheme_param(authorization)
34
+ if not authorization or scheme.lower() != "bearer":
35
+ return None
36
+ return cast(str, param)
37
+
38
+
39
+ OAUTH2_SCHEME = OAuth2PasswordToken(tokenUrl="/auth/token")
40
+
41
+
42
+ def _get_token_from_cookie(request: Request) -> Optional[str]:
43
+ """
44
+ Extract JWT token from cookies using access_token and token_type pair.
45
+
46
+ Looks for 'access_token' and 'token_type' cookies. If both are present
47
+ and token_type is 'Bearer', returns the access_token.
48
+
49
+ Args:
50
+ request: The incoming HTTP request.
51
+
52
+ Returns:
53
+ The access_token if valid cookie pair exists, None otherwise.
54
+ """
55
+ access_token = request.cookies.get("access_token")
56
+ token_type = request.cookies.get("token_type")
57
+
58
+ if access_token and token_type and token_type.lower() == "bearer":
59
+ return access_token
60
+
61
+ return None
62
+
63
+
64
+ async def get_user_id_from_cookie(request: Request) -> str:
65
+ """
66
+ Extract user_id from JWT token stored in cookies.
67
+
68
+ This dependency:
69
+ 1. Extracts access_token and token_type from cookies
70
+ 2. Validates that token_type is 'Bearer'
71
+ 3. Verifies the token signature using HS256 or RS256 (auto-detected)
72
+ 4. Extracts user_id from the verified token payload
73
+
74
+ Args:
75
+ request: The incoming HTTP request.
76
+
77
+ Returns:
78
+ The user_id as a string.
79
+
80
+ Raises:
81
+ HTTPException: If cookies are missing, invalid, or token lacks user_id.
82
+
83
+ Usage:
84
+ ```python
85
+ from fastapi import FastAPI, Depends
86
+ from vibrant_auth_middleware import get_user_id_from_cookie
87
+
88
+ app = FastAPI()
89
+
90
+ @app.get("/protected")
91
+ def protected_route(user_id: str = Depends(get_user_id_from_cookie)):
92
+ return {"user_id": user_id}
93
+ ```
94
+ """
95
+ token = _get_token_from_cookie(request)
96
+
97
+ if not token:
98
+ raise HTTPException(
99
+ status_code=status.HTTP_401_UNAUTHORIZED,
100
+ detail="Not authenticated: missing or invalid cookie credentials",
101
+ headers={"WWW-Authenticate": "Bearer"},
102
+ )
103
+
104
+ try:
105
+ return get_user_id_from_token(token)
106
+ except HTTPException:
107
+ raise
108
+ except Exception:
109
+ raise HTTPException(
110
+ status_code=status.HTTP_401_UNAUTHORIZED,
111
+ detail="Could not validate credentials",
112
+ headers={"WWW-Authenticate": "Bearer"},
113
+ )
114
+
115
+
116
+ async def get_user_id(
117
+ request: Request,
118
+ token: Optional[str] = Depends(OAUTH2_SCHEME),
119
+ ) -> str:
120
+ """
121
+ Extract user_id from verified JWT token in Authorization header or cookies.
122
+
123
+ This dependency:
124
+ 1. First tries to extract the Bearer token from the Authorization header
125
+ 2. Falls back to cookies (access_token + token_type pair) if header is missing
126
+ 3. Verifies the token signature using HS256 or RS256 (auto-detected)
127
+ 4. Extracts user_id from the verified token payload
128
+
129
+ Supports multiple user_id fields from ClinicUserPayload:
130
+ - user_id (string or number)
131
+ - userId (number, alternative field)
132
+ - internal_user_id (fallback field)
133
+
134
+ Args:
135
+ request: The incoming HTTP request.
136
+ token: Bearer token extracted from Authorization header (injected by Depends)
137
+
138
+ Returns:
139
+ The user_id as a string.
140
+
141
+ Raises:
142
+ HTTPException: If token is missing, invalid, or lacks user_id.
143
+
144
+ Usage:
145
+ ```python
146
+ from fastapi import FastAPI, Depends
147
+ from vibrant_auth_middleware import get_user_id
148
+
149
+ app = FastAPI()
150
+
151
+ @app.get("/protected")
152
+ def protected_route(user_id: str = Depends(get_user_id)):
153
+ return {"user_id": user_id}
154
+ ```
155
+ """
156
+ # Try Authorization header first, then fall back to cookies
157
+ if not token:
158
+ token = _get_token_from_cookie(request)
159
+
160
+ if not token:
161
+ raise HTTPException(
162
+ status_code=status.HTTP_401_UNAUTHORIZED,
163
+ detail="Not authenticated",
164
+ headers={"WWW-Authenticate": "Bearer"},
165
+ )
166
+
167
+ try:
168
+ return get_user_id_from_token(token)
169
+ except HTTPException:
170
+ raise
171
+ except Exception:
172
+ raise HTTPException(
173
+ status_code=status.HTTP_401_UNAUTHORIZED,
174
+ detail="Could not validate credentials",
175
+ headers={"WWW-Authenticate": "Bearer"},
176
+ )
177
+
178
+
179
+ async def get_user(
180
+ request: Request,
181
+ token: Optional[str] = Depends(OAUTH2_SCHEME),
182
+ ) -> Dict[str, Any]:
183
+ """
184
+ Extract full payload from verified JWT token in Authorization header or cookies.
185
+
186
+ This dependency:
187
+ 1. First tries to extract the Bearer token from the Authorization header
188
+ 2. Falls back to cookies (access_token + token_type pair) if header is missing
189
+ 3. Verifies the token signature using HS256 or RS256 (auto-detected)
190
+ 4. Returns the verified token payload
191
+
192
+ Args:
193
+ request: The incoming HTTP request.
194
+ token: Bearer token extracted from Authorization header (injected by Depends)
195
+
196
+ Returns:
197
+ The verified token payload as a dictionary.
198
+
199
+ Raises:
200
+ HTTPException: If token is missing or invalid.
201
+
202
+ Usage:
203
+ ```python
204
+ from fastapi import FastAPI, Depends
205
+ from vibrant_auth_middleware import get_user
206
+
207
+ app = FastAPI()
208
+
209
+ @app.get("/protected")
210
+ def protected_route(user: dict = Depends(get_user)):
211
+ return {"user": user}
212
+ ```
213
+ """
214
+ # Try Authorization header first, then fall back to cookies
215
+ if not token:
216
+ token = _get_token_from_cookie(request)
217
+
218
+ if not token:
219
+ raise HTTPException(
220
+ status_code=status.HTTP_401_UNAUTHORIZED,
221
+ detail="Not authenticated",
222
+ headers={"WWW-Authenticate": "Bearer"},
223
+ )
224
+
225
+ try:
226
+ return get_token_payload(token)
227
+ except HTTPException:
228
+ raise
229
+ except Exception:
230
+ raise HTTPException(
231
+ status_code=status.HTTP_401_UNAUTHORIZED,
232
+ detail="Could not validate credentials",
233
+ headers={"WWW-Authenticate": "Bearer"},
234
+ )
@@ -0,0 +1,364 @@
1
+ """
2
+ JWT Authentication Service.
3
+
4
+ Provides JWT token verification supporting both HS256 (symmetric) and RS256 (asymmetric) algorithms.
5
+ Automatically detects the algorithm from the JWT header and uses the appropriate verification method:
6
+ - HS256: Uses secret from Azure Key Vault or environment variable
7
+ - RS256: Uses public key from Azure App Configuration or environment variable
8
+ """
9
+
10
+ import logging
11
+ import os
12
+ import time
13
+ from typing import Any, Dict, Optional
14
+
15
+ from azure.appconfiguration import AzureAppConfigurationClient
16
+ from azure.core.exceptions import ResourceNotFoundError
17
+ from azure.identity import DefaultAzureCredential, WorkloadIdentityCredential
18
+ from fastapi import HTTPException, status
19
+ from jose import jwt
20
+ from jose.exceptions import ExpiredSignatureError, JWTError
21
+
22
+ from .config import jwt_config
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # Cache for keys and configuration
27
+ _jwt_secret_cache: Optional[str] = None
28
+ _jwt_public_key_cache: Optional[str] = None
29
+ _cache_timestamp: float = 0
30
+ CACHE_TTL = 300 # 5 minutes cache
31
+
32
+
33
+ def _get_azure_credential():
34
+ """
35
+ Get the appropriate Azure credential based on the environment.
36
+
37
+ Returns:
38
+ WorkloadIdentityCredential for production (Kubernetes/AKS),
39
+ DefaultAzureCredential for other environments (staging, dev).
40
+
41
+ Environment Variables:
42
+ APP_ENV: "production" uses WorkloadIdentityCredential,
43
+ "staging" or "dev" uses DefaultAzureCredential (default: "dev")
44
+ """
45
+ app_env = os.getenv("APP_ENV", "dev").lower()
46
+
47
+ if app_env == "production":
48
+ logger.debug(
49
+ f"Using WorkloadIdentityCredential for Azure authentication (APP_ENV={app_env})"
50
+ )
51
+ return WorkloadIdentityCredential()
52
+ else:
53
+ logger.debug(
54
+ f"Using DefaultAzureCredential for Azure authentication (APP_ENV={app_env})"
55
+ )
56
+ return DefaultAzureCredential()
57
+
58
+
59
+ def _get_azure_app_config_client() -> Optional[AzureAppConfigurationClient]:
60
+ """Create and return an Azure App Configuration client."""
61
+ connection_string = jwt_config.azure_app_config_connection_string
62
+ endpoint = jwt_config.azure_app_config_endpoint
63
+
64
+ if connection_string:
65
+ return AzureAppConfigurationClient.from_connection_string(connection_string)
66
+ elif endpoint:
67
+ credential = _get_azure_credential()
68
+ return AzureAppConfigurationClient(base_url=endpoint, credential=credential)
69
+
70
+ return None
71
+
72
+
73
+ def _get_hs256_secret() -> str:
74
+ """
75
+ Get HS256 secret key from Azure Key Vault or environment.
76
+
77
+ Returns:
78
+ The secret key for HS256 verification.
79
+
80
+ Raises:
81
+ HTTPException: If secret cannot be retrieved.
82
+ """
83
+ global _jwt_secret_cache, _cache_timestamp
84
+
85
+ current_time = time.time()
86
+ # Return cached secret if still valid
87
+ if _jwt_secret_cache and (current_time - _cache_timestamp) < CACHE_TTL:
88
+ logger.debug("Using cached HS256 secret")
89
+ return _jwt_secret_cache
90
+
91
+ # Try environment variable first
92
+ if jwt_config.jwt_secret_key:
93
+ logger.debug("Using HS256 secret from environment variable")
94
+ _jwt_secret_cache = jwt_config.jwt_secret_key
95
+ _cache_timestamp = current_time
96
+ return _jwt_secret_cache
97
+
98
+ # Try Azure Key Vault
99
+ if jwt_config.azure_key_vault_uri:
100
+ try:
101
+ from azure.keyvault.secrets import SecretClient
102
+
103
+ credential = _get_azure_credential()
104
+ client = SecretClient(
105
+ vault_url=jwt_config.azure_key_vault_uri, credential=credential
106
+ )
107
+
108
+ logger.debug(
109
+ f"Fetching HS256 secret from Azure Key Vault: {jwt_config.azure_key_vault_secret_name}"
110
+ )
111
+ secret = client.get_secret(jwt_config.azure_key_vault_secret_name)
112
+ _jwt_secret_cache = secret.value
113
+ _cache_timestamp = current_time
114
+ logger.info("Successfully retrieved HS256 secret from Azure Key Vault")
115
+ return _jwt_secret_cache
116
+
117
+ except Exception as e:
118
+ logger.error(f"Failed to retrieve secret from Azure Key Vault: {e}")
119
+
120
+ raise HTTPException(
121
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
122
+ detail="JWT secret not configured. Please set JWT_SECRET_KEY or configure Azure Key Vault.",
123
+ )
124
+
125
+
126
+ def _get_rs256_public_key() -> str:
127
+ """
128
+ Get RS256 public key from Azure App Configuration or environment.
129
+
130
+ Returns:
131
+ The public key for RS256 verification.
132
+
133
+ Raises:
134
+ HTTPException: If public key cannot be retrieved.
135
+ """
136
+ global _jwt_public_key_cache, _cache_timestamp
137
+
138
+ current_time = time.time()
139
+ # Return cached public key if still valid
140
+ if _jwt_public_key_cache and (current_time - _cache_timestamp) < CACHE_TTL:
141
+ logger.debug("Using cached RS256 public key")
142
+ return _jwt_public_key_cache
143
+
144
+ # Try environment variable first
145
+ if jwt_config.jwt_public_key:
146
+ logger.debug("Using RS256 public key from environment variable")
147
+ _jwt_public_key_cache = jwt_config.jwt_public_key
148
+ _cache_timestamp = current_time
149
+ return _jwt_public_key_cache
150
+
151
+ # Try Azure App Configuration
152
+ client = _get_azure_app_config_client()
153
+ if client:
154
+ try:
155
+ logger.debug(
156
+ f"Fetching RS256 public key from Azure App Configuration: {jwt_config.azure_app_config_jwt_key}"
157
+ )
158
+ item = client.get_configuration_setting(
159
+ key=jwt_config.azure_app_config_jwt_key
160
+ )
161
+
162
+ # Handle Key Vault references
163
+ if (
164
+ item.content_type
165
+ == "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8"
166
+ ):
167
+ logger.warning(
168
+ f"Key Vault reference found for {item.key}. Attempting to resolve..."
169
+ )
170
+ if item.value:
171
+ _jwt_public_key_cache = item.value
172
+ _cache_timestamp = current_time
173
+ logger.info(
174
+ "Successfully retrieved RS256 public key from Key Vault reference"
175
+ )
176
+ return _jwt_public_key_cache
177
+ elif item.value:
178
+ _jwt_public_key_cache = item.value
179
+ _cache_timestamp = current_time
180
+ logger.info(
181
+ "Successfully retrieved RS256 public key from Azure App Configuration"
182
+ )
183
+ return _jwt_public_key_cache
184
+
185
+ except ResourceNotFoundError:
186
+ logger.debug(
187
+ f"Public key not found in Azure App Configuration: {jwt_config.azure_app_config_jwt_key}"
188
+ )
189
+ except Exception as e:
190
+ logger.error(
191
+ f"Failed to retrieve public key from Azure App Configuration: {e}"
192
+ )
193
+
194
+ raise HTTPException(
195
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
196
+ detail="JWT public key not configured. Please set JWT_PUBLIC_KEY or configure Azure App Configuration.",
197
+ )
198
+
199
+
200
+ def _decode_key_with_pem_header(key: str, key_type: str) -> str:
201
+ """
202
+ Ensure the key has the proper PEM header/footer.
203
+
204
+ Args:
205
+ key: The key string.
206
+ key_type: Either 'PUBLIC' or 'PRIVATE'.
207
+
208
+ Returns:
209
+ The key with proper PEM formatting.
210
+ """
211
+ key = key.strip()
212
+ prefix = "-----BEGIN PUBLIC KEY-----"
213
+ suffix = "-----END PUBLIC KEY-----"
214
+
215
+ if key_type == "PUBLIC":
216
+ if not key.startswith("-----BEGIN"):
217
+ if "\\n" in key:
218
+ key = key.replace("\\n", "\n")
219
+ return f"{prefix}\n{key}\n{suffix}"
220
+ return key
221
+
222
+ return key
223
+
224
+
225
+ def verify_jwt_token(token: str) -> Dict[str, Any]:
226
+ """
227
+ Verify a JWT token and return its payload.
228
+
229
+ Automatically detects the algorithm (HS256 or RS256) from the token header
230
+ and uses the appropriate verification method.
231
+
232
+ Args:
233
+ token: The JWT token string (without 'Bearer ' prefix).
234
+
235
+ Returns:
236
+ The decoded token payload as a dictionary.
237
+
238
+ Raises:
239
+ HTTPException: If token is invalid, expired, or verification fails.
240
+ """
241
+ if not token:
242
+ raise HTTPException(
243
+ status_code=status.HTTP_401_UNAUTHORIZED,
244
+ detail="No token provided",
245
+ )
246
+
247
+ try:
248
+ # Get the header to determine the algorithm
249
+ header = jwt.get_unverified_header(token)
250
+ algorithm = header.get("alg", "HS256")
251
+
252
+ logger.debug(f"Verifying JWT token with algorithm: {algorithm}")
253
+
254
+ # Prepare decode options
255
+ decode_options = {
256
+ "verify_signature": True,
257
+ "verify_exp": True,
258
+ "verify_iat": True,
259
+ "leeway": jwt_config.jwt_leeway,
260
+ }
261
+
262
+ # Verify based on algorithm
263
+ if algorithm == "HS256":
264
+ secret = _get_hs256_secret()
265
+ payload = jwt.decode(
266
+ token,
267
+ secret,
268
+ algorithms=[algorithm],
269
+ options=decode_options,
270
+ audience=jwt_config.jwt_audience,
271
+ issuer=jwt_config.jwt_issuer,
272
+ )
273
+ elif algorithm == "RS256":
274
+ public_key = _get_rs256_public_key()
275
+ public_key = _decode_key_with_pem_header(public_key, "PUBLIC")
276
+ payload = jwt.decode(
277
+ token,
278
+ public_key,
279
+ algorithms=[algorithm],
280
+ options=decode_options,
281
+ audience=jwt_config.jwt_audience,
282
+ issuer=jwt_config.jwt_issuer,
283
+ )
284
+ else:
285
+ raise HTTPException(
286
+ status_code=status.HTTP_401_UNAUTHORIZED,
287
+ detail=f"Unsupported algorithm: {algorithm}",
288
+ )
289
+
290
+ logger.debug("JWT token verified successfully")
291
+ return payload
292
+
293
+ except ExpiredSignatureError:
294
+ raise HTTPException(
295
+ status_code=status.HTTP_401_UNAUTHORIZED,
296
+ detail="Token has expired",
297
+ )
298
+ except JWTError as e:
299
+ logger.warning(f"JWT verification failed: {e}")
300
+ raise HTTPException(
301
+ status_code=status.HTTP_401_UNAUTHORIZED,
302
+ detail=f"Invalid token: {str(e)}",
303
+ )
304
+ except HTTPException:
305
+ raise
306
+ except Exception as e:
307
+ logger.error(f"Unexpected error during JWT verification: {e}")
308
+ raise HTTPException(
309
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
310
+ detail="Token verification failed",
311
+ )
312
+
313
+
314
+ def get_user_id_from_token(token: str) -> str:
315
+ """
316
+ Extract user_id from a verified JWT token.
317
+
318
+ Checks multiple possible fields in the token payload:
319
+ - user_id (string or number)
320
+ - userId (number, alternative field)
321
+ - internal_user_id (fallback field)
322
+
323
+ Args:
324
+ token: The JWT token string (without 'Bearer ' prefix).
325
+
326
+ Returns:
327
+ The user_id as a string.
328
+
329
+ Raises:
330
+ HTTPException: If token is invalid or user_id cannot be extracted.
331
+ """
332
+ payload = verify_jwt_token(token)
333
+
334
+ # Try different fields for user_id based on ClinicUserPayload interface
335
+ user_id = (
336
+ payload.get("user_id")
337
+ or payload.get("userId")
338
+ or payload.get("internal_user_id")
339
+ )
340
+
341
+ if not user_id:
342
+ logger.warning("Token verified but no user_id found in payload")
343
+ raise HTTPException(
344
+ status_code=status.HTTP_401_UNAUTHORIZED,
345
+ detail="Token does not contain user_id",
346
+ )
347
+
348
+ return str(user_id)
349
+
350
+
351
+ def get_token_payload(token: str) -> Dict[str, Any]:
352
+ """
353
+ Get the full verified token payload.
354
+
355
+ Args:
356
+ token: The JWT token string (without 'Bearer ' prefix).
357
+
358
+ Returns:
359
+ The complete token payload as a dictionary.
360
+
361
+ Raises:
362
+ HTTPException: If token is invalid.
363
+ """
364
+ return verify_jwt_token(token)