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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user