Skip to content

PPT-2536: token-endpoint + refresh-semantics specs (TK-03..08, RF-06..09) - #19

Merged
camreeves merged 1 commit into
masterfrom
PPT-2536-p1-token
Jul 31, 2026
Merged

PPT-2536: token-endpoint + refresh-semantics specs (TK-03..08, RF-06..09)#19
camreeves merged 1 commit into
masterfrom
PPT-2536-p1-token

Conversation

@camreeves

Copy link
Copy Markdown
Contributor

P1 coverage for the OAuth token endpoint and remaining refresh semantics. Test-only. 23 examples.

Every refresh success asserts the resulting access token's exact scope array and sub — the two claims whose loss caused both production reverts. Every negative is paired with a positive control so a refusal cannot pass for an unrelated reason (TK-06 mints two sibling codes differing only in redirect_uri).

Deliberate divergences now pinned

Rather than asserting RFC text and failing, these record what auth.cr actually does so a change surfaces as a decision:

  • client-auth failure → unauthorized_client (Doorkeeper: invalid_client; status and headers do match)
  • redirect_uri mismatch → invalid_redirect_uri (Doorkeeper: invalid_grant)
  • refresh with an unknown client_id → 400 invalid_scope, because Grant#token validates scope before authorization
  • client_credentials issues a refresh_token (RFC 6749 §4.4.3 says SHOULD NOT; Doorkeeper issued none) which redeems to an empty scope

Defect pinned as KNOWN GAP — must flip when fixed

A revoked refresh token is still redeemable. Authly::RefreshToken#validate_code! only checks that the JWT decodes; it never consults the token store, and the refresh jti is never persisted. So POST /auth/revoke writes revoked_at and the token keeps minting access tokens for its full 30-day TTL — revocation and logout do not end a session. Doorkeeper had no such gap (both tokens were columns of one row).

Two examples pin the current behaviour deliberately and must flip to 400 invalid_grant when this is closed, so the fix cannot land silently.

Independently found by two reviewers. Verified as upstream authly behaviour, not introduced by PR #10's override of that method. The fix is not trivial: naive enforcement breaks RF-05 (P0 — the SPA boot race double-submits its refresh token, and rotation already revokes the old jti), so it needs the refresh jti linked to the access-token row plus a grace window.

Also noted

refresh_hardening_spec.cr:110 wraps its only assertion in if result.status_code == 200, so it passes vacuously on rejection. RF-07 here supersedes it using the real client-credentials refresh token the endpoint actually issues.

195 examples, 0 failures, 1 pending.

🤖 Generated with Claude Code

…07/08, RF-06..09)

23 examples across two new files. Every refresh success asserts the resulting
access token's exact `scope` array AND `sub` — the two claims whose loss caused
both production reverts. Every negative is paired with a positive control so a
refusal cannot pass for an unrelated reason (e.g. TK-06 mints two sibling codes
that differ only in redirect_uri).

Pins several deliberate divergences from Ruby/Doorkeeper rather than asserting
the RFC text: client-auth failure returns unauthorized_client (Doorkeeper:
invalid_client — status and headers do match), redirect_uri mismatch returns
invalid_redirect_uri, refresh with an unknown client_id returns invalid_scope
because Grant#token validates scope before authorization, and client_credentials
issues a refresh_token that redeems to an empty scope.

Two examples are marked KNOWN GAP and pin current behaviour deliberately: a
revoked refresh token is still redeemable, because validate_code! never
consults the token store. They must flip to 400 invalid_grant when that is
fixed, so the fix cannot land silently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@camreeves
camreeves merged commit 3b1ffc4 into master Jul 31, 2026
6 of 7 checks passed
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.

1 participant