Skip to content

Commit 52529aa

Browse files
committed
docs(examples): introduce pkg/client SDK example and full SDK reference
Split the distributed-oidc-client example into two variants: - distributed-oidc-client/ — rewritten as the recommended ~150-line SDK consumer using pkg/client with OIDC client-credentials, multi-endpoint failover, and topology refresh (down from ~480 lines) - distributed-oidc-client-raw/ — the original hand-rolled net/http version retained as a wire-protocol reference and for non-Go consumers Add docs/client-sdk.md: comprehensive SDK reference covering all four auth modes (bearer, Basic, OIDC client credentials, mTLS via WithHTTPClient), failover policy, topology refresh semantics with the 1s floor and seed fallback, the full sentinel + *StatusError recipe set, command surface, and production caveats (connection pooling, retry policy, OTel propagation, OIDC refresh visibility). Update __examples/README.md to list both variants with the SDK path flagged as recommended, and register the new page in mkdocs.yml Reference navigation.
1 parent 5cad2ca commit 52529aa

9 files changed

Lines changed: 1161 additions & 537 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,19 @@ All notable changes to HyperCache are recorded here. The format follows
88

99
### Added
1010

11+
- **Client SDK reference + example migration.** New [`docs/client-sdk.md`](docs/client-sdk.md) is the
12+
recommended starting point for Go consumers — covers every auth mode (bearer / Basic / OIDC client
13+
credentials / custom mTLS via `WithHTTPClient`), the multi-endpoint failover policy, topology refresh
14+
semantics with the 1s floor and seed fallback, the full sentinel + `*StatusError` recipe set, and the
15+
production caveats (connection pooling, retry policy, OTel propagation, OIDC refresh visibility). The
16+
existing hand-rolled HTTP demo at `__examples/distributed-oidc-client/` was renamed to
17+
[`__examples/distributed-oidc-client-raw/`](__examples/distributed-oidc-client-raw/) — kept in-tree as the
18+
"what the SDK does under the hood" reference and for non-Go consumers reading along — while
19+
[`__examples/distributed-oidc-client/`](__examples/distributed-oidc-client/) is now the ~150-line SDK
20+
consumer that collapses the prior 480 lines down by ~70%. Top-level
21+
[`__examples/README.md`](__examples/README.md) lists both with the SDK version flagged as recommended. The
22+
SDK page is registered under Reference in [`mkdocs.yml`](mkdocs.yml) alongside the API reference and
23+
changelog.
1124
- **`pkg/client` — Go SDK for hypercache-server clusters.** Closes the three operational gaps the OIDC-client
1225
example surfaced: - **Multi-endpoint HA without an external LB.** `client.New([]string{...}, opts...)`
1326
accepts a slice of seed endpoints. Each request picks one at random; on transport failure / 5xx / 503

__examples/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@ All the code in this directory is for demonstration purposes only.
2424

2525
1. [`Observability (OpenTelemetry)`](./observability/otel.go) - Demonstrates wrapping the service with tracing and metrics middleware using OpenTelemetry.
2626

27-
1. [`Distributed OIDC client`](./distributed-oidc-client/) - End-to-end demo of a backend service authenticating to a `hypercache-server` cluster via OIDC client credentials, then exercising the PUT/GET/DELETE/batch surface. See the example's [README](./distributed-oidc-client/README.md) for env-var setup and IdP integration patterns.
27+
1. [`Distributed OIDC client (SDK)`](./distributed-oidc-client/) - **Recommended**: ~150-line consumer using [`pkg/client`](../pkg/client/) for OIDC client-credentials auth, multi-endpoint failover, topology refresh, and typed errors. The path most Go integrators should follow. See [`docs/client-sdk.md`](../docs/client-sdk.md) for the full SDK reference.
28+
29+
1. [`Distributed OIDC client (raw HTTP)`](./distributed-oidc-client-raw/) - The hand-crafted version of the above against `net/http` — kept in the tree as a reference for what the SDK does internally and for environments that can't depend on `pkg/client` (non-Go consumers reading along, code-review reference, etc.).
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Distributed cache client with OIDC auth (raw HTTP version)
2+
3+
> **Most consumers should use the SDK.** See
4+
> [`__examples/distributed-oidc-client/`](../distributed-oidc-client/) for the recommended ~30-line
5+
> equivalent built on [`pkg/client`](../../pkg/client/), and [`docs/client-sdk.md`](../../docs/client-sdk.md)
6+
> for the full SDK reference.
7+
>
8+
> This raw-HTTP version stays in the tree as a reference for what the SDK does internally — the auth-header
9+
> bookkeeping, content negotiation, base64-batching, and error-envelope parsing operators occasionally need
10+
> to inspect when integrating non-Go clients or auditing the wire protocol.
11+
12+
A runnable example showing a backend service connecting to a `hypercache-server` cluster, authenticating via
13+
OIDC client credentials, and exercising the full client API — implemented by hand against `net/http` so the
14+
wire format is visible end-to-end.
15+
16+
This is a **service-to-service** flow (no browser, no user redirect). The application proves its identity to
17+
the IdP with a client ID and secret, gets back a short-lived JWT, and presents that JWT to the cache. The
18+
cache validates the JWT against the same IdP and resolves the caller's identity + scopes. The same model fits
19+
Keycloak, Auth0, Okta, Entra ID, Google, and any RFC 6749 §4.4 compliant IdP.
20+
21+
## What the example does
22+
23+
1. Discovers the IdP's token endpoint from `$OIDC_ISSUER/.well-known/openid-configuration`.
24+
1. Uses `golang.org/x/oauth2/clientcredentials` to exchange the client ID + secret for an access token. The
25+
library caches the token in memory and transparently refreshes it before expiry — every cache call below is
26+
a plain `http.Client.Do` with no header bookkeeping.
27+
1. Hits `GET /v1/me` to verify the bound identity + scopes (canary: "is my token actually valid against this
28+
cluster?").
29+
1. Exercises `PUT /v1/cache/:key`, `GET` with raw bytes, `GET` with the JSON envelope (metadata view),
30+
`DELETE`, and `POST /v1/cache/batch/put`.
31+
32+
## Requirements
33+
34+
- Go 1.26+ (see [`go.mod`](../../go.mod))
35+
- A reachable `hypercache-server` running with OIDC enabled (i.e. `HYPERCACHE_OIDC_ISSUER` and
36+
`HYPERCACHE_OIDC_AUDIENCE` set on the server). See
37+
[`cmd/hypercache-server/README.md`](../../cmd/hypercache-server/README.md).
38+
- An OIDC client registered in your IdP with -. The **client_credentials** grant type enabled. -. A scope (or
39+
audience claim mapper) that produces the scopes the cache expects — see [Scope mapping](#scope-mapping)
40+
below.
41+
42+
## Environment variables
43+
44+
| Variable | Required | Default | Description |
45+
| --------------------- | -------- | ----------------------- | ------------------------------------------------------------------------ |
46+
| `HYPERCACHE_ENDPOINT` | no | `http://localhost:8080` | Cache server base URL (client API port). |
47+
| `OIDC_ISSUER` | **yes** || IdP base URL (no trailing `/.well-known`). |
48+
| `OIDC_AUDIENCE` | **yes** || Must match the server's `HYPERCACHE_OIDC_AUDIENCE`. |
49+
| `OIDC_CLIENT_ID` | **yes** || OAuth2 client ID registered for this service in the IdP. |
50+
| `OIDC_CLIENT_SECRET` | **yes** || OAuth2 client secret. Treat as a secret — never commit. |
51+
| `OIDC_SCOPES` | no | `openid` | Space-separated scope list. See [Scope mapping](#scope-mapping). |
52+
| `OIDC_TOKEN_INSECURE` | no | `0` | Set to `1` to skip TLS verification on the token endpoint. **Dev only.** |
53+
54+
## Run
55+
56+
```sh
57+
export HYPERCACHE_ENDPOINT=https://cache.example.com:8080
58+
export OIDC_ISSUER=https://keycloak.example.com/realms/cache
59+
export OIDC_AUDIENCE=hypercache-cluster
60+
export OIDC_CLIENT_ID=my-service
61+
export OIDC_CLIENT_SECRET=...
62+
export OIDC_SCOPES="openid cache:read cache:write"
63+
64+
go run ./__examples/distributed-oidc-client/
65+
```
66+
67+
Expected output:
68+
69+
```text
70+
== /v1/me (verify bound identity) ==
71+
resolved identity: my-service
72+
granted scopes: [read write]
73+
74+
== PUT /v1/cache/example-key (5 min TTL) ==
75+
stored
76+
77+
== GET /v1/cache/example-key (raw bytes) ==
78+
value: "hello from oidc client"
79+
80+
== GET /v1/cache/example-key (JSON envelope) ==
81+
key: example-key
82+
version: 1
83+
owners: [cache-0 cache-1 cache-2]
84+
encoding: base64
85+
86+
== DELETE /v1/cache/example-key ==
87+
deleted
88+
89+
== batch PUT /v1/cache/batch/put (3 keys) ==
90+
stored 3 keys
91+
```
92+
93+
## Scope mapping
94+
95+
The cache treats scopes as a closed set: `read`, `write`, `admin`. Your IdP's scope/claim values must map to
96+
those three strings for the cache to grant access.
97+
98+
Two configuration knobs on the server control this mapping:
99+
100+
- `HYPERCACHE_OIDC_SCOPE_CLAIM` (default `scope`) — which JWT claim to read. Standard OAuth2 uses `scope`
101+
(space-separated string); some IdPs use a custom array claim like `cache_scopes`.
102+
- The values inside that claim must be exactly `read`, `write`, or `admin`. Anything else is dropped silently.
103+
104+
**Pattern 1: OAuth2 standard scopes.** Register scopes `read` and `write` in your IdP, grant them to the
105+
service client, and request them via `OIDC_SCOPES="openid read write"`. The cache reads them from the standard
106+
`scope` claim.
107+
108+
**Pattern 2: Mapped scopes** (when your IdP namespaces scopes, e.g. `cache:read`). Use the IdP's claim-mapper
109+
feature to project the `cache:read` scope into a custom claim, then set
110+
`HYPERCACHE_OIDC_SCOPE_CLAIM=cache_scopes` server-side. Map `cache:read``read`, `cache:write``write` at
111+
the mapper level.
112+
113+
**Pattern 3: Role-based.** Map IdP roles (e.g. Keycloak realm roles `cache-reader`, `cache-writer`) into the
114+
custom claim. Same shape as Pattern 2.
115+
116+
## Coexistence with static bearer tokens
117+
118+
A cluster can run with both OIDC verification and static bearer tokens configured at the same time. The auth
119+
chain resolves in this order:
120+
121+
1. `Authorization: Bearer <token>` matched against `HYPERCACHE_AUTH_CONFIG`'s `tokens` list → static identity.
122+
1. If no static match, the OIDC verifier runs → OIDC identity.
123+
1. If neither matches and `AllowAnonymous: true`, request runs as anonymous. Otherwise 401.
124+
125+
This means a single deployment can have humans signing in via OIDC (through the monitor's redirect flow) and
126+
machine integrations using long-lived static bearers — both succeed against the same cache. See
127+
[`pkg/httpauth/policy.go`](../../pkg/httpauth/policy.go) for the implementation.
128+
129+
## Production caveats
130+
131+
This example is intentionally minimal. For real services:
132+
133+
- **Pool HTTP connections.** `oauth2.NewClient` returns a fresh `http.Client`; in production you want a
134+
configured `Transport` with `MaxIdleConnsPerHost`, keepalives, and connection-level timeouts.
135+
- **Retry policy.** This example does no retries; transient 5xx or network errors surface as failures. Wrap
136+
the cache calls in a bounded exponential-backoff retry for production.
137+
- **Observability.** The cache emits OpenTelemetry traces if the server is configured with a tracer provider;
138+
propagate the trace context by setting `traceparent` headers on your requests.
139+
- **Token caching across processes.** `clientcredentials` caches tokens per-process. If your service spawns
140+
many short-lived workers, consider a shared cache (e.g. Redis-backed) to avoid re-hitting the IdP on every
141+
process start.
142+
143+
## See also
144+
145+
- [`docs/auth.md`](../../docs/auth.md) — server-side auth surface (when present; otherwise
146+
[`cmd/hypercache-server/README.md`](../../cmd/hypercache-server/README.md) is the current source of truth).
147+
- [`docs/oncall.md`](../../docs/oncall.md#auth-failures) — debugging 401/403s when the client surface is
148+
misbehaving.
149+
- [`cmd/hypercache-server/oidc.go`](../../cmd/hypercache-server/oidc.go) — the cache's OIDC verifier closure,
150+
for reference on what's enforced server-side.

0 commit comments

Comments
 (0)