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
-
Enable auth on an app: Call
enable_app_authwhich 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. -
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.
-
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.
-
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
| Tool | Use |
|---|---|
enable_app_auth | Provision realm + client and publish OF_AUTH_* into the app env |
get_app_auth_status | Inspect the current binding (never returns client_secret) |
rotate_app_auth_secret | Generate a new client_secret and republish it to the env |
disable_app_auth | Turn 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).
| Method | Path | Purpose |
|---|---|---|
POST | /api/apps/{app_id}/auth | Enable auth: provision realm + client, return client_secret once |
GET | /api/apps/{app_id}/auth | Read the current binding (never returns client_secret) |
DELETE | /api/apps/{app_id}/auth?purge=true | Disable auth; purge=true also deletes the user pool (irreversible). Does not touch OF_AUTH_* env vars |
POST | /api/apps/{app_id}/auth/rotate-secret | Rotate 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):
| Key | Purpose |
|---|---|
OF_AUTH_ISSUER | OIDC issuer URL for token validation and discovery |
OF_AUTH_CLIENT_ID | Public client identifier for this app’s realm |
OF_AUTH_CLIENT_SECRET | Confidential client secret (rotate with rotate_app_auth_secret) |
OF_AUTH_JWKS_URL | JWKS 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
-
App owner calls
enable_app_auth(app_id='my-app', providers=['password','google'])via MCP or POST /api/apps/my-app/auth. -
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.
-
The App.auth dict is persisted on disk without client_secret. The secret is returned in the response once only.
-
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.
-
The owner deploys the app. On startup, the app reads OF_AUTH_* from /etc/openfactory/app.env.
-
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).
-
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.
Related
-
App Secrets & Env — the producer API that publishes OF_AUTH_* keys.
-
Prompt-to-App Agent — generates apps with OIDC middleware pre-configured to read OF_AUTH_*.
-
App Templates Gallery — starter templates with App Auth support built in.
-
App Gateway — fronts all apps at *.apps.openfactory.tech.