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.
- vibrant_auth_middleware_fastapi-0.1.0/PKG-INFO +171 -0
- vibrant_auth_middleware_fastapi-0.1.0/README.md +153 -0
- vibrant_auth_middleware_fastapi-0.1.0/pyproject.toml +33 -0
- vibrant_auth_middleware_fastapi-0.1.0/src/vibrant_auth_middleware/__init__.py +63 -0
- vibrant_auth_middleware_fastapi-0.1.0/src/vibrant_auth_middleware/config.py +41 -0
- vibrant_auth_middleware_fastapi-0.1.0/src/vibrant_auth_middleware/dependencies.py +234 -0
- vibrant_auth_middleware_fastapi-0.1.0/src/vibrant_auth_middleware/jwt_auth.py +364 -0
|
@@ -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)
|