A standalone identity service (a self-hosted IdP) shared across products — one deployment is one realm
with a single shared user pool (ADR-0018). It ships the
service (OAuth 2.0 + OIDC token issuance), a headless SDK, an optional drop-in React <Login/>, an
authenticated management plane (HTTP /admin/v1 + an MCP server for agents), and an optional operator
admin console. It owns authentication (who you are); consuming products keep their own
authorization (what you may do).
identity-service/
├── docker/ # Docker Compose base + overrides; backup.sh (nightly encrypted backups)
├── service/ # REST API + Docker assets
│ ├── src/ # Express app, OAuth + session cores, admin plane, MCP server, models
│ ├── Dockerfile # Container build
├── sdk/ # Headless TypeScript client for the API
│ └── src/
├── react/ # Optional React UI: drop-in <Login/> (@fps4/identity-service-react)
│ └── src/
├── console/ # Optional operator admin console (Next.js, @fps4/identity-service-console)
│ └── app/
├── config/ # seed.example.yaml → seed.yaml (gitignored): applications (+role catalogues + credentials) + users + assignments
├── docs/ # Two-plane docs: design/ · reference/ · guides/ · product/ (index: docs/README.md)
└── README.md
-
Copy
service/.env.exampleto.envand set values:MONGO_URI,MONGO_DB_NAMEAUTH_JWT_SECRET,AUTH_JWT_ISSUER,AUTH_JWT_AUDIENCE- OAuth settings: token TTLs, deployment limits, optional key passphrase (see comments in
.env.example) - Optionally update
SESSION_TTL_MINUTES,CORS_ORIGINS
-
Install dependencies & build:
cd service npm install npm run build npm test npm start
-
(Optional) Run with Docker:
docker compose -f docker/compose.yaml -f docker/compose.dev.yaml up --build
Use
docker compose -f docker/compose.yaml -f docker/compose.dev.yaml downto stop containers.
The service listens on PORT (default 7305). Health check at GET /health.
POST /oauth2/token– client credentials grant issuing RS256 access tokens.GET /.well-known/jwks.json– JWKS for verifying issued tokens.POST /v1/sessions– persist session, issue legacy session JWT (in migration).PATCH /v1/sessions/:sessionId– attach contact identifiers or cookie context./admin/v1/*– the authenticated management plane (ADR-0007): applications (their role catalogues, members, and credentials — ADR-0020), credentials (OAuth clients under an application), users, assignments (user↔app entitlements — ADR-0019), invites, signing keys, stats, and audit. Network-restricted, scoped per actor, append-only audited. The same operations are exposed to agents over an MCP server (npm run mcp).- See
docs/reference/api.mdfor full payloads and responses.
import { ComponentAuthClient } from '@fps4/identity-service-sdk';
const client = new ComponentAuthClient({
baseUrl: 'https://auth.example.com'
});
const session = await client.createSession({ visitorId: 'visitor-001' });
await client.updateSession({ sessionId: session.sessionId, contactId: 'contact-42' });
const token = await client.requestClientCredentialsToken({
clientId: process.env.CORE_AUTH_CLIENT_ID!,
clientSecret: process.env.CORE_AUTH_CLIENT_SECRET!,
scope: ['telemetry:write']
});
console.log(token.accessToken);A browser frontend drives the redirect login with PKCE and forwards the issued token as
Authorization: Bearer to its own API. The SDK calls are named for Google because that was the first
IdP, but the flow is provider-agnostic: /oauth2/authorize redirects to Google when the deployment
configures a Google app, and otherwise serves this service's own login form. The consumer's half —
beginGoogleLogin → navigate → completeGoogleLogin — is identical either way.
// 1. Begin: stash the verifier/state, then navigate to the authorization endpoint.
const { authorizationUrl, codeVerifier, state } = await client.beginGoogleLogin({
clientId: 'client-maestro',
redirectUri: 'https://app.example.com/auth/callback'
});
sessionStorage.setItem('pkce', JSON.stringify({ codeVerifier, state }));
window.location.assign(authorizationUrl);
// 2. On the redirect back (…/auth/callback?code=…&state=…): exchange the code.
const { codeVerifier, state } = JSON.parse(sessionStorage.getItem('pkce')!);
if (params.get('state') !== state) throw new Error('state mismatch');
const token = await client.completeGoogleLogin({
code: params.get('code')!,
codeVerifier,
redirectUri: 'https://app.example.com/auth/callback',
clientId: 'client-maestro'
});
// token.accessToken → send as `Authorization: Bearer`; token.refreshToken → client.refreshUserToken(...)Run npm install && npm run build inside sdk/ to compile distributable assets. Consumers need a fetch implementation (Node 18+ or polyfill); the login helpers also require WebCrypto (browser or Node 18+).
For React consumers, @fps4/identity-service-react (in react/) ships a drop-in <Login/> for the
local email/password IdP — so apps don't rebuild the form. It's a separate, opt-in package (React
peer dependency only); the headless SDK stays UI-free.
import { Login } from '@fps4/identity-service-react';
<Login
baseUrl="https://auth-dev.example.com"
clientId="client-local"
onSuccess={(token) => sessionStorage.setItem('access_token', token.accessToken)}
/>See react/README.md for styling (Tailwind/shadcn) and the full API.
For day-2 operations, @fps4/identity-service-console (in console/) is an optional Next.js app — a
thin server-side client over the /admin/v1 management plane (ADR-0007): dashboards plus application
(with role-catalogue, members, and credentials — ADR-0020), user, assignment, and signing-key management.
The admin bearer token stays in server env and never reaches the browser; no direct database access.
Distinct from the consumer-facing <Login/> widget. See console/README.md.
Docs follow a two-plane structure — see docs/README.md for the full index, or
docs/overview.md for the landing.
docs/design/architecture.md– overall architecture, OAuth components, and the management plane.docs/reference/api.md– endpoint contract (incl./admin/v1) and token shape.docs/guides/tenant-config.md– deployment configuration & OAuth clients.docs/guides/deployment.md– how the service is deployed, plus nightly backups & recovery.console/README.md– the optional operator admin console over/admin/v1.docs/product/– theRQ-*functional specs (e.g.RQ-0001adds user identity via Google SSO, issued as a verifiable JWT).docs/design/decisions/– architecture decision records (ADRs).CONTRIBUTING.md– how to build & test the packages.tests/– manual harness + scripts for integration checks on deployed environments.
The service is a stateless container with MongoDB as its only persistent dependency, deployed
manually over SSH to a Docker host (DOCKER_HOST=ssh://<host>). Secrets live in a gitignored
docker/.env (GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET, OAUTH_KEY_PASSPHRASE, the HTTPS
AUTH_JWT_ISSUER, etc.) — never committed. A public HTTPS endpoint (reverse proxy or tunnel) fronts
the service; it listens on PORT (default 7305). The image build runs npm run build && npm test,
so a red test fails the deploy.
See docs/guides/deployment.md for the full procedure, prerequisites,
and how a consumer's verifier env lines up with what the service mints.