diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d5146f2..818bb69 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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