Sign-in for Ignite apps
If your app runs on Ignite , this is the integration to use. Each person on your customer’s team gets their own account in a pool and signs in as themselves. The Ignite gateway handles the whole login (PKCE, the session cookie, refresh, JWT verification) and passes your app an identity it has already verified. Your app contains no login code and no JWT library.
This is not the Dodil connection you use to build.
dodil auth login, the CLI, and the MCP server sign you, the builder, into your Dodil org. Your app’s users never touch any of that. They live in an AppID pool, a separate identity plane (see Core Concepts). A pool user can’t reach your buckets or apps directly, and nobody shares a login.
1. Create the pool and its people
dodil appid pool create acme --display-name "Acme" --with-local
# role -> permission catalog (roles set REPLACES the catalog: get, merge, set)
dodil appid roles set acme rep=deals:read,deals:write manager=deals:read,deals:write,deals:approve
# a business system: no self-signup, invite each person
dodil appid settings set acme --allow-signup=false
dodil appid user create acme [email protected] --roles rep # no --password: they get an invite/reset emailWant Sign in with Google (or Microsoft, or another OpenID Connect provider) as well? Add the connection; see CLI → connection. The hosted page then shows a “Continue with …” button.
With allow_signup off, nobody can create their own account. Invite at least one person, or the login screen will work and still let nobody in.
2. Attach the pool to the app
In .dodil/deploy.yaml:
apps:
- name: acme-app
user_pool: acme # "pool" (same org) or "org/pool"; "" detaches; absent = no change
public_invoke: false # false = sign-in required (applied only when the app is first created)public_invoke in deploy.yaml takes effect only when the app is created. To change it later, use dodil ignite app update acme-app --allow-unauthenticated=<true|false>. The pool must already exist.
Or on an existing app:
dodil ignite app update acme-app --user-pool <org>/acmeuser_pool and public_invoke (--allow-unauthenticated) together decide who gets in:
user_pool | public_invoke | Who can call the app |
|---|---|---|
| set | false | Sign-in required. Anonymous requests get 401 plus a login_url. Platform principals (your service accounts, CI, dodil ignite invoke) still work. |
| set | true | Optional sign-in. Anonymous requests pass. Identity is injected when a session or pool token is present, and the app decides. Required for an app with an /mcp endpoint (see below); the app’s own gate then refuses identity-less requests except /healthz and its discovery metadata. |
| unset | false | Platform principals only. |
| unset | true | Fully public, no identity. |
The pool adds sign-in. It never removes platform access.
3. Allowlist the callback
The gateway signs people in with the redirect URI https://<public_url>/.dodil/auth/callback. A pool with no redirect_uris signs nobody in.
dodil ignite app get acme-app -o json # copy the host from public_urls; never build it by hand
dodil appid settings get acme -o json # settings set REPLACES the object: read first
dodil appid settings set acme --allow-signup=false \
--redirect-uris https://<public_url>/.dodil/auth/callbackRegistering the callback is a manual step today. Several apps on one domain can share one entry with a wildcard label, e.g. https://*.apps.example.com/.dodil/auth/callback (one label only; see Redirect URIs).
Copy the host from public_urls, because it escapes some characters (an org named acme-co appears as acme--co). If the URI is missing, the person signing in sees “redirect_uri is not in the pool’s allowlist”.
4. Read the identity in your handler
On every request that passes the check, the gateway strips any inbound copy of these headers and then sets them itself:
| Header | Contents |
|---|---|
X-Dodil-User | JSON: sub, email, connection, app_roles |
X-Dodil-User-Jwt | The pool access token the gateway verified. The expanded permissions claim (and tenant, when pinned) is only here. Decode it, or verify it again against the pool JWKS if you prefer. |
X-Dodil-Auth-Source | pool (a person from the pool) or platform (a service account or other platform principal) |
import base64, json
from fastapi import Request, HTTPException
def claims(request: Request) -> dict:
"""The signed-in person. Already verified by the gateway; do not verify again."""
if request.headers.get("X-Dodil-Auth-Source") != "pool":
raise HTTPException(401, "sign-in required")
body = request.headers["X-Dodil-User-Jwt"].split(".")[1]
body += "=" * (-len(body) % 4)
return json.loads(base64.urlsafe_b64decode(body)) # sub, email, permissions, tenants, tenant…
def require(request: Request, perm: str) -> dict:
c = claims(request)
if perm not in c.get("permissions", []):
raise HTTPException(403, f"missing {perm}")
return cBecause the gateway removes forged copies, a header sent by a client never reaches your app. A local test server does not strip them, so a test suite that sets X-Dodil-User by hand passes even when production would refuse the request. Check once on the deployed URL (see Verify).
The browser only ever holds the gateway’s HttpOnly, host-only session cookie, so no token is exposed to page JavaScript. Serve the UI and the API from one app (one host). If you split them across two hosts, the cookie stops reaching the API.
5. Verify with a real person
dodil ignite invoke and service-client tokens both bypass the browser login, so they can’t show that sign-in works. Before you call the app done:
- Open
https://<public_url>/in a private window. You should be sent to the pool’s sign-in page. - Sign in as an invited user. You should land back in the app as that person.
- Sign in as a user with a different role and confirm they see only what that role allows.
If you can’t hand someone a URL they can sign in to, the app is not verified.
Other callers
- Another app calling this one: use a pool service client, with a role that holds only what that call needs, and send its
client_credentialstoken asAuthorization: Bearer:dodil appid service-client create acme --client-id billing-sync --role sync - A person connecting Claude.ai or ChatGPT to the app’s
/mcp: see the next section. - Apps not hosted on Ignite: there is no gateway in front of them, so verify the pool token yourself. See Verify tokens in your backend.
A person connecting Claude.ai or ChatGPT
Each person can connect their own assistant to the app’s /mcp and sign in as themselves (authorization code + PKCE against the pool), so their agent sees exactly what they see. Nobody shares a token.
| Who | Does what |
|---|---|
| The app | Serves GET /.well-known/oauth-protected-resource/mcp (RFC 9728 ) naming the pool issuer https://appid.dodil.io/<org>/<pool> as its authorization server. Answers an unauthenticated /mcp with 401 and WWW-Authenticate: Bearer resource_metadata="<base>/.well-known/oauth-protected-resource/mcp". Gates every other route on X-Dodil-User. |
| The Ignite gateway | Verifies the pool bearer token the assistant presents and injects X-Dodil-User / X-Dodil-User-Jwt, exactly as for a browser session. It does not serve the discovery metadata. |
| AppID | Runs the sign-in. Two ways in. Dynamic registration (the default): the assistant registers itself and starts out pending; until you approve it, the person trying to connect gets an error page. Approve with dodil appid client list acme, then dodil appid client approve acme <client-id>. Client metadata URL (CIMD): turn on cimd_enabled in the pool settings (off by default; see settings) and assistants identify themselves by a metadata URL and skip the approval step. Claude picks this automatically; ChatGPT uses it with private_key_jwt. Each person approves the assistant once on a consent screen and can withdraw it at {issuer}/user/consents. client suspend cuts an assistant off and revokes those consents. |
The app must be user_pool + public_invoke: true. With public_invoke: false, the gateway refuses the assistant’s anonymous discovery requests before the app can answer them.
The token carries that person’s roles and permissions, and is bound to your MCP server when the assistant asks for it by resource. Verified with Claude.ai against the reference CRM (AppID 0.15.1).
You don’t have to write this from scratch. The ERP platform brick (erp-platform, bricks.py: the protected-resource metadata and the sign-in gate) implements it, and it runs in the reference CRM.
Next
The Ignite side of this (the flags, the reserved /.dodil/auth/* paths, the API fields) is in Ignite → Access & sign-in .
Sign-in tells you who the person is. Making sure they only see their data is the next step, covered in Keep each customer’s data separate.