Refine auth flow: server-provided reauth challenge for expired tokens.

Two rejection paths: expired-but-valid tokens get a ReauthChallenge
with a server-generated nonce (fast path, saves a round trip).
Invalid/corrupted tokens get plain Unauthenticated (client falls back
to full self-generated auth flow).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 00:40:26 -07:00
parent 66af104155
commit b7b1b27064

View File

@@ -198,29 +198,55 @@ service GardenSync {
2. Client attaches token as `x-sgard-auth-token` gRPC metadata 2. Client attaches token as `x-sgard-auth-token` gRPC metadata
3. Server verifies JWT signature and expiry 3. Server verifies JWT signature and expiry
4. If valid → request proceeds 4. If valid → request proceeds
5. If expired or invalid → server returns `Unauthenticated`
**Auto-renewal flow (transparent to user):** **Token rejection — two cases:**
1. Client sends request with cached token
2. Server rejects with `Unauthenticated` The server distinguishes between an expired-but-previously-valid token
3. Client interceptor catches the error and a completely invalid one:
4. Client calls `Authenticate` RPC with SSH signature
5. Server issues new JWT - **Expired token** (valid signature, known fingerprint still in
authorized_keys, but past expiry): server returns `Unauthenticated`
with a `ReauthChallenge` — a server-generated nonce embedded in the
error details. This is the fast path.
- **Invalid token** (bad signature, unknown fingerprint, corrupted):
server returns a plain `Unauthenticated` with no challenge. The client
falls back to the full Authenticate flow.
**Fast re-auth flow (expired token, transparent to user):**
1. Client sends request with expired token
2. Server returns `Unauthenticated` + `ReauthChallenge{nonce, timestamp}`
3. Client signs the server-provided nonce+timestamp with SSH key
4. Client calls `Authenticate` with the signature
5. Server verifies, issues new JWT
6. Client caches new token to disk 6. Client caches new token to disk
7. Client retries the original request with the new token 7. Client retries the original request with the new token
### SSH Key Signing (Fallback) This saves a round trip compared to full re-auth — the server provides
the nonce, so the client doesn't need to generate one and hope it's
accepted. The server controls the challenge, which also prevents any
client-side nonce reuse.
Used only during the `Authenticate` RPC. The client signs a challenge **Full auth flow (no valid token, transparent to user):**
payload to prove possession of an authorized SSH private key. 1. Client has no cached token or token is completely invalid
2. Client calls `Authenticate` with a self-generated nonce+timestamp,
signed with SSH key
3. Server verifies, issues JWT
4. Client caches token, proceeds with original request
### SSH Key Signing
Used during the `Authenticate` RPC to prove possession of an authorized
SSH private key. The challenge can come from the server (re-auth fast
path) or be generated by the client (initial auth).
**Challenge payload:** `nonce (32 random bytes) || timestamp (big-endian int64)` **Challenge payload:** `nonce (32 random bytes) || timestamp (big-endian int64)`
**Metadata fields on Authenticate RPC:** **Authenticate RPC request fields:**
- `x-sgard-auth-nonce` — base64-encoded 32-byte nonce - `nonce` — 32-byte nonce (from server's ReauthChallenge or client-generated)
- `x-sgard-auth-timestamp` — Unix seconds as decimal string - `timestamp` — Unix seconds
- `x-sgard-auth-signature`base64-encoded SSH signature - `signature`SSH signature over (nonce || timestamp)
- `x-sgard-auth-pubkey` — SSH public key in authorized_keys format - `public_key` — SSH public key in authorized_keys format
**Server verification:** **Server verification:**
- Parse public key, check fingerprint against `authorized_keys` file - Parse public key, check fingerprint against `authorized_keys` file