Skip to content

Core · Service · Active

platform-auth-service

Shared auth and session service for the workspace, owning cookie sessions, provider configuration, local credentials, OIDC login and trusted identity routes for product backends.

  • TypeScript
  • NestJS 11
  • PostgreSQL
  • JWT/JWKS
  • cookie-parser

Spec sheet

Boundary

Core / Auth

Runtime

NestJS 11 HTTP service

Default port

3100

Persistence

Dedicated auth-postgres via AUTH_DATABASE_URL

Exposure

Public auth surface plus internal trusted routes

Responsibilities

  • Issue and resolve shared HttpOnly sessions across products.
  • Expose provider discovery, CSRF bootstrap and password login.
  • Support OIDC provider start and callback flows.
  • Manage provider configuration and local credentials through guarded admin routes.
  • Provide internal identity, membership and session orchestration routes.
  • Expose JWKS and public key material for token validation.

Interfaces and contract surface

  • GET /health
  • GET /auth/runtime
  • GET /.well-known/jwks.json
  • GET /auth/providers
  • GET /auth/session
  • GET /auth/csrf
  • POST /auth/login/password
  • POST /auth/logout
  • GET /auth/oidc/:providerId/start
  • GET /auth/oidc/:providerId/callback
  • GET /auth/admin/providers
  • GET /auth/admin/providers/:providerId
  • PUT /auth/admin/providers/:providerId
  • GET /auth/admin/capabilities
  • PUT /auth/admin/capabilities/:capabilityCode
  • GET /auth/admin/capability-grants/role
  • PUT /auth/admin/capability-grants/role
  • GET /auth/admin/capability-grants/membership
  • PUT /auth/admin/capability-grants/membership
  • GET /auth/admin/effective-capabilities
  • GET /auth/admin/local-credentials
  • PUT /auth/admin/local-credentials/:subjectId
  • DELETE /auth/admin/local-credentials/:subjectId
  • GET /internal/public-key.pem
  • POST /internal/identities/upsert
  • POST /internal/session/resolve
  • POST /internal/session/issue
  • POST /internal/session/bootstrap
  • DELETE /internal/memberships/by-subject

Consumers

Dependencies and external touchpoints

Notes

  • Auth state is Postgres-only; the previous file-state fallback has been removed.
  • The runtime is split across public, admin and internal route surfaces to keep responsibilities isolated.
  • A bare auth-service startup does not preload local users; product backends may bootstrap identities and credentials via internal routes.

Source references

  • platform-auth-service/README.md
  • docs/core-services-integration.md
  • platform-auth-service/package.json

Come integrarsi davvero

Gli esempi sotto servono per smoke test e integrazione backend-to-core. Nel flusso applicativo normale il frontend deve continuare a chiamare il proprio backend/BFF same-origin su /api/auth/*, non auth.cs.lvh.me direttamente.

Persistenza runtime

platform-auth-service usa PostgreSQL via AUTH_DATABASE_URL e persiste lo stato auth in tabelle dedicate:

  • platform_auth_identities
  • platform_auth_accounts
  • platform_auth_memberships
  • platform_auth_capabilities
  • platform_auth_capability_grants
  • platform_auth_provider_configs
  • platform_auth_signing_keys

La tabella legacy platform_auth_state con payload JSONB unico non viene piu letta, scritta, migrata o droppata dal runtime. Le membership hanno colonne interrogabili per product_code, subject_id, tenant_id, role e status; solo gli attributi estensibili restano in attributes JSONB.

I token di sessione propagano anche tenantId, role, permissions legacy e capabilities runtime. Le capability sono default-deny: senza grant esplicito la lista capabilities e vuota e i BFF non devono abilitare funzioni CORE o tool MCP. Il ruolo SUPER_ADMIN continua a emettere la permission trasversale platform:* per compatibilita, ma non abilita automaticamente le capability.

Le capability sono record generici (code, serviceKey, kind, riskLevel, metadata, enabled) e i grant sono assegnati per productCode a uno scope role o membership. Gli override membership possono usare deny, che prevale sugli allow del ruolo.

Variabili utili

bash
export AUTH_PUBLIC_URL=http://auth.cs.lvh.me:8080
export AUTH_INTERNAL_URL=http://127.0.0.1:3100
export INTERNAL_TOKEN=platform-local-stack-internal-token

1. Bootstrap identity, membership e credenziale locale

platform-auth-service non parte con un utente locale pre-caricato. Se stai testando il servizio in isolamento, crea prima identity, membership e password tramite la surface interna.

bash
curl -sS -X POST "$AUTH_INTERNAL_URL/internal/identities/upsert" \
  -H "Content-Type: application/json" \
  -H "x-platform-internal-token: $INTERNAL_TOKEN" \
  -d '{
    "email": "admin@local.test",
    "username": "admin@local.test",
    "password": "ChangeMe123!",
    "enabled": true,
    "membership": {
      "productCode": "funnel-ia-engine",
      "subjectId": "bootstrap-admin",
      "role": "SUPER_ADMIN"
    }
  }'

Nei prodotti reali questo bootstrap viene orchestrato dal backend di prodotto. Un riferimento concreto e funnel-ia-engine/backend/src/auth/auth.service.ts, che sincronizza utenti locali verso /internal/identities/upsert.

Questo e il flusso piu utile per verificare che il servizio emetta davvero la sessione cookie-based condivisa. Le POST pubbliche richiedono double-submit CSRF: prima chiama /auth/csrf, poi invia il token come X-CSRF-Token insieme al cookie jar.

bash
COOKIE_JAR=/tmp/platform-auth.cookies
rm -f "$COOKIE_JAR"

CSRF_TOKEN=$(
  curl -sS -c "$COOKIE_JAR" "$AUTH_PUBLIC_URL/auth/csrf" \
    | node -e "let body=''; process.stdin.on('data', chunk => body += chunk); process.stdin.on('end', () => console.log(JSON.parse(body).csrfToken));"
)

curl -i -sS -X POST "$AUTH_PUBLIC_URL/auth/login/password" \
  -H "Content-Type: application/json" \
  -H "X-CSRF-Token: $CSRF_TOKEN" \
  -b "$COOKIE_JAR" \
  -c "$COOKIE_JAR" \
  -d '{
    "email": "admin@local.test",
    "password": "ChangeMe123!",
    "productCode": "funnel-ia-engine"
  }'

La risposta JSON contiene anche accessToken, ma per validare il comportamento reale conviene poi leggere la sessione usando il cookie appena emesso:

bash
curl -sS "$AUTH_PUBLIC_URL/auth/session" \
  -b "$COOKIE_JAR"

3. Emissione e risoluzione sessione dal backend di prodotto

Quando il prodotto ha gia autenticato o provisionato un proprio soggetto, il backend puo chiedere a platform-auth-service di emettere o risolvere una sessione per quello specifico productCode / subjectId.

Emissione token:

bash
curl -sS -X POST "$AUTH_INTERNAL_URL/internal/session/issue" \
  -H "Content-Type: application/json" \
  -H "x-platform-internal-token: $INTERNAL_TOKEN" \
  -d '{
    "productCode": "funnel-ia-engine",
    "subjectId": "bootstrap-admin",
    "authProvider": "local"
  }'

Bootstrap sessione con sostituzione membership di prodotto:

bash
curl -sS -X POST "$AUTH_INTERNAL_URL/internal/session/bootstrap" \
  -H "Content-Type: application/json" \
  -H "x-platform-internal-token: $INTERNAL_TOKEN" \
  -d '{
    "email": "admin@local.test",
    "username": "admin@local.test",
    "authProvider": "local",
    "memberships": [
      {
        "productCode": "cs-portal",
        "subjectId": "portal-admin",
        "role": "PORTAL_ADMIN"
      }
    ]
  }'

Risoluzione del token appena ricevuto:

bash
curl -sS -X POST "$AUTH_INTERNAL_URL/internal/session/resolve" \
  -H "Content-Type: application/json" \
  -H "x-platform-internal-token: $INTERNAL_TOKEN" \
  -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -d '{
    "productCode": "funnel-ia-engine"
  }'

Per backend e facade pubbliche, il pattern corretto resta:

  • frontend -> /api/auth/* same-origin
  • backend prodotto -> platform-auth-service
  • header tecnico obbligatorio x-platform-internal-token
  • Authorization: Bearer <session-token> inoltrato solo come contesto utente delegato, mai come sostituto del token interno

4. Admin provider e credenziali locali

Le viste operations e i BFF amministrativi usano la surface admin protetta da token interno:

bash
curl -sS "$AUTH_INTERNAL_URL/auth/admin/providers" \
  -H "x-platform-internal-token: $INTERNAL_TOKEN"
bash
curl -sS "$AUTH_INTERNAL_URL/auth/admin/local-credentials?productCode=cs-portal" \
  -H "x-platform-internal-token: $INTERNAL_TOKEN"

Surface capability:

bash
curl -sS "$AUTH_INTERNAL_URL/auth/admin/capabilities" \
  -H "x-platform-internal-token: $INTERNAL_TOKEN"

curl -sS "$AUTH_INTERNAL_URL/auth/admin/effective-capabilities?productCode=cs-chat&subjectId=<SUBJECT_ID>" \
  -H "x-platform-internal-token: $INTERNAL_TOKEN"

Endpoint reference

Le route pubbliche usano cookie HttpOnly condivisi e double-submit CSRF sulle POST. Le route admin e internal richiedono sempre x-platform-internal-token: $INTERNAL_TOKEN.

Runtime, chiavi e sessione pubblica

EndpointScopoRequestEsempioRisposta rappresentativaErrori e consumer
GET /healthVerifica liveness auth service.Nessun body.curl -sS "$AUTH_INTERNAL_URL/health"{"status":"ok","service":"platform-auth-service"}5xx infrastrutturali. Consumer: status dashboard.
GET /auth/runtimeEspone runtime pubblico necessario ai BFF/UI auth.Nessun body.curl -sS "$AUTH_PUBLIC_URL/auth/runtime"{"cookieName":"cs_session","csrfCookieName":"cs_csrf","sessionTtlSeconds":604800}5xx config non valida. Consumer: BFF/facade auth.
GET /.well-known/jwks.jsonPubblica JWKS per validazione token.Nessun body.curl -sS "$AUTH_PUBLIC_URL/.well-known/jwks.json"{"keys":[{"kty":"RSA","kid":"platform-auth-current","use":"sig","alg":"RS256"}]}5xx signing key non disponibile. Consumer: servizi Core e BFF.
GET /internal/public-key.pemEspone chiave pubblica PEM per consumer interni legacy.Header interno.curl -sS "$AUTH_INTERNAL_URL/internal/public-key.pem" -H "x-platform-internal-token: $INTERNAL_TOKEN"-----BEGIN PUBLIC KEY-----...401/403 token. Consumer: servizi che validano JWT senza JWKS.
GET /auth/providersLista provider login pubblici abilitati.Nessun body.curl -sS "$AUTH_PUBLIC_URL/auth/providers"{"items":[{"id":"local","type":"local","label":"Email e password","enabled":true}]}5xx state store. Consumer: BFF/facade auth.
GET /auth/csrfCrea cookie CSRF e restituisce token da inviare sulle POST pubbliche.Cookie jar opzionale.curl -sS -c "$COOKIE_JAR" "$AUTH_PUBLIC_URL/auth/csrf"{"csrfToken":"csrf_123"}5xx cookie config. Consumer: login/logout facade.
POST /auth/login/passwordLogin locale con cookie sessione condiviso.email, password, productCode; header X-CSRF-Token e cookie CSRF.curl -sS -X POST "$AUTH_PUBLIC_URL/auth/login/password" -H "Content-Type: application/json" -H "X-CSRF-Token: $CSRF_TOKEN" -b "$COOKIE_JAR" -c "$COOKIE_JAR" -d '{"email":"admin@local.test","password":"ChangeMe123!","productCode":"cs-chat"}'{"authenticated":true,"accessToken":"<ACCESS_TOKEN>","user":{"email":"admin@local.test"},"memberships":[{"productCode":"cs-chat","subjectId":"chat-admin","role":"SUPER_ADMIN"}],"capabilities":[]}400 body/CSRF, 401 credenziali, 403 provider disabilitato. Consumer: backend/facade prodotto.
GET /auth/sessionRisolve sessione cookie pubblica.Cookie sessione.curl -sS "$AUTH_PUBLIC_URL/auth/session" -b "$COOKIE_JAR"{"authenticated":true,"user":{"email":"admin@local.test"},"memberships":[{"productCode":"cs-chat","subjectId":"chat-admin","role":"SUPER_ADMIN"}],"permissions":["platform:*"],"capabilities":["platform:mcp:tool:platform_status_overview:call"]}200 con authenticated:false se assente/scaduta. Consumer: BFF/facade auth.
POST /auth/logoutCancella cookie sessione pubblica.Header/cookie CSRF.curl -sS -X POST "$AUTH_PUBLIC_URL/auth/logout" -H "X-CSRF-Token: $CSRF_TOKEN" -b "$COOKIE_JAR" -c "$COOKIE_JAR"{"authenticated":false}400 CSRF. Consumer: BFF/facade auth.
GET /auth/oidc/:providerId/startAvvia login OIDC con redirect provider.Path provider, query productCode?, redirectUrl?.curl -i "$AUTH_PUBLIC_URL/auth/oidc/google/start?productCode=cs-chat"302 Location: https://accounts.google.com/...404 provider sconosciuto, 403 provider disabilitato. Consumer: facade auth.
GET /auth/oidc/:providerId/callbackCompleta login OIDC e imposta sessione.Query provider OIDC code, state.curl -i "$AUTH_PUBLIC_URL/auth/oidc/google/callback?code=<CODE>&state=<STATE>"302 Location: http://chat.cs.lvh.me:8080/...400 state/code non validi, 401 exchange fallito. Consumer: provider OIDC via browser.

Admin provider, capability e credenziali

EndpointScopoRequestEsempioRisposta rappresentativaErrori e consumer
GET /auth/admin/providersLista provider auth con config redatta.Header interno.curl -sS "$AUTH_INTERNAL_URL/auth/admin/providers" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"items":[{"id":"local","type":"local","enabled":true},{"id":"google","type":"oidc","enabled":false,"config":{"clientSecret":"***"}}]}401/403 token. Consumer: operations e admin BFF.
GET /auth/admin/providers/:providerIdDettaglio provider auth redatto.Path providerId.curl -sS "$AUTH_INTERNAL_URL/auth/admin/providers/google" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"id":"google","type":"oidc","enabled":false,"config":{"issuer":"https://accounts.google.com","clientSecret":"***"}}404 provider. Consumer: operations.
PUT /auth/admin/providers/:providerIdAggiorna provider auth.JSON provider config.curl -sS -X PUT "$AUTH_INTERNAL_URL/auth/admin/providers/google" -H "Content-Type: application/json" -H "x-platform-internal-token: $INTERNAL_TOKEN" -d '{"type":"oidc","enabled":true,"label":"Google","config":{"issuer":"https://accounts.google.com","clientId":"<CLIENT_ID>","clientSecret":"<CLIENT_SECRET>"}}'{"id":"google","type":"oidc","enabled":true,"config":{"clientSecret":"***"}}400 config non valida, 401/403 token. Consumer: operations.
GET /auth/admin/capabilitiesLista capability runtime registrate.Header interno.curl -sS "$AUTH_INTERNAL_URL/auth/admin/capabilities" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"items":[{"code":"platform:mcp:tool:platform_status_overview:call","serviceKey":"platform-mcp-service","kind":"mcp-tool","riskLevel":"low","enabled":true}]}401/403 token. Consumer: BFF ACL e operations.
PUT /auth/admin/capabilities/:capabilityCodeCrea o aggiorna una capability.Body serviceKey, kind, riskLevel, metadata?, enabled?.curl -sS -X PUT "$AUTH_INTERNAL_URL/auth/admin/capabilities/platform:mcp:tool:platform_status_overview:call" -H "Content-Type: application/json" -H "x-platform-internal-token: $INTERNAL_TOKEN" -d '{"serviceKey":"platform-mcp-service","kind":"mcp-tool","riskLevel":"low","enabled":true}'{"code":"platform:mcp:tool:platform_status_overview:call","enabled":true}400 codice/body non valido. Consumer: MCP/catalog sync operations.
GET /auth/admin/capability-grants/role?productCode=&role=Legge grant capability assegnati a un ruolo prodotto.Query opzionali productCode, role.curl -sS "$AUTH_INTERNAL_URL/auth/admin/capability-grants/role?productCode=cs-chat&role=ADMIN" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"items":[{"productCode":"cs-chat","role":"ADMIN","capabilityCode":"platform:mcp:tool:platform_status_overview:call","effect":"allow"}]}400 query incompleta se richiesta dal service, 401/403 token. Consumer: admin ACL.
PUT /auth/admin/capability-grants/roleSostituisce grant capability di un ruolo.productCode, role, grants.curl -sS -X PUT "$AUTH_INTERNAL_URL/auth/admin/capability-grants/role" -H "Content-Type: application/json" -H "x-platform-internal-token: $INTERNAL_TOKEN" -d '{"productCode":"cs-chat","role":"ADMIN","grants":[{"capabilityCode":"platform:mcp:tool:platform_status_overview:call","effect":"allow"}]}'{"productCode":"cs-chat","role":"ADMIN","items":[{"capabilityCode":"platform:mcp:tool:platform_status_overview:call","effect":"allow"}]}400 grant non valido, 404 capability assente. Consumer: admin ACL.
GET /auth/admin/capability-grants/membership?productCode=&subjectId=Legge override capability per membership.Query productCode, subjectId.curl -sS "$AUTH_INTERNAL_URL/auth/admin/capability-grants/membership?productCode=cs-chat&subjectId=chat-admin" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"items":[{"productCode":"cs-chat","subjectId":"chat-admin","capabilityCode":"platform:mcp:tool:platform_ai_generate_text:call","effect":"deny"}]}400 query incompleta. Consumer: admin ACL.
PUT /auth/admin/capability-grants/membershipSostituisce override capability di una membership.productCode, subjectId, grants.curl -sS -X PUT "$AUTH_INTERNAL_URL/auth/admin/capability-grants/membership" -H "Content-Type: application/json" -H "x-platform-internal-token: $INTERNAL_TOKEN" -d '{"productCode":"cs-chat","subjectId":"chat-admin","grants":[{"capabilityCode":"platform:mcp:tool:platform_ai_generate_text:call","effect":"deny"}]}'{"productCode":"cs-chat","subjectId":"chat-admin","items":[{"capabilityCode":"platform:mcp:tool:platform_ai_generate_text:call","effect":"deny"}]}400 grant non valido. Consumer: admin ACL.
GET /auth/admin/effective-capabilities?productCode=&subjectId=Calcola capability effettive dopo role grant e membership override.Query productCode, subjectId.curl -sS "$AUTH_INTERNAL_URL/auth/admin/effective-capabilities?productCode=cs-chat&subjectId=chat-admin" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"productCode":"cs-chat","subjectId":"chat-admin","capabilities":["platform:mcp:tool:platform_status_overview:call"]}404 membership assente. Consumer: BFF e operations.
GET /auth/admin/local-credentials?productCode=Lista credenziali locali per prodotto.Query opzionale productCode.curl -sS "$AUTH_INTERNAL_URL/auth/admin/local-credentials?productCode=cs-portal" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"items":[{"productCode":"cs-portal","subjectId":"portal-admin","email":"admin@local.test","enabled":true}]}401/403 token. Consumer: operations.
PUT /auth/admin/local-credentials/:subjectId?productCode=Crea o aggiorna password locale per subject.Query productCode, body email?, password, enabled?.curl -sS -X PUT "$AUTH_INTERNAL_URL/auth/admin/local-credentials/portal-admin?productCode=cs-portal" -H "Content-Type: application/json" -H "x-platform-internal-token: $INTERNAL_TOKEN" -d '{"email":"admin@local.test","password":"ChangeMe123!","enabled":true}'{"productCode":"cs-portal","subjectId":"portal-admin","email":"admin@local.test","enabled":true}400 password/query non valida. Consumer: operations bootstrap.
DELETE /auth/admin/local-credentials/:subjectId?productCode=Disabilita/rimuove credenziale locale.Query opzionale productCode.curl -i -sS -X DELETE "$AUTH_INTERNAL_URL/auth/admin/local-credentials/portal-admin?productCode=cs-portal" -H "x-platform-internal-token: $INTERNAL_TOKEN"204 No Content404 credenziale assente. Consumer: operations.

Internal identity e session orchestration

EndpointScopoRequestEsempioRisposta rappresentativaErrori e consumer
POST /internal/identities/upsertCrea/aggiorna identity, membership e opzionalmente password locale.email, username?, password?, enabled?, membership?.curl -sS -X POST "$AUTH_INTERNAL_URL/internal/identities/upsert" -H "Content-Type: application/json" -H "x-platform-internal-token: $INTERNAL_TOKEN" -d '{"email":"admin@local.test","username":"admin@local.test","password":"ChangeMe123!","enabled":true,"membership":{"productCode":"cs-chat","subjectId":"chat-admin","role":"SUPER_ADMIN"}}'{"identityId":"id_123","subjectId":"chat-admin","email":"admin@local.test","memberships":[{"productCode":"cs-chat","subjectId":"chat-admin","role":"SUPER_ADMIN"}]}400 payload, 409 conflitto subject/email. Consumer: BFF bootstrap utenti.
POST /internal/session/issueEmette access token per subject gia noto.productCode, subjectId, authProvider?.curl -sS -X POST "$AUTH_INTERNAL_URL/internal/session/issue" -H "Content-Type: application/json" -H "x-platform-internal-token: $INTERNAL_TOKEN" -d '{"productCode":"cs-chat","subjectId":"chat-admin","authProvider":"local"}'{"accessToken":"<ACCESS_TOKEN>","expiresAt":"2026-05-10T10:00:00.000Z","identity":{"subjectId":"chat-admin","capabilities":[]}}404 membership/identity assente. Consumer: BFF prodotto.
POST /internal/session/bootstrapUpsert identity/membership e restituisce sessione in una singola operazione.Identity, authProvider, memberships.curl -sS -X POST "$AUTH_INTERNAL_URL/internal/session/bootstrap" -H "Content-Type: application/json" -H "x-platform-internal-token: $INTERNAL_TOKEN" -d '{"email":"admin@local.test","username":"admin@local.test","authProvider":"local","memberships":[{"productCode":"cs-portal","subjectId":"portal-admin","role":"PORTAL_ADMIN"}]}'{"accessToken":"<ACCESS_TOKEN>","identity":{"email":"admin@local.test","memberships":[{"productCode":"cs-portal","subjectId":"portal-admin","role":"PORTAL_ADMIN"}]}}400 membership non valida. Consumer: BFF migration/bootstrap.
POST /internal/session/resolveRisolve Authorization: Bearer in identity normalizzata e capability di prodotto.Header bearer, body productCode?.curl -sS -X POST "$AUTH_INTERNAL_URL/internal/session/resolve" -H "Content-Type: application/json" -H "Authorization: Bearer <ACCESS_TOKEN>" -H "x-platform-internal-token: $INTERNAL_TOKEN" -d '{"productCode":"cs-chat"}'{"authenticated":true,"identity":{"email":"admin@local.test","subjectId":"chat-admin","tenantId":null,"role":"SUPER_ADMIN","permissions":["platform:*"],"capabilities":["platform:mcp:tool:platform_status_overview:call"]}}401 bearer assente/scaduto, 403 membership non valida. Consumer: BFF, Core bearer validation, MCP.
DELETE /internal/memberships/by-subject?productCode=&subjectId=Rimuove membership prodotto per subject.Query productCode, subjectId.curl -sS -X DELETE "$AUTH_INTERNAL_URL/internal/memberships/by-subject?productCode=cs-chat&subjectId=chat-admin" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"deleted":true}400 query mancante, 404 membership assente. Consumer: deprovisioning BFF.

I frontend devono continuare a chiamare il proprio BFF same-origin. Solo backend, worker, status/operations e servizi Core usano direttamente queste route.

Workspace reference: /Users/jeanpaul/projects/cs-repository