From a720330b658b731a38ca1f575060f6530e9effaa Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 21:14:33 +0000 Subject: [PATCH] docs(socrate): set AdminBaseURL explicitly in production The AdminBaseURL derived when it is empty keeps BaseURL's scheme and host and swaps the port for 8081. Behind a TLS reverse proxy, the usual production layout, BaseURL is the public issuer, so the derived URL is wrong: the admin API is plain HTTP bound to loopback on the server's ADMIN_PORT, which is 8082 where a legacy server holds 8081. The integration guide, the README environment table and the package doc now say to set it (e.g. http://127.0.0.1:8081), where the port comes from, and what a backend on another host can still call. No code change. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GKRxaeYxyDhmt42cehLsGA --- CHANGELOG.md | 9 +++++++++ README.md | 2 +- docs/CLIENT-INTEGRATION.md | 24 +++++++++++++++++++----- socrate/client.go | 7 +++++-- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca2b070..e571b97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ All notable changes to backendkit are documented here. Format: ## [Unreleased] +### Changed + +- Docs: `socrate.ClientConfig.AdminBaseURL` is documented as required in production. The value + derived when it is empty (`BaseURL` with port 8081, same scheme and host) is wrong behind a + TLS reverse proxy, where the admin API is plain HTTP on loopback; the integration guide, the + README and the package doc now say to set it (e.g. `http://127.0.0.1:8081`), to read the port + from the server's `ADMIN_PORT` (8082 in the layout that co-hosts a legacy server on 8081), + and what a backend on another host can and cannot call. No code change. + ## [1.15.0] - 2026-09-29 Minor release on the **v1** line: no breaking change to any exported identifier. It adds opt-in diff --git a/README.md b/README.md index a93f483..06212dd 100644 --- a/README.md +++ b/README.md @@ -963,7 +963,7 @@ however you like in your own service. | `SOCRATE_JWKS_URL` | `jwtauth.New` | JWKS endpoint used to validate RS256 signatures, e.g. `https://socrate.example.com/.well-known/jwks.json` | | `SOCRATE_ISSUER` | `jwtauth.New` | Expected `iss` claim — optional; enforced only when non-empty (an empty value logs a warning at startup) | | `SOCRATE_BASE_URL` | `socrate.NewClient` | Socrate OAuth port base URL (e.g. `https://socrate.example.com`) | -| `SOCRATE_ADMIN_BASE_URL` | `socrate.NewClient` | Socrate admin API base URL — optional; derived from `SOCRATE_BASE_URL` with port 8081 when empty | +| `SOCRATE_ADMIN_BASE_URL` | `socrate.NewClient` | Socrate admin API base URL, e.g. `http://127.0.0.1:8081` (the server's `ADMIN_PORT`; 8082 where a legacy server holds 8081). Set it in production: the value derived from `SOCRATE_BASE_URL` (port 8081, same scheme and host) is wrong behind a TLS proxy | | `SOCRATE_CLIENT_ID` | `socrate.NewClient`, `jwtauth.WithAudience` | OAuth client ID | | `SOCRATE_CLIENT_SECRET` | `socrate.NewClient` | Client secret — required for service-account calls, `Decide`, `RevokeToken`, `IntrospectToken` and a BFF's token exchange | | `SOCRATE_APP_ID` | `socrate.NewClient` | Pre-resolved numeric app ID — **required** for every service-account method, including `Decide` | diff --git a/docs/CLIENT-INTEGRATION.md b/docs/CLIENT-INTEGRATION.md index 06703d9..405c181 100644 --- a/docs/CLIENT-INTEGRATION.md +++ b/docs/CLIENT-INTEGRATION.md @@ -134,7 +134,7 @@ guide: | `SOCRATE_JWKS_URL` | `jwtauth.New` | JWKS endpoint, e.g. `https://socrate.example.com/.well-known/jwks.json` | | `SOCRATE_ISSUER` | `jwtauth.New` | Expected `iss` claim — optional but recommended in production | | `SOCRATE_BASE_URL` | `socrate.NewClient` | OAuth (public) port base URL, e.g. `https://socrate.example.com` | -| `SOCRATE_ADMIN_BASE_URL` | `socrate.NewClient` | Admin API base URL — optional; derived from `BaseURL` with port 8081 (the default deployment's admin port) if omitted | +| `SOCRATE_ADMIN_BASE_URL` | `socrate.NewClient` | Admin API base URL, e.g. `http://127.0.0.1:8081`. Set it in production: when empty it is derived from `BaseURL` with port 8081, which is wrong behind a TLS proxy (§6.3) | | `SOCRATE_CLIENT_ID` | `socrate.NewClient` | Your app's OAuth client ID | | `SOCRATE_CLIENT_SECRET` | `socrate.NewClient` | Client secret — required for service-account calls, `Decide`, `RevokeToken`, `IntrospectToken` and a BFF's token exchange | | `SOCRATE_APP_ID` | `socrate.NewClient` | Pre-resolved numeric app ID — **required** for every service-account method | @@ -302,7 +302,7 @@ client, err := socrate.NewClient(socrate.ClientConfig{ ClientID: os.Getenv("SOCRATE_CLIENT_ID"), // required ClientSecret: os.Getenv("SOCRATE_CLIENT_SECRET"), // service-account / introspect / revoke AppID: os.Getenv("SOCRATE_APP_ID"), // required for service-account calls - // AdminBaseURL: "https://socrate.example.com:8081", // optional; derived from BaseURL if empty + AdminBaseURL: os.Getenv("SOCRATE_ADMIN_BASE_URL"), // e.g. http://127.0.0.1:8081 — see §6.3 // Timeout: 30 * time.Second, // optional; default 30s }) if err != nil { @@ -365,9 +365,23 @@ URLs yourself: everything else — app-user management, app management, superadmins, security, dashboard, audit logs, magic links, policy decisions. -`AdminBaseURL` defaults to `BaseURL` with the host port replaced by `8081`. -Set it explicitly when your deployment differs, for example when the admin API -listens on another host or port. +`AdminBaseURL` defaults to `BaseURL` with the host port replaced by `8081`, +keeping its scheme and host. That default only fits a Socrate reached directly, +with both ports on one host. **Set `AdminBaseURL` explicitly in production:** + +- Behind a TLS reverse proxy (the usual layout), `BaseURL` is the public issuer, + e.g. `https://socrate.example.com`, and the derived + `https://socrate.example.com:8081` is wrong: the admin API is plain HTTP bound + to loopback (`ADMIN_BIND_HOST=127.0.0.1`), not published by the proxy. Use + `http://127.0.0.1:` from a service on the same host. +- The port is the server's `ADMIN_PORT`: 8081 by default, but a host that runs + another service on 8081 moves it — the layout that co-hosts Socrate with a + legacy server uses **8082**. Read it from the server's environment file rather + than assuming 8081; a wrong port can reach a different service. +- A backend on another host cannot reach a loopback-bound admin API at all. It + can still validate tokens (JWKS) and use the OAuth-port methods; admin, app-user + and policy calls need the backend on the Socrate host, or an admin API bound + to a private interface and firewalled to that backend. ### 6.4 Method reference diff --git a/socrate/client.go b/socrate/client.go index 339705b..98f6baf 100644 --- a/socrate/client.go +++ b/socrate/client.go @@ -19,7 +19,10 @@ // 8081 — Admin API (internal; restrict at network level) // /api/admin/*, /api/apps/{id}/users/*, /api/apps/{id}/service/* // -// AdminBaseURL defaults to BaseURL with the host port replaced by 8081. +// AdminBaseURL defaults to BaseURL with the host port replaced by 8081, keeping +// its scheme and host. Behind a TLS reverse proxy that default is wrong (the admin +// API is plain HTTP on loopback, on the server's ADMIN_PORT), so production +// callers set it explicitly, e.g. http://127.0.0.1:8081. // All user-management and admin calls are routed to AdminBaseURL automatically. package socrate @@ -70,7 +73,7 @@ type Client struct { // ClientConfig holds the constructor options for Client. type ClientConfig struct { BaseURL string // OAuth port URL (e.g. https://auth.example.com) - AdminBaseURL string // Admin port URL (e.g. https://auth.example.com:8081); derived from BaseURL if empty + AdminBaseURL string // Admin port URL (e.g. http://127.0.0.1:8081); derived from BaseURL (port 8081) if empty — set it behind a TLS proxy ClientID string // OAuth client ID ClientSecret string // OAuth client secret (required for service-account calls) AppID string // Pre-resolved numeric app ID; skips runtime resolution when set