aminghadersohi opened a new pull request, #37972:
URL: https://github.com/apache/superset/pull/37972
### SUMMARY
Replaces the generic `"invalid_token"` JWT error response with specific,
actionable error messages for each validation failure type in the MCP service.
**The Problem:**
When JWT authentication fails, the MCP service returns the same generic
error regardless of what actually went wrong:
```json
{"error": "invalid_token", "error_description": "Authentication required"}
```
This makes debugging JWT configuration nearly impossible — you can't tell if
the token is expired, signed with the wrong algorithm, has the wrong issuer,
etc.
**The Solution:**
A `DetailedJWTVerifier` that performs step-by-step JWT validation and
returns the specific failure reason:
```json
{"error": "invalid_token", "error_description": "Algorithm mismatch: token
uses 'RS256', expected 'HS256'"}
{"error": "invalid_token", "error_description": "Token expired for client
'auth0|65b03fdcf56f87da5abb00ad'"}
{"error": "invalid_token", "error_description": "Issuer mismatch: token has
'wrong-issuer', expected 'http://manager.local.preset.zone'"}
```
#### How it works
```mermaid
flowchart TD
A["Client sends Bearer token"] --> B["AuthenticationMiddleware"]
B --> C["DetailedBearerAuthBackend.authenticate()"]
C --> D["DetailedJWTVerifier.verify_token()"]
D --> E["load_access_token() — step-by-step validation"]
E --> F{"1. Decode header"}
F -->|"Malformed"| FAIL1["Set reason: 'Malformed token header'"]
F -->|"OK"| G{"2. Check algorithm"}
G -->|"Mismatch"| FAIL2["Set reason: 'Algorithm mismatch: token uses X,
expected Y'"]
G -->|"OK"| H{"3. Get verification key"}
H -->|"JWKS unreachable"| FAIL3["Set reason: 'Failed to get verification
key'"]
H -->|"OK"| I{"4. Verify signature"}
I -->|"Bad signature"| FAIL4["Set reason: 'Signature verification
failed'"]
I -->|"OK"| J{"5. Check expiration"}
J -->|"Expired"| FAIL5["Set reason: 'Token expired for client X'"]
J -->|"OK"| K{"6. Validate issuer"}
K -->|"Mismatch"| FAIL6["Set reason: 'Issuer mismatch'"]
K -->|"OK"| L{"7. Validate audience"}
L -->|"Mismatch"| FAIL7["Set reason: 'Audience mismatch'"]
L -->|"OK"| M{"8. Check scopes"}
M -->|"Missing"| FAIL8["Set reason: 'Missing required scopes'"]
M -->|"OK"| SUCCESS["Return AccessToken ✅"]
FAIL1 & FAIL2 & FAIL3 & FAIL4 & FAIL5 & FAIL6 & FAIL7 & FAIL8 -->
CTX["Store reason in ContextVar"]
CTX --> RETURN_NONE["Return None"]
RETURN_NONE --> BACKEND["DetailedBearerAuthBackend reads ContextVar"]
BACKEND --> RAISE["Raise AuthenticationError with specific reason"]
RAISE --> HANDLER["JSON error handler → 401 with reason"]
```
#### Before vs After
```mermaid
flowchart LR
subgraph BEFORE["❌ Before — Base JWTVerifier"]
direction TB
B1["Any failure"] --> B2["Catch exception → return None"]
B2 --> B3["DEBUG log only"]
B3 --> B4["RequireAuthMiddleware"]
B4 --> B5["'invalid_token' — always the same"]
end
subgraph AFTER["✅ After — DetailedJWTVerifier"]
direction TB
A1["Step-by-step validation"]
A1 --> A2["Store specific reason in ContextVar"]
A2 --> A3["WARNING log with details"]
A3 --> A4["DetailedBearerAuthBackend"]
A4 --> A5["JSON 401 with exact reason"]
end
BEFORE ~~~ AFTER
```
#### Class hierarchy
```mermaid
classDiagram
class JWTVerifier {
+load_access_token(token) AccessToken|None
+get_middleware() list
-_get_verification_key(token) str
-_extract_scopes(claims) list
}
class DetailedJWTVerifier {
+load_access_token(token) AccessToken|None
+get_middleware() list
+_decode_token_header(token) dict
}
class BearerAuthBackend {
+authenticate(conn) tuple|None
}
class DetailedBearerAuthBackend {
+authenticate(conn) tuple|None
}
JWTVerifier <|-- DetailedJWTVerifier : extends
BearerAuthBackend <|-- DetailedBearerAuthBackend : extends
DetailedJWTVerifier ..> _jwt_failure_reason : writes
DetailedBearerAuthBackend ..> _jwt_failure_reason : reads
```
#### Auth factory fallback (server.py)
```mermaid
flowchart TD
START["server.py: run_server()"] --> CHECK_FACTORY{"MCP_AUTH_FACTORY
set?"}
CHECK_FACTORY -->|"Yes"| USE_CUSTOM["Use custom factory"]
CHECK_FACTORY -->|"No"| CHECK_ENABLED{"MCP_AUTH_ENABLED = True?"}
CHECK_ENABLED -->|"Yes"| USE_DEFAULT["Use
create_default_mcp_auth_factory()"]
CHECK_ENABLED -->|"No"| NO_AUTH["No auth provider"]
USE_DEFAULT --> DETAILED["Creates DetailedJWTVerifier"]
USE_CUSTOM --> PROVIDER["Auth provider ready"]
DETAILED --> PROVIDER
```
#### Files changed
| File | Change | Why |
|------|--------|-----|
| `superset/mcp_service/jwt_verifier.py` | **NEW** | `DetailedJWTVerifier`,
`DetailedBearerAuthBackend`, JSON error handler, ContextVar |
| `superset/mcp_service/mcp_config.py` | Modified |
`create_default_mcp_auth_factory()` now uses `DetailedJWTVerifier` |
| `superset/mcp_service/server.py` | Modified | Falls back to default
factory when `MCP_AUTH_ENABLED=True` but no `MCP_AUTH_FACTORY` |
| `superset/mcp_service/auth.py` | Modified | `get_user_from_request()`
shows diagnostic details on failure |
| `tests/unit_tests/mcp_service/test_jwt_verifier.py` | **NEW** | 21 unit
tests covering every failure mode |
### BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
**Before** — Every JWT failure returns this:
```
HTTP 401
{"error": "invalid_token", "error_description": "Authentication required"}
```
**After** — Each failure gets a specific message:
```
HTTP 401 (expired token)
{"error": "invalid_token", "error_description": "Token expired for client
'auth0|65b03fdcf56f87da5abb00ad'"}
HTTP 401 (wrong algorithm)
{"error": "invalid_token", "error_description": "Algorithm mismatch: token
uses 'RS256', expected 'HS256'"}
HTTP 401 (wrong issuer)
{"error": "invalid_token", "error_description": "Issuer mismatch: token has
'wrong-issuer', expected 'http://manager.local.preset.zone'"}
HTTP 401 (wrong audience)
{"error": "invalid_token", "error_description": "Audience mismatch: token
has 'wrong-aud', expected 'http://manager.local.preset.zone'"}
HTTP 401 (bad signature)
{"error": "invalid_token", "error_description": "Signature verification
failed"}
HTTP 401 (missing scopes)
{"error": "invalid_token", "error_description": "Missing required scopes:
{'admin'}. Token has: {'read'}"}
```
### TESTING INSTRUCTIONS
#### Prerequisites
- Running Superset instance with MCP service enabled
- `superset_config.py` with `MCP_AUTH_ENABLED = True` and JWT config (JWKS
URI or secret)
#### Quick validation (unit tests)
```bash
pytest tests/unit_tests/mcp_service/test_jwt_verifier.py -v
# Expected: 21 tests pass
```
#### Manual testing — expired token
1. Start MCP server: `superset mcp run --port 5008 --debug`
2. Send request with an expired JWT token:
```bash
curl -s http://localhost:5008/mcp \
-H "Authorization: Bearer <expired-token>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
```
3. **Expected**: Response contains `"Token expired for client '...'"` (not
generic `"invalid_token"`)
#### Manual testing — wrong algorithm
1. Generate an HS256 token when server expects RS256 (or vice versa)
2. Send request with that token
3. **Expected**: Response contains `"Algorithm mismatch: token uses 'HS256',
expected 'RS256'"`
#### Manual testing — wrong issuer
1. Generate a token with issuer `"wrong-issuer"` when server expects
`"http://manager.local.preset.zone"`
2. Send request with that token
3. **Expected**: Response contains `"Issuer mismatch: token has
'wrong-issuer', expected '...'"`
#### Manual testing — no auth header
1. Send request without Authorization header
2. **Expected**: Normal behavior (falls through to `MCP_DEV_USERNAME` if
configured)
#### Manual testing — default factory fallback
1. Remove `MCP_AUTH_FACTORY` from `superset_config.py`
2. Keep `MCP_AUTH_ENABLED = True` with JWT config keys (`MCP_JWKS_URI`, etc.)
3. Start MCP server
4. **Expected**: Server starts with `"Auth provider created from default
factory: DetailedJWTVerifier"` in logs
### ADDITIONAL INFORMATION
- [ ] Has associated issue:
- [ ] Required feature flags:
- [ ] Changes UI
- [ ] Includes DB Migration (follow approval process in
[SIP-59](https://github.com/apache/superset/issues/13351))
- [ ] Migration is atomic, supports rollback & is backwards-compatible
- [ ] Confirm DB migration upgrade and downgrade tested
- [ ] Runtime estimates and downtime expectations provided
- [x] Introduces new feature or API
- [ ] Removes existing feature or API
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]