Repository navigation
Store tokens and client secrets as digests (D1 of 3: read by digest, plaintext fallback) - #3192
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
Security findingsAdvisory findings (1)ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
🟡 Changes recommended
Token reveal caching and legacy-token digest healing need correction before approval.
2 open findings
What changed in this PR
Implements the first stage of digest-based credential storage in Fizzy’s OAuth stack while preserving compatibility with existing personal access tokens.
Changes:
- Adds keyed digests, indexed lookups, legacy-token healing, and credential migrations.
- Stores refresh-retry credentials encrypted and reveals new personal tokens only in creation responses.
- Updates deployment secrets, documentation, and regression tests.
[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or rungh pr ready --undo.
Click "Ready for review" or rungh pr readyto reengage.
| File | Description |
|---|---|
| test/models/token_digester_test.rb | Tests hashing, versions, and pepper requirements. |
| test/models/oauth/retired_refresh_token_test.rb | Tests encrypted successors and expiry. |
| test/models/oauth/client_test.rb | Verifies client-secret digests. |
| test/models/identity/access_token_test.rb | Tests digest storage, lookup, and healing. |
| test/migrations/backfill_token_digests_test.rb | Tests backfill safety and repeatability. |
| test/integration/oauth_revocation_test.rb | Updates revocation credential handling. |
| test/integration/oauth_refresh_replay_test.rb | Tests digest-based replay handling. |
| test/integration/oauth_grant_reach_test.rb | Uses in-memory bearer credentials. |
| test/integration/oauth_grant_idle_expiry_test.rb | Updates idle-expiry assertions for digests. |
| test/integration/oauth_flow_test.rb | Updates flows and tests secret authentication. |
| test/integration/oauth_delegate_resources_test.rb | Updates delegate-resource credentials. |
| test/integration/oauth_client_secret_basic_test.rb | Updates Basic authentication tests. |
| test/integration/oauth_availability_test.rb | Updates availability tests for digests. |
| test/fixtures/oauth/clients.yml | Provides a client-secret digest. |
| test/fixtures/identity/access_tokens.yml | Adds personal-token digests. |
| test/controllers/my/access_tokens_controller_test.rb | Tests reveal responses and cache headers. |
| saas/config/deploy.staging.yml | Exposes the staging pepper. |
| saas/config/deploy.production.yml | Exposes the production pepper. |
| saas/config/deploy.beta.yml | Exposes the beta pepper. |
| saas/.kamal/secrets.staging | Fetches the staging pepper. |
| saas/.kamal/secrets.production | Fetches the production pepper. |
| saas/.kamal/secrets.beta | Shares production’s pepper with beta. |
| docs/oauth/spec_conformance.md | Records digest-storage conformance decisions. |
| docs/kamal-deployment.md | Documents key-rotation consequences. |
| docs/docker-deployment.md | Explains independent pepper configuration. |
| db/schema.rb | Updates MySQL credential schema. |
| db/schema_sqlite.rb | Updates SQLite credential schema. |
| db/migrate/20261015120200_drop_plaintext_oauth_credentials.rb | Replaces plaintext OAuth storage. |
| db/migrate/20261015120100_backfill_token_digests.rb | Backfills existing credential digests. |
| db/migrate/20261015120000_add_token_digests.rb | Adds digest columns and indexes. |
| config/routes.rb | Removes token-reveal GET routing. |
| config/recurring.yml | Schedules successor-payload cleanup. |
| config/initializers/token_digester.rb | Validates the pepper at boot. |
| app/views/my/access_tokens/show.html.erb | Removes the previous reveal page. |
| app/views/my/access_tokens/new.html.erb | Adds the stream replacement target. |
| app/views/my/access_tokens/create.turbo_stream.erb | Renders the token through a stream. |
| app/views/my/access_tokens/create.html.erb | Adds the HTML creation response. |
| app/views/my/access_tokens/_reveal.html.erb | Shares token-reveal markup. |
| app/models/token_digester.rb | Implements versioned keyed digests. |
| app/models/oauth/retired_refresh_token.rb | Encrypts and expires retry credentials. |
| app/models/oauth/client.rb | Generates and verifies secret digests. |
| app/models/identity/access_token.rb | Implements digest lookup, healing, and rotation. |
| app/controllers/oauth/tokens_controller.rb | Uses digests and encrypted retry successors. |
| app/controllers/oauth/revocations_controller.rb | Finds revocable credentials by digest. |
| app/controllers/oauth/clients_controller.rb | Returns the in-memory registration secret. |
| app/controllers/my/access_tokens_controller.rb | Returns uncached, one-time reveal responses. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 124ce8da02
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
|
Review threads: 5 resolved (3 fixed, 2 declined with the reasoning in each thread).
Head 86e7fd7: CI green. OSS SQLite, OSS MySQL and SaaS MySQL suites also pass locally. The two new fallback tests failed on MySQL against the previous model. |
|
Codex Review: Didn't find any major issues. Delightful! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
86e7fd7 to
5cf35e0
Compare
ec809fe to
2fd0056
Compare
|
Restacked onto #3169's 2fd0056 (the train is rebased on main). Conflicts with #3163's new |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5cf35e0557
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
2fd0056 to
aef9fb5
Compare
5cf35e0 to
1ef16f3
Compare
aef9fb5 to
898baf5
Compare
|
@codex review |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0fb6c23a01
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Restack onto main for the crass 1.0.8 bump (#3226): merged main as da3f593, then 52d49bc and 0fb6c23 bring main's new tests and filters up to this branch.
CI is green on 0fb6c23. Held, not merged. |
successor_payload is encrypted at rest but decrypts to a live successor token pair, and logs, exports and error reports are what the redaction guards. oauth_retired_refresh_tokens joins TABLES, its digest columns' not-secret entries go, and a test pins the payload and both digests out of the logged insert, lookup and blanking update.
|
@codex review |
|
Codex Review: Didn't find any major issues. Delightful! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
There was a problem hiding this comment.
🛡️ Codex Security Review · Automatically triggered
Here are some automated security review suggestions for this pull request.
Reviewed commit: 0cfe381119
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
… stopping partway * origin/oauth-delegate-resources: Make this branch's migrations safe to run again after stopping partway Make the OAuth migrations safe to run again after stopping partway
MySQL commits each DDL statement on its own, so a migration of several statements that stops partway leaves the first ones applied and its version unrecorded, and running it again fails on the first one. Each step now skips itself when it's already done (if_not_exists, if_exists). The DDL they emit is unchanged, except where noted in the migration.
|
@codex review |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
* origin/oauth-delegate-resources: Answer 404 from a dark endpoint however many times it's asked # Conflicts: # app/controllers/oauth/clients_controller.rb # app/controllers/oauth/tokens_controller.rb
|
@codex review |
|
Codex Review: Didn't find any major issues. Bravo. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
* tp/oauth-delegate-resources: Name v0.2.0 in the pin's comment Pin fizzy-mcp-server v0.2.0 Refuse a join code that was reset while it was being redeemed Filter presigned storage signatures from logs Keep the reviewer's memberships in reach, and print the member audit escaped Import an export that lacks a column this database has only just gained List the reviewer accounts' members when signing the reviewer out Bump thruster from 0.1.26 to 0.1.27 (#3199) Give the asset build a placeholder direct-upload credential Anchor the stubbed storage host pattern Sign direct uploads with a write-only storage credential Lock the reviewer's email address, so a code holder can't move its membership out of revocation's reach Use Ruby's GC defaults and log job memory (#3238) # Conflicts: # config/application.rb # config/routes.rb # saas/.kamal/secrets.beta # saas/.kamal/secrets.production # saas/.kamal/secrets.staging # test/models/account/data_transfer/record_set_test.rb
* tp/oauth-delegate-resources: Bring main's tests up to this branch: Ruby 4 and the access token finder # Conflicts: # test/lib/rails_ext/active_record_sql_redaction_test.rb
* tp/oauth-delegate-resources: Pin the load balancers to kamal-proxy v0.10.2, and say why configure-lb-mcp.sh still checks Give the load balancer the real client IP
|
@codex review |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4fa5587fbf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
* tp/oauth-delegate-resources: Bring main's SQL redaction tests up to bearer lookup by find_honored Redact OAuth client statements from logged SQL, now that a client has a secret # Conflicts: # lib/rails_ext/active_record_sql_redaction.rb
* tp/oauth-delegate-resources: Redact retired refresh tokens from logged SQL # Conflicts: # lib/rails_ext/active_record_sql_redaction.rb
* tp/oauth-delegate-resources: Filter the code parameter by its whole name, so code_challenge stays readable
* tp/oauth-delegate-resources: Read a replayed refresh token's grant past the query cache


Warning
This PR contains migrations. Don't merge it directly. It has to go through the beta integration branch (
oauth-integration-2026-10-15) and be migrated by hand on beta before it lands. beta1 runs againstfizzy_production(saas/config/database.ymlendsbeta: *production), so migrating on beta1 is migrating production. The rows to add to that branch's migration inventory are below; this PR doesn't push to the integration branch.Before beta boots this code,
OAUTH_TOKEN_PEPPERmust exist in 1Password (account23QPQDKZC5BKBIIG7UGT5GR5RM, vault Deploy, item Fizzy) as two fields:Production/OAUTH_TOKEN_PEPPER(beta's secrets read this one too) andStaging/OAUTH_TOKEN_PEPPER. A SaaS deploy without it failskamal secrets fetch, and the app refuses to boot. See Deploy prerequisites.Stacked on #3169 (train: #2296 → #3080 → #3081 → #3082 → #3160 → #3161 → #3159 → #3163 → #3169 → this).
Implements step D1 of hashing tokens at rest. The plan has three deploys, so no token stops working at any point:
main. OAuth credentials are digest-only.tokentoignored_columns.identity_access_tokens.token.What D1 does
TokenDigester(app/models/token_digester.rb)OpenSSL::HMAC.hexdigest("SHA256", pepper, raw): lowercase hex, byte-for-byte what bc3'sOauth::TokenDigester#digestcomputes. A test pins it to the published HMAC-SHA256 vector."<n>$<hex>", and a bare digest is version 1. Lookups go throughTokenDigester.digests(raw), one digest per pepper version in service, so a future v2 can be added without a flag day. Only v1 exists today.OAUTH_TOKEN_PEPPER, named to mirror bc3'scredentials.oauth.token_pepper(personal access tokens are digested under it too; in Fizzy they're OAuth grants of a first-party client after Issue personal access tokens from a first-party client, and backfill existing ones onto it #3205). Fizzy keeps no encrypted credentials file, so every SaaS secret,SECRET_KEY_BASEincluded, comes from the environment through.kamal/secrets; reading credentials first would add a second source for one value, and self-hosted installs need the variable either way. Outside development and test it's required, and must be at least 32 characters. The initializer checks at boot, so a deploy missing it fails before it serves anything. There's no fallback to a derived key, which would digest with a different key than every stored token, as bc3'sverify_pepper!also refuses to. Asset precompilation (SECRET_KEY_BASE_DUMMY) skips the check.OAUTH_TOKEN_PEPPERwhen it's set. Otherwise it usesRails.application.key_generator.generate_key("token_digest_pepper", 32), a key derived fromSECRET_KEY_BASE.docs/docker-deployment.mdnow says so: on a self-hosted install, rotatingSECRET_KEY_BASEinvalidates every personal access token and connected app unlessOAUTH_TOKEN_PEPPERis set separately.identity_access_tokenstoken_digestandrefresh_token_digest(string, limit 80), each with a unique index. The indexes are created in the schema migration, before any digest is written, so a duplicate fails loudly and no lookup ever scans the table. Before this,tokenhad no index, and every Bearer request scanned all ~3,500 rows.BackfillTokenDigests).disable_ddl_transaction!.update_all, soupdated_at, the grant idle clock from Lapse OAuth grants left idle for 90 days #3161, doesn't move.downdoes nothing.db:migrate:redo:primary VERSION=20261015120100runs it again.tokenandtoken_digest, because productionmainstill readstokenwhile beta runs this.tokenstays NULL.raw_token,raw_refresh_token), on the record that issued or rotated them.find_by_raw_tokenlooks up the digest first. If that misses, it falls back topersonal.where(token_digest: nil).find_by(token: raw)and heals the row it finds: the digest goes to the primary, even when the lookup ran on the replica as GET requests do, viaupdate_columns, soupdated_atdoesn't move.with_reading_rolefailed on MySQL withActiveRecord::ReadOnlyErrorbefore the fix.fizzy_pat_tokens without digests on beta today. A plaintext match is confirmed byte for byte, since MySQL comparestokencase-insensitively, so a differently cased token neither authenticates nor installs a wrong digest.oauth.find_by_raw_tokennever matches a personal token.OAuth-only plaintext columns: replaced now
These have never held production data, since the OAuth tables and columns have never served production traffic, and OAuth has never shipped on
main, so no self-hosted install has them either. So they're replaced in this step rather than kept for two more deploys. That holds only while this PR ships in the same release as the rest of the OAuth stack (#2296 onward).identity_access_tokens.refresh_token→refresh_token_digest. The plaintext column itself stays until Drop the plaintext personal access token column (D3 of 3: drop identity_access_tokens.token) #3203 (D3) drops it besidetoken:Identity::AccessTokenignores it, and the backfill empties it. Productionmaincaches that table's columns and names every one on INSERT, so dropping it undermainwould fail its personal access token inserts until a restart (see the runbook notes).oauth_retired_refresh_tokensis rebuilt keyed by digest:refresh_token_digest,successor_refresh_token_digest, and an encryptedsuccessor_payload. Retired tokens exist only to recognize a replay, so they're dropped rather than converted. A replay of a token rotated before the migration is answered as an unknown token.oauth_clients.client_secret→client_secret_digest.Refresh replay grace with digests (
Oauth::RetiredRefreshToken). A retry within 60s used to answer from the grant's plaintext tokens. Now each rotation stores its successor pair in the retired row, the way bc3'srotate_to!storessuccessor_raw_token:MessageEncryptorkey derived fromsecret_key_base(saltoauth_refresh_token_successor, as in bc3).expires_in: GRACE, so it can't be decrypted after the window even if nothing blanks it.Oauth::RetiredRefreshToken.forget_successorsevery 5 minutes (config/recurring.yml). The sweep looks back one day, so it stays an index range over recent rows rather than a scan of 90 days of retired tokens.superseded?comparesrefresh_token_digestwithsuccessor_refresh_token_digest.invalid_grant"Refresh token no longer retryable", without revoking. That matches bc3's "Grace window token not available".Client secrets.
raw_client_secret).TokenDigester.verify, a constant-time compare of HMACs.has_secure_password :secret). Fizzy only issues generated secrets, 257 bits of randomness since Prefix newly issued access tokens, refresh tokens and personal access tokens #3163 (about 140 before it), so a deliberately slow hash adds no guessing resistance that the entropy doesn't already give, and one keyed digest covers every credential. The deviation is recorded indocs/oauth/spec_conformance.md. That doc's "Secrets stored in plaintext … Deliberate" line is now Fixed, and a Token format row records the digests.Personal access token: shown once
Creating a token used to redirect to
/my/access_tokens/<signed id>, which showed the raw token to anyone who loaded it within 10 seconds. Now:turbo_streamthat replaces the form, or an HTML 201 without Turbo.Cache-Control: no-store.showroute and action are gone.id,token,description,permission,created_at).Issuer without a trailing slash (
Oauth::BaseController#oauth_issuer). The metadataissuer, theissauthorization response parameter, the Basic challenge's realm and the protected resource'sauthorization_serverswereroot_url,https://app.fizzy.do/. Clients compare issuers as exact strings (RFC 8414 §3.3), bc3'scanonical_issuer_urlis the bare origin, and fizzy-mcp-server trims the slash from the issuer it's configured with, so its startup check would have refused this metadata. They're nowhttps://app.fizzy.do, still from the one method; the protected resource's ownresourceidentifier is unchanged. A test checksauthorization_serversis exactly the advertised issuer.Deploy prerequisites
OAUTH_TOKEN_PEPPERbefore anything running this code boots on beta, staging or production. Use at least 32 characters, e.g.bin/rails secret.Production/OAUTH_TOKEN_PEPPER(the Production section of the Fizzy item in the Deploy vault): read by production and by every beta (saas/.kamal/secrets.beta, whichsecrets.beta1–4link to, fetchesProduction/OAUTH_TOKEN_PEPPER). Beta and production must share it, because beta writes digests into production's database. Reading the one field from both, rather than copying it into the Beta section, makes sharing structural. Beta deployers already read this vault.Staging/OAUTH_TOKEN_PEPPER: staging has its own database, so it gets its own pepper.TokenDigester), not editing this one.kamal secrets fetchfails for that destination, and a container started without it refuses to boot (TokenDigester::MissingPepper).SECRET_KEY_BASEdiffers between beta and production (the Beta and Production sections). It only keys the 60-second successor payload, so a retry answered by the other deployment would get "no longer retryable" rather than revocation. They're different hosts, so in practice this never happens.Migration inventory rows for the integration branch
For
docs/oauth/integration-migrations.mdonoauth-integration-2026-10-15. This PR doesn't change that branch. These rows extend its table, and itsmigrate:up/downlists, after #7:20261015120000_add_token_digests.rbchange_table :identity_access_tokens, bulk: true:token_digest,refresh_token_digest(string 80), a unique index on each.add_column :oauth_clients, :client_secret_digest(string 80)identity_access_tokens: ~3,500 rows20261015120100_backfill_token_digests.rbtokens first. Then oneUPDATE … WHERE id = ?per row:token_digestfor every token (OAuth rows also gettokenNULLed),refresh_token_digest(andrefresh_tokenNULLed) andclient_secret_digestwhere those columns still exist.updated_atdoesn't move.disable_ddl_transaction!, batches of 500.downis a no-opidentity_access_tokens20261015120200_drop_plaintext_oauth_credentials.rboauth_clients.client_secret. Drop and recreateoauth_retired_refresh_tokenskeyed by digest, withsuccessor_payloadtext. Leavesidentity_access_tokensaloneoauth_*tables only, whichmaindoesn't modeloauth_*tables are empty in productionNotes for the runbook:
20261015120200,20261015120100(no-op),20261015120000. Undoing Bump rails from16d8b82to16f1222#10 restores an emptyclient_secretcolumn and an empty retired-token table, not the old values. Undoing Bump rails from16d8b82to4fa5681#8 drops the digests, and the personal access tokens still hold their plaintext.mainuses, so production needs no restart. Productionmainruns withpartial_insertsoff (load_defaults 8.1) and no schema cache dump, so a process names every column it has cached on INSERT. Droppingidentity_access_tokens.refresh_tokenin Bump rails from16d8b82to16f1222#10, as this PR first did, failed personal access token inserts from anymainprocess that cached the table between Bump rails from16d8b82to6045f25#3 and Bump rails from16d8b82to16f1222#10, until it restarted (reproduced on thelio). That column now goes in D3.test/migrations/drop_plaintext_oauth_credentials_test.rbcaches the table's columns the waymaindoes, runs Bump rails from16d8b82to16f1222#10 and creates a token with them; against the old Bump rails from16d8b82to16f1222#10 it fails withUnknown column 'refresh_token'.16d8b82to4fa5681#8 to Bump rails from16d8b82to16f1222#10 in the same session as Bump rails from16d8b82tode4d874#1 to Spike running PostgreSQL in app container #7.fizzy_pat_token written without a digest is never found. Only pre-D1 stack code writes those: the integration branch without this PR, deployed to beta. So merge this PR into the integration branch before its migrations run on beta1. If anything else wrote such tokens after the backfill, rerun it withbin/rails db:migrate:redo:primary VERSION=20261015120100.mainkeeps writing personal access tokens without digests until D1 reaches production. Each is digested the first time it's used, from beta or, after D1 lands, from production. D2 starts by rerunning the backfill for any never used.recurring.ymlruns only Solid Queue maintenance, soforget_successorsdoesn't run from beta. OAuth is dark there, and the payload expires cryptographically anyway.Verified on thelio against a dedicated MySQL 8.4 container (SaaS):
updated_ats were unchanged.redoof the backfill. Each dump differs from the committeddb/schema.rb(or, after down, from End an app's own plumbing with its grant; keep what it set up for the account #3169's) in column order only, physical rather than sorted, the same as the integration doc found for Bump rails from16d8b82tode4d874#1–Spike running PostgreSQL in app container #7.Tests
New:
test/models/token_digester_test.rb:verifyrefuses an unknown version, a malformed prefix, blanks, nil and non-strings.OAUTH_TOKEN_PEPPER(the name the deploy configs and docs give; any other would quietly fall back to the derived key).test/models/identity/access_token_test.rb:updated_at, and the next lookup is digest-only.token_digestandrefresh_token_digest.test/migrations/backfill_token_digests_test.rb:updated_atis untouched.test/models/oauth/retired_refresh_token_test.rb:forget_successorsblanks only past the window.superseded?compares by digest.OauthRefreshReplayTest: a retry inside the window whose pair is gone is refused without revoking. The existing Revoke the grant when a rotated refresh token is replayed, with a 60s retry grace #3160 tests (same pair at 59s, revoke at exactly 60s, superseded, lost race, and so on) pass unchanged in substance.OauthFlowTest: a DCR-registered secret is stored only as its digest and authenticates with Basic.Oauth::ClientTestchecks the digest on create and none for public clients.My::AccessTokensControllerTest:no-store.no-store, and its token matches the stored digest.TokenDigester.digest(...)).Existing tests now use
raw_tokenandraw_refresh_tokenfor values held in memory, and compare digests where they used to reread plaintext. Anassert_not exists?(token: …)would now pass vacuously, so each becameexists?(token_digest: TokenDigester.digest(…)).#3191 pins. Merged
test/pin-shipped-client-authinto this branch locally (not pushed) and ranShippedClientAuthenticationTestand the SaaSAdmin::AuditsControllerTest: all pass, in OSS (8 tests) and SaaS (17 tests, auditor pins included). (The two pins the integration doc lists as failing are against #3158, which isn't below this PR.)Suites (thelio, gated on exit status):
DATABASE_ADAPTER=mysql)These ran at
239292edd. The head adds only the PR number to the conformance doc. System tests weren't run locally; CI runs them.RuboCop, Brakeman and gitleaks are clean.
Not done here, on purpose
fizzy_cs_prefix yet. That's out of scope and still the follow-up Prefix newly issued access tokens, refresh tokens and personal access tokens #3163 names.mainreads onlytokenuntil D1 is deployed there.