Skip to content

fix: a client without a secret cannot authenticate to token introspection (0.6.0-rc159) - #321

Merged
ciresnave-bot merged 1 commit into
mainfrom
fix/introspection-secretless-client
Oct 27, 2026
Merged

ciresnave-bot merged 1 commit into
mainfrom
fix/introspection-secretless-client

Conversation

@ciresnave-bot

@ciresnave-bot ciresnave-bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

/oauth/introspect read the stored client secret as client_data["client_secret"].as_str().unwrap_or("") and compared the presented secret with it, so a record with no secret, an empty one or a non-string one authenticated with an empty presented secret (Basic client_id: or POST client_secret=). RFC 7662 section 2.1: the endpoint MUST require authorization. A client without a secret now gets invalid_client (401). Confidential clients are unaffected, and so are clients created by POST /oauth/register, which always issues a secret. Reachable only for secret-less clients an operator wrote into storage (public clients); the caller still needs the token it asks about.

Stacked on #318 (it uses api::client_secret); merge #316 and #318 first.

Red first, in Docker (rust:latest, --features api-server,axum-integration, jobs=2): with only the new test file, a_client_without_a_secret_cannot_introspect fails on the old code and a_client_with_a_secret_still_introspects passes; with the fix both pass. Regression, same run: introspection_status_tests 4, oauth_advanced_tests 19, oauth2_client_secret_hash_tests 3, oauth2_code_grant_client_auth_tests 10 pass; cargo fmt --all -- --check clean. Not run: the rest of the suite, clippy. No existing test authenticated a secret-less client at introspection (checked by reading the seeds in the introspection tests).

Version 0.6.0-rc159 (PM-allocated); the 0.5.x backport (rc71) follows. GHSA (Low) and RustSec after the fixed versions are live.

🤖 Generated with Claude Code

https://claude.ai/code/session_013YEhSGGFctL3d4hGevi7tC

Summary by Sourcery

Require a real client secret for OAuth token introspection and release the change as 0.6.0-rc159.

Bug Fixes:

  • Prevent clients without a configured secret from authenticating to the OAuth token introspection endpoint with empty credentials, returning invalid_client as required by RFC 7662.

Enhancements:

  • Update the project, SDK, API documentation, and health metadata to version 0.6.0-rc159.

Tests:

  • Add regression coverage for secret-less clients being rejected through both Basic authentication and form credentials, while preserving valid confidential-client introspection.

@sourcery-ai

sourcery-ai Bot commented Oct 26, 2026

Copy link
Copy Markdown

Sorry @ciresnave-bot, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 2 days and 16 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Oct 26, 2026

Copy link
Copy Markdown

Reviewer's Guide

This release centralizes OAuth2 client-secret handling with Argon2id storage and legacy migration, closes secret-less client authentication at token introspection, and authenticates JARM JWE protected headers as AES-GCM AAD; the remaining changes synchronize the 0.6.0-rc159 release metadata.

Sequence diagram for secretless client introspection authorization

sequenceDiagram
    participant Client
    participant Introspect as /oauth/introspect
    participant Storage
    participant SecretAuth as client_secret

    Client->>Introspect: POST introspection with client_id and empty client_secret
    Introspect->>Storage: load oauth2_client record
    Storage-->>Introspect: client record without a secret
    Introspect->>SecretAuth: verify_client_credentials
    SecretAuth-->>Introspect: false
    Introspect-->>Client: invalid_client HTTP 401
Loading

Sequence diagram for hashed OAuth2 client-secret authentication

sequenceDiagram
    participant Client
    participant TokenEndpoint
    participant SecretAuth as client_secret
    participant Storage

    Client->>TokenEndpoint: Authenticate with client secret
    TokenEndpoint->>SecretAuth: authenticate
    alt Hashed secret
        SecretAuth-->>TokenEndpoint: Argon2id verification result
    else Legacy plaintext secret
        SecretAuth->>Storage: upgrade legacy client record
        Storage-->>SecretAuth: upgraded record with client_secret_hash
        SecretAuth-->>TokenEndpoint: authentication successful
    else No secret
        SecretAuth-->>TokenEndpoint: authentication failed
    end
    TokenEndpoint-->>Client: OAuth response
Loading

Sequence diagram for JARM JWE protected-header authentication

sequenceDiagram
    participant JARM
    participant Encryptor as AES256GCM
    participant Recipient

    JARM->>JARM: create_jwe_header
    JARM->>Encryptor: encrypt_payload with protected-header AAD
    Encryptor-->>JARM: ciphertext and authentication tag
    JARM-->>Recipient: JWE compact serialization
    Recipient->>Encryptor: decrypt with protected-header AAD
    Encryptor-->>Recipient: plaintext or decryption failure
Loading

File-Level Changes

Change Details Files
Centralize OAuth2 client-secret classification, verification, hashing, and legacy-record migration.
  • Added Argon2id hashing for newly registered secrets and verification for hashed credentials.
  • Retained legacy plaintext authentication with one-time upgrade while preserving record TTL.
  • Updated authorization-code, refresh-token, and public/confidential-client checks to use the shared secret logic.
src/api/client_secret.rs
src/api/oauth2.rs
src/api/mod.rs
tests/oauth2_client_secret_hash_tests.rs
Require actual client credentials for token introspection, rejecting secret-less clients with invalid_client.
  • Stopped treating missing, empty, or non-string stored secrets as an empty credential.
  • Covered Basic and POST authentication for public/secret-less clients and preserved confidential-client behavior.
src/api/oauth_advanced.rs
tests/introspection_secretless_client_tests.rs
CHANGELOG.md
Bind JARM JWE AES-GCM content encryption to the protected header as authenticated data.
  • Passed the encoded protected header into encryption and decryption.
  • Added tamper-detection and standards-interoperability tests for header AAD.
  • Updated decrypt helper signatures and explicitly preserves incompatibility with pre-fix JWEs.
src/server/oidc/oidc_advanced_jarm.rs
Release 0.6.0-rc159 and synchronize package, documentation, SDK, and lockfile metadata.
  • Updated workspace, SDK, API documentation, roadmap, changelog, and version consistency tests.
Cargo.toml
auth-storage-sqlite/Cargo.toml
Cargo.lock
docs/ROADMAP.md
docs/api/README.md
docs/api/openapi-bundled.yaml
docs/api/openapi-modular.yaml
docs/api/openapi.yaml
docs/api/schemas/health.yaml
sdks/javascript/package.json
sdks/javascript/package-lock.json
sdks/python/pyproject.toml
sdks/python/src/authframework/__init__.py
sdks/python/tests/member_versions_match_cargo.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

…tion (0.6.0-rc159)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013YEhSGGFctL3d4hGevi7tC
@ciresnave-bot
ciresnave-bot force-pushed the fix/introspection-secretless-client branch from c78580c to edfba52 Compare October 27, 2026 00:26
@ciresnave-bot
ciresnave-bot merged commit e54afe7 into main Oct 27, 2026
21 of 22 checks passed
@ciresnave-bot
ciresnave-bot deleted the fix/introspection-secretless-client branch October 27, 2026 01:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants