Skip to Content
TestingApp Auth Service

App Auth Service

Per-app OIDC authentication with zero setup. Enable auth on an app, and OpenFactory provisions a dedicated OIDC realm, confidential client, and hosted login page. Four environment variables land in your app’s environment, and standard middleware (Express, FastAPI, Next.js) handles the rest.

M1 Status (stub mode): The data plane (App.auth schema, persistence, REST + MCP, env publishing via the producer API) is production-ready. OIDC issuer URLs, JWKS endpoints, and client secrets are fabricated values that point at stub-auth.apps.openfactory.tech. They will not validate real tokens yet. M1.5 swaps in a real provider (Keycloak or Ory) behind the same AuthProviderAdapter interface, with no changes required to app code or callers.

How it works

  1. Enable auth on an app: Call enable_app_auth which creates a realm (e.g. app-myshop) and an OIDC confidential client (of-app-myshop), selects login providers (password, Google, GitHub, etc.), and persists an App.auth block on disk. The client_secret is returned once in the response and never stored on the app record.

  2. Credentials published to app env: The service auto-publishes OF_AUTH_ISSUER, OF_AUTH_CLIENT_ID, OF_AUTH_CLIENT_SECRET, and OF_AUTH_JWKS_URL into the app’s environment via the App Secrets & Env producer API.

  3. App reads env, middleware validates tokens: On next deploy, the app reads /etc/openfactory/app.env and standard OIDC middleware (provided by the Prompt-to-App Agent) validates tokens against OF_AUTH_ISSUER and OF_AUTH_JWKS_URL.

  4. Users sign in at hosted login: End users navigate to auth.apps.openfactory.tech, authenticate via their chosen provider, and return to the app with a valid token. (Stub URLs today; real hosted login in M1.5.)

MCP tools

ToolUse
enable_app_authProvision realm + client and publish OF_AUTH_* into the app env
get_app_auth_statusInspect the current binding (never returns client_secret)
rotate_app_auth_secretGenerate a new client_secret and republish it to the env
disable_app_authTurn auth off; optionally purge the user pool (irreversible)

enable_app_auth

Provision a per-app OIDC realm and client, and auto-publish OF_AUTH_* into the app environment. The client_secret is returned once in the response and never persisted on the App.auth record.

MCP response (includes extra MCP-specific fields):

{ "realm": "app-myshop", "client_id": "of-app-myshop", "issuer_url": "https://stub-auth.apps.openfactory.tech/realms/app-myshop", "jwks_url": "https://stub-auth.apps.openfactory.tech/realms/app-myshop/protocol/openid-connect/certs", "providers": ["password", "google"], "status": "stub", "env_keys": ["OF_AUTH_ISSUER", "OF_AUTH_CLIENT_ID", "OF_AUTH_CLIENT_SECRET", "OF_AUTH_JWKS_URL"], "env_published": true, "session_token": "user-id", "notes": "STUB MODE — no real OIDC provider..." }

When env_published is false (producer token unconfigured), the response additionally includes:

{ "client_secret": "...", "env_payload": { "OF_AUTH_ISSUER": "...", "OF_AUTH_CLIENT_ID": "...", "OF_AUTH_CLIENT_SECRET": "...", "OF_AUTH_JWKS_URL": "..." }, "publish_skipped_reason": "...", "next": "set_app_env(app_id='...', set=<env_payload from this response>) then deploy_app(...)" }

Usage:

enable_app_auth(app_id='app-123', providers=['password', 'google'])

get_app_auth_status

Get the current auth binding for an app including realm, client_id, providers, status, and timestamps. The response never includes client_secret. If you need to inspect the secret, use reveal_app_env with name=‘OF_AUTH_CLIENT_SECRET’ (audited).

MCP response:

{ "enabled": true, "realm": "app-myshop", "client_id": "of-app-myshop", "issuer_url": "https://stub-auth.apps.openfactory.tech/realms/app-myshop", "jwks_url": "https://stub-auth.apps.openfactory.tech/realms/app-myshop/protocol/openid-connect/certs", "providers": ["password", "google"], "status": "stub", "created_at": "2026-06-25T14:30:00Z", "last_rotated_at": "2026-06-25T14:30:00Z", "provider_mode": "stub", "session_token": "user-id" }

Usage:

get_app_auth_status(app_id='app-123')

rotate_app_auth_secret

Generate a new client_secret and republish OF_AUTH_CLIENT_SECRET to the app env via the producer API. Update App.auth.last_rotated_at. The new secret is returned once in the response.

MCP response:

{ "realm": "app-myshop", "client_id": "of-app-myshop", "rotated_at": "2026-06-25T14:35:00Z", "env_published": true, "session_token": "user-id" }

When env_published is false:

{ "realm": "app-myshop", "client_id": "of-app-myshop", "rotated_at": "2026-06-25T14:35:00Z", "env_published": false, "client_secret": "...", "publish_skipped_reason": "...", "next": "set_app_env(app_id='...', set={'OF_AUTH_CLIENT_SECRET': <secret>}) then redeploy...", "session_token": "user-id" }

Usage:

rotate_app_auth_secret(app_id='app-123')

disable_app_auth

Disable auth on the app. Pass purge=true to also delete the user pool (irreversible, all app end-user accounts lost). The OF_AUTH_* env vars are not removed automatically. The response includes the exact follow-up call needed to clean them up.

MCP response:

{ "disabled": true, "purged": false, "realm": "app-myshop", "next": "set_app_env(app_id='app-123', delete=['OF_AUTH_ISSUER', 'OF_AUTH_CLIENT_ID', 'OF_AUTH_CLIENT_SECRET', 'OF_AUTH_JWKS_URL']) to fully remove the credentials from the app env.", "session_token": "user-id" }

Usage:

disable_app_auth(app_id='app-123', purge=false)

REST endpoints

All endpoints are owner-scoped. Authenticate using a JWT in the Authorization header or an X-Guest-Id header. You must own the target app (403 if not owner).

MethodPathPurpose
POST/api/apps/{app_id}/authEnable auth: provision realm + client, return client_secret once
GET/api/apps/{app_id}/authRead the current binding (never returns client_secret)
DELETE/api/apps/{app_id}/auth?purge=trueDisable auth; purge=true also deletes the user pool (irreversible). Does not touch OF_AUTH_* env vars
POST/api/apps/{app_id}/auth/rotate-secretRotate client_secret and republish to env

REST response shapes

POST /api/apps/{app_id}/auth (enable):

{ "realm": "app-myshop", "client_id": "of-app-myshop", "client_secret": "...", "issuer_url": "https://stub-auth.apps.openfactory.tech/realms/app-myshop", "jwks_url": "https://stub-auth.apps.openfactory.tech/realms/app-myshop/protocol/openid-connect/certs", "providers": ["password", "google"], "status": "stub", "notes": "STUB MODE — no real OIDC provider..." }

GET /api/apps/{app_id}/auth (status):

{ "enabled": true, "realm": "app-myshop", "client_id": "of-app-myshop", "issuer_url": "https://stub-auth.apps.openfactory.tech/realms/app-myshop", "jwks_url": "https://stub-auth.apps.openfactory.tech/realms/app-myshop/protocol/openid-connect/certs", "providers": ["password", "google"], "theme": { "display_name": "My Shop", "logo_url": "...", "accent": "..." }, "status": "stub", "created_at": "2026-06-25T14:30:00Z", "last_rotated_at": "2026-06-25T14:30:00Z", "provider_mode": "stub" }

Note: REST GET returns theme (optional dict), MCP status does not.

POST /api/apps/{app_id}/auth/rotate-secret (rotate):

{ "realm": "app-myshop", "client_id": "of-app-myshop", "client_secret": "...", "rotated_at": "2026-06-25T14:35:00Z" }

Note: REST response includes only these 4 fields. MCP adds env_published and session_token.

DELETE /api/apps/{app_id}/auth (disable):

Returns 200 with:

{ "disabled": true, "purged": false, "realm": "app-myshop" }

Note: REST response omits the next field and session_token. The caller is responsible for calling set_app_env to remove OF_AUTH_* vars if needed.

Published environment variables

On every enable and every rotate, the service publishes these keys into the app’s environment via the producer API (when OPENFACTORY_AUTH_PRODUCER_TOKEN is configured):

KeyPurpose
OF_AUTH_ISSUEROIDC issuer URL for token validation and discovery
OF_AUTH_CLIENT_IDPublic client identifier for this app’s realm
OF_AUTH_CLIENT_SECRETConfidential client secret (rotate with rotate_app_auth_secret)
OF_AUTH_JWKS_URLJWKS endpoint for verifying token signatures

Standard OIDC middleware reads these directly from the environment. Do not copy values around or manage .env files manually.

End-to-end example

  1. App owner calls enable_app_auth(app_id='my-app', providers=['password','google']) via MCP or POST /api/apps/my-app/auth.

  2. The service creates the realm (e.g. app-myapp) and an OIDC confidential client. In stub mode the issuer and JWKS URLs are fabricated under stub-auth.apps.openfactory.tech. M1.5 will point them at the real provider.

  3. The App.auth dict is persisted on disk without client_secret. The secret is returned in the response once only.

  4. The MCP tool auto-publishes OF_AUTH_ISSUER, OF_AUTH_CLIENT_ID, OF_AUTH_CLIENT_SECRET, and OF_AUTH_JWKS_URL into the app’s env via the App Secrets & Env producer API. If the producer token is not configured, the caller receives the plaintext secret in the response and must call set_app_env manually.

  5. The owner deploys the app. On startup, the app reads OF_AUTH_* from /etc/openfactory/app.env.

  6. Standard Express / FastAPI / Next.js OIDC middleware (shipped by the Prompt-to-App Agent) validates tokens against OF_AUTH_ISSUER and OF_AUTH_JWKS_URL. End users sign up and log in at auth.apps.openfactory.tech (stub URLs today, real provider in M1.5).

  7. To rotate: call rotate_app_auth_secret(app_id). The new OF_AUTH_CLIENT_SECRET is published to the env via the producer API. Redeploy the app to pick it up.

Key guarantees

  • One realm per app: Each app gets its own user pool. User accounts never leak between apps, and you can delete an app’s user pool independently.

  • client_secret is returned exactly once: On enable and on rotate. After that, only the encrypted environment store holds it. The App.auth record never stores client_secret.

  • Disable does not clear the environment: Calling disable_app_auth flips the auth binding off but leaves OF_AUTH_* env vars in place so a running app does not break immediately. Use set_app_env to remove the vars when you are ready to fully shut down auth on an app.

  • Stub mode is complete end-to-end plumbing: You can wire up code paths, middleware integration, and CI/CD pipelines today. Swapping to the real provider in M1.5 changes only the issuer and JWKS hostnames, not the API contracts.

  • REST and MCP response shapes differ: REST responses omit session_token and env_published fields that are present in MCP responses. When building REST clients, do not expect those fields.

Last updated on