Skip to content

fix: OAuth2 client secrets stored as Argon2id hashes, plain ones upgraded on first auth (0.6.0-rc158) - #318

Merged
ciresnave-bot merged 2 commits into
mainfrom
fix/client-secret-hash
Oct 26, 2026
Merged

ciresnave-bot merged 2 commits into
mainfrom
fix/client-secret-hash

Conversation

@ciresnave-bot

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

Copy link
Copy Markdown
Collaborator

Part of #90 (step A of the design the PM agreed). POST /oauth/register stored the client secret in plain text under oauth2_client:{id}, and the token, refresh, introspection and authorize paths compared against it. Now the record holds only client_secret_hash (Argon2id via utils::password, run on the blocking pool), the secret is still returned once, and a record that still holds a plain client_secret keeps authenticating and is rewritten with the hash on its first successful authentication (hash and plaintext removal are one write; the record keeps its time to live; a wrong secret changes nothing; a failed upgrade is logged without the secret and does not fail the authentication). New module src/api/client_secret.rs.

Red first, in Docker (rust:latest, --features api-server,axum-integration, jobs=2): with only the new tests added, both fail on the old code (registration stores plaintext; a plain record is not upgraded). With the change both pass. Regression, same run, no name filter: client_bound_token_tests 7, introspection_status_tests 4, oauth2_code_grant_client_auth_tests 10, oauth2_integration_test 13, oauth2_refresh_client_binding_tests 10, oauth2_scope_and_refresh_tests 8, oauth_advanced_tests 19, plus the 2 new: all pass (73 tests) (these seed plain-secret records, so they cover the legacy path). cargo fmt --all -- --check clean. Not run: the rest of the suite, clippy.

Behaviour kept as is on purpose: a record with no secret still accepts an empty secret at /oauth/introspect; that is its own fix and advisory (rc159). The other two registries in #90 are not touched here (steps B and C). Argon2id verification is deliberately costly (46 MiB of memory per verification, parameters from utils::password); I did not measure its time here. Introspection verifies the client secret on every call, so a deployment with heavy introspection traffic will pay that cost. Version 0.6.0-rc158 (PM-allocated); backport to 0.5.x (rc69) follows.

🤖 Generated with Claude Code

https://claude.ai/code/session_013YEhSGGFctL3d4hGevi7tC

Summary by Sourcery

Protect OAuth2 client secrets at rest with Argon2id hashing and transparently migrate legacy plaintext secrets on successful authentication.

New Features:

  • Store newly registered OAuth2 client secrets as Argon2id hashes while returning the secret only in the registration response.
  • Support transparent first-use migration of legacy plaintext client secrets to hashed storage across OAuth2 client-authentication flows.

Bug Fixes:

  • Prevent OAuth2 client secrets from remaining in plaintext in newly registered client records.
  • Ensure hashed-secret clients are treated as confidential clients during authorization, token, refresh, and introspection authentication.

Enhancements:

  • Centralize OAuth2 client-secret classification, verification, and legacy upgrade behavior in a dedicated API module while preserving record TTLs and authentication compatibility.

Build:

  • Bump the project, storage crate, and SDK versions to 0.6.0-rc158.

Documentation:

  • Document hashed OAuth2 client-secret storage, legacy migration behavior, and associated operational considerations in the changelog.

Tests:

  • Add coverage for hashed registration storage, successful and failed authentication, legacy-secret migration, TTL preservation, and token-endpoint confidentiality behavior.

@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

OAuth2 client registration now persists only Argon2id client-secret hashes, while all supported client-authentication paths verify hashes and transparently migrate legacy plaintext records on successful authentication without changing TTL or exposing secrets. New integration tests cover registration, rejection, migration, and TTL behavior, alongside the 0.6.0-rc158 metadata and documentation updates.

Sequence diagram for OAuth2 client-secret registration

sequenceDiagram
    participant Client
    participant Register as POST /oauth/register
    participant Password as utils::password
    participant Storage as AuthStorage

    Client->>Register: register_client()
    Register->>Password: hash_password_argon2id()
    Password-->>Register: client_secret_hash
    Register->>Storage: store_kv(oauth2_client:{id})
    Register-->>Client: client_id and client_secret once
Loading

Sequence diagram for legacy OAuth2 client-secret migration

sequenceDiagram
    participant Client
    participant Endpoint as OAuth2 authentication path
    participant Storage as AuthStorage
    participant Password as utils::password

    Client->>Endpoint: present client secret
    Endpoint->>Storage: load oauth2_client:{id}
    Storage-->>Endpoint: legacy client_secret record
    Endpoint->>Endpoint: constant_time_string_compare()
    alt secret matches
        Endpoint->>Password: hash_password_argon2id()
        Password-->>Endpoint: client_secret_hash
        Endpoint->>Storage: get_kv_ttl()
        Endpoint->>Storage: store_kv(hash and remove plaintext)
        Endpoint-->>Client: authentication succeeds
    else secret is wrong
        Endpoint-->>Client: authentication fails
    end
Loading

File-Level Changes

Change Details Files
Centralize OAuth2 client-secret storage, verification, and transparent legacy migration.
  • Hash newly generated secrets with Argon2id on Tokio's blocking pool before persistence.
  • Verify hashed secrets asynchronously and retain constant-time comparison for legacy plaintext records.
  • Upgrade a legacy record atomically after successful authentication, preserving its TTL and leaving failed authentication or upgrade errors non-destructive.
  • Use the shared helper for authorization-code, refresh-token, and introspection authentication, and classify hashed clients as confidential.
  • Preserve the existing empty-secret behavior for records without a configured secret.
src/api/client_secret.rs
src/api/mod.rs
src/api/oauth2.rs
src/api/oauth_advanced.rs
Add integration coverage for secure registration and backward-compatible secret migration.
  • Assert registration stores only an Argon2id hash while returning the plaintext secret once.
  • Exercise successful and unsuccessful authentication for hashed clients.
  • Verify wrong legacy credentials do not modify storage and successful credentials remove plaintext, preserve data and TTL, and continue working after migration.
tests/oauth2_client_secret_hash_tests.rs
Publish the release and synchronize project metadata for 0.6.0-rc158.
  • Document storage format, migration behavior, backup considerations, and introspection verification cost.
  • Update Rust, SDK, API documentation, roadmap, lockfile, and version consistency metadata.
CHANGELOG.md
Cargo.toml
Cargo.lock
auth-storage-sqlite/Cargo.toml
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
tests/member_versions_match_cargo.rs

Possibly linked issues


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

ciresnave and others added 2 commits October 26, 2026 14:43
…upgraded on first authentication (#90) (0.6.0-rc158)

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/client-secret-hash branch from 11710d9 to c69c182 Compare October 26, 2026 21:43
@ciresnave-bot
ciresnave-bot merged commit 897469a into main Oct 26, 2026
21 of 22 checks passed
@ciresnave-bot
ciresnave-bot deleted the fix/client-secret-hash branch October 26, 2026 22:50
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