Summary
Two authentication bypasses were identified in the middleware flow:
- A valid refresh JWT is accepted directly as a session bearer token because token purpose is not checked.
- A refresh-only request can execute a protected handler without DPoP proof because post-refresh DPoP enforcement is conditional on an original session token being present.
- Suggested severity: High
- CWE: CWE-287
- Audited commit:
6b126d355df8681218b78c198f9d334608946d90
Finding 1: refresh JWT accepted as session JWT
Authorization: Bearer is classified as a session token at descope/internal/auth/auth.go:45. Session validation performs generic JWT verification at auth.go:486 and auth.go:792, but never distinguishes drn=DS from drn=DSR.
The resulting identity reaches protected handlers through:
descope/sdk/middleware.go:18-22
descope/gin/api.go:13-17
Tests identify DSR as the refresh marker at descope/internal/auth/dpop_test.go:1034.
Impact: longer-lived refresh credentials can authenticate directly, bypassing refresh rotation, revocation, and policy checks.
Finding 2: DPoP skipped on refresh-only request
The refresh cookie is read at auth.go:66, validated/exchanged at auth.go:521, and the refreshed identity is allowed through middleware.
DPoP is enforced after refresh only when the original sessionToken != "" at auth.go:550. A caller with only a refresh cookie can omit both the session token and DPoP proof while still executing the protected handler.
Existing tests cover post-refresh DPoP only when an expired session token is present (dpop_test.go:1008), not the refresh-only case.
Recommended remediation
- Enforce token-purpose claims on every validation entry point.
- Always evaluate DPoP requirements against the final authenticated session token, including refresh-only requests.
- Add negative tests for refresh-as-bearer and refresh-only requests without proof.
Additional issue in the example application
The example registers /mgmt/audit/search and /mgmt/user/load outside the authenticated router (examples/webapp/main.go:98-101). The handlers expose audit records and arbitrary user profiles (main.go:568-589) when a management key is configured.
Summary
Two authentication bypasses were identified in the middleware flow:
6b126d355df8681218b78c198f9d334608946d90Finding 1: refresh JWT accepted as session JWT
Authorization: Beareris classified as a session token atdescope/internal/auth/auth.go:45. Session validation performs generic JWT verification atauth.go:486andauth.go:792, but never distinguishesdrn=DSfromdrn=DSR.The resulting identity reaches protected handlers through:
descope/sdk/middleware.go:18-22descope/gin/api.go:13-17Tests identify
DSRas the refresh marker atdescope/internal/auth/dpop_test.go:1034.Impact: longer-lived refresh credentials can authenticate directly, bypassing refresh rotation, revocation, and policy checks.
Finding 2: DPoP skipped on refresh-only request
The refresh cookie is read at
auth.go:66, validated/exchanged atauth.go:521, and the refreshed identity is allowed through middleware.DPoP is enforced after refresh only when the original
sessionToken != ""atauth.go:550. A caller with only a refresh cookie can omit both the session token and DPoP proof while still executing the protected handler.Existing tests cover post-refresh DPoP only when an expired session token is present (
dpop_test.go:1008), not the refresh-only case.Recommended remediation
Additional issue in the example application
The example registers
/mgmt/audit/searchand/mgmt/user/loadoutside the authenticated router (examples/webapp/main.go:98-101). The handlers expose audit records and arbitrary user profiles (main.go:568-589) when a management key is configured.