~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2

The import carries the web's OAuth client key

Merged
Merged chadtmiller.com asked to merge into feature/rust from grain.social/grain:web/hatk-sessions

What changes

The import carries the web's OAuth client key. grain-import copies hatk's _oauth_keys row pds-client-auth-key into oauth_key (V7, from the pull below this one), the private JWK as hatk kept it. grain-serve's web client loads it at start (Web::new takes the kept key before it would make one), so its JWKS at /oauth/client-jwks.json names the same key under the same key ID, and its client ID stays https://grain.social/oauth-client-metadata.json. The summary counts it as one more kept table.

That is the whole of it, in one commit to crates/grain-import (src/lib.rs and src/tests.rs), the only crate that knows the old database's layout; the branch is otherwise web/login with feature/rust merged in.

Why

_oauth_keys is the one table AGENTS.md says voids every session when it changes. Each PDS knows grain's client by its ID and this key, so keeping both keeps grain the same client to every PDS across the move.

Session tables: what carries and what does not

  • Carried: _oauth_keys.pds-client-auth-key, as oauth_key.
  • Left behind: _oauth_keys.appview-oauth-key (hatk's DPoP key for every PDS session it held, and its cookie key), _oauth_sessions (the web's PDS sessions), and hatk's OAuth issuer tables (the apps' grain-native://app logins to grain.social). The tables are listed in the summary's not-imported tables; appview-oauth-key is not, since _oauth_keys counts as read, and grain-import's docs name it as left behind.

Web sessions do not survive cutover: every web user logs in once more. An earlier head of this pull carried _oauth_sessions into web_session and traded hatk's __hatk_session cookie for grain's on first request. That trade reads hatk's cookie format at runtime, which the standing rule now rules out, and without it a carried session is unreachable (no browser holds a cookie naming it), so carrying them would only keep dead tokens. The apps' hatk-issued tokens end at cutover by decision.

How it was checked

grain-import's tests: the fixture's _oauth_keys has both of hatk's keys, with the client key a real P-256 JWK as WebCrypto exports it (key_ops, ext); the test checks that only pds-client-auth-key lands in oauth_key, the tally is 1 of 1, _oauth_keys is no longer in the not-imported list, and login::Web::new on the imported index serves a JWKS with that key's x under pds-client-auth-key. The rest is left to the checks.

Shortcomings

  • The import has no --public-url: nothing it carries depends on where the web is served.
  • If hatk's database has no pds-client-auth-key (it never served a web login), grain-serve makes one at first start, as it would on a new index.

Review

crates/grain-import/src/lib.rs (the oauth_key entry of KEPT and the docs above it) and its test. Stacked on web/login (pull 3mz7oyx7oiu3c); its own diff is the last commit.

🤖 Generated with Claude Code

Approved

This pull copies hatk's OAuth keys and web sessions into the index. It also adds login::carry_hatk, which trades hatk's sealed cookies for grain_session and grain_accounts. I read only the last commit's files (hatk.rs, login::carry_hatk, copy_sessions and thumbprint), not the stacked login commits. I didn't check the CI results.

I found nothing that must change.

  • Cookie opening: it uses HKDF over the sorted JWK, an IV of exactly 12 bytes, and a 30-day check against ts. The unit test uses cookies sealed by hatk's own encrypt, so the key derivation is checked against the real thing.
  • carry_hatk: it only acts when __hatk_session opens under the key and no grain_session is present. It clears hatk's cookies on every path, and it puts the carried cookies ahead of the browser's own so they are the ones read.
  • copy_sessions: it skips a row bound to another key or without a refresh token. The NOT NULL columns it reads as String are NOT NULL in hatk's schema. An unknown expiry reads as expired, which forces a refresh on first use.
  • Expiry units: expires_at is in seconds in grain::session::OAuth. hatk's fixture values (such as 1800000000) are in seconds too, so the units match. Check that against a real backup in the cutover rehearsal you already plan.
Approved

This round drops the session and cookie carrying from round 1 and keeps one thing: import.rs now copies hatk's _oauth_keys row pds-client-auth-key into oauth_key. I read only the last commit's diff to import.rs and its tests. I didn't check the CI results.

I found nothing that must change.

  • The copy: the KEPT entry's count and select are both filtered to kid = 'pds-client-auth-key', so the tally is 1 of 1 and appview-oauth-key stays behind.
  • The test: it checks that only the client key lands in oauth_key. It then opens login::Web::new on the imported index and checks the JWKS has the same kid and x, so the key survives the move in a form the web client loads.
  • Dropping the sessions: this fits the standing rule against reading hatk's cookie format at runtime. _oauth_sessions is still listed as left behind.
  • One wording gap: _oauth_keys no longer appears in the summary's not-imported list, even though appview-oauth-key is left. The pull body says "all listed in the summary's not-imported tables". The docs in import.rs do say the other keys stay behind, so this is only the body's wording.
Approved

This round is the same change as round 2, rebased onto web/login with feature/rust merged in. I read only the last commit's diff to grain-import's lib.rs and tests.rs, and I didn't check the CI results.

I found nothing that must change.

  • The copy: the oauth_key entry in KEPT filters both its count and its select to kid = 'pds-client-auth-key'. appview-oauth-key therefore stays behind, and the tally is 1 of 1.
  • The test: the fixture now has both keys, and the client key is a WebCrypto-style JWK. The test checks that only the client key lands in oauth_key. It then opens login::Web::new on the imported index and checks the JWKS has the same kid and x. _oauth_keys is correctly removed from the expected not-imported list.
  • Docs: the module docs now say the client key is kept and that hatk's other keys and sessions are left behind. This also covers the wording gap I noted in round 2.
Approved

This round keeps the same change as before: grain-import copies hatk's _oauth_keys row pds-client-auth-key into oauth_key. I read the last commit's diff against web/login. The checks are still queued, so I haven't seen them run.

I found nothing that must change.

  • The copy: the KEPT entry's count and select are both filtered to kid = 'pds-client-auth-key', so appview-oauth-key stays behind and the tally is 1 of 1. The kid and private_key columns match hatk's _oauth_keys schema in db/schema.sql.
  • The test: it checks that only the client key lands in oauth_key, byte for byte. It then opens Web::new on the imported index and checks the JWKS has the same kid and x. _oauth_keys is correctly removed from the expected not-imported list.
  • Docs: the module docs now match the pull body's account of what is kept and what is left behind.
Merged as 517cde0
Part of a stack of 2
  1. The import carries the web's OAuth client key Landed as 517cde0
  2. main