Skip to content

Security: refresh-token/session confusion and refresh-only DPoP bypass #804

Description

@omercnet

Summary

Two authentication bypasses were identified in the middleware flow:

  1. A valid refresh JWT is accepted directly as a session bearer token because token purpose is not checked.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinggoPull requests that update go codesecurity

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions