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
3. Server verifies JWT signature and expiry
4. If valid → request proceeds
5. If expired or invalid → server returns `Unauthenticated`
**Auto-renewal flow (transparent to user):**
1. Client sends request with cached token
2. Server rejects with `Unauthenticated`
3. Client interceptor catches the error
4. Client calls `Authenticate` RPC with SSH signature
5. Server issues new JWT
**Token rejection — two cases:**
The server distinguishes between an expired-but-previously-valid token
and a completely invalid one:
- **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
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
payload to prove possession of an authorized SSH private key.
**Full auth flow (no valid token, transparent to user):**
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)`
**Metadata fields on Authenticate RPC:**
- `x-sgard-auth-nonce` — base64-encoded 32-byte nonce
- `x-sgard-auth-timestamp` — Unix seconds as decimal string
- `x-sgard-auth-signature`base64-encoded SSH signature
- `x-sgard-auth-pubkey` — SSH public key in authorized_keys format
**Authenticate RPC request fields:**
- `nonce` — 32-byte nonce (from server's ReauthChallenge or client-generated)
- `timestamp` — Unix seconds
- `signature`SSH signature over (nonce || timestamp)
- `public_key` — SSH public key in authorized_keys format
**Server verification:**
- Parse public key, check fingerprint against `authorized_keys` file