Auth and Access
AppID has two doors with two different locks. Keep them straight and everything else follows.
Control plane β managing pools and users
Everything under dodil appid β¦ / https://api.dodil.io/v1/appid/β¦ is platform-authenticated: you sign in with dodil auth login and the gateway resolves your organization from your token. There is no --org flag and no organization field in any request.
Access is role-based:
| Role | Can |
|---|---|
appid.admin | Everything β create/delete pools, rotate keys, manage connections, policies, users, user stores |
appid.viewer | Read β get/list pools, connections, users |
| org owner | Always everything for their orgβs pools, with no explicit grant |
Grant the roles with platform IAM (dodil auth user grant-role), the same way as any other product role.
Fine-grained scopes behind the roles (what a permission check is actually named): CreatePool, GetPool, ListPools, DeletePool, RotatePoolKeys, ManageConnections, ListConnections, ManagePolicies, ManageUserStore, ManageAppUsers, ListAppUsers.
App plane β how end users authenticate
End users never see any of the above. They authenticate against the pool issuer (https://appid.dodil.io/{org}/{pool}), which is public and unauthenticated by design:
- Authorization code + PKCE (recommended) β your app redirects to
{issuer}/authorize, the user signs in on the hosted page, your app exchanges the returned code at{issuer}/token. PKCE S256 is mandatory; plain and no-PKCE are refused. Tokens never pass through the browser URL. - Direct grants β
POST {issuer}/tokenwithgrant_type=passwordorrefresh_token, for apps that own their login UI. JSON and form-encoded both accepted. - Self-service β
{issuer}/signup,{issuer}/recover(+ email link to/reset),{issuer}/verifyfor email confirmation.
The pool mints EdDSA JWTs your backend verifies locally against the pool JWKS β checking iss and aud β with no call back to AppID. Full endpoint contract: API Reference β Issuer API.
Redirect URIs are an allowlist
{issuer}/authorize redirects only to URIs listed in the poolβs settings.redirect_uris. An unlisted redirect_uri gets an error page, never a redirect β so a stolen client_id canβt exfiltrate codes to an attackerβs site.
CORS is per-pool and exact
Browser JavaScript may call the JSON endpoints only from origins listed in policies.cors_allowed_origins. Matching is exact (scheme + host + port); no wildcard subdomains. No configured origins β no CORS headers β browser calls fail. Tokens are sent as Authorization: Bearer β AppID sets no auth cookies on your appβs behalf.
Anti-enumeration posture
Responses are shaped so an outsider canβt probe your user base: recover always answers 200 whether or not the email exists; signing in to a tenant youβre not a member of fails exactly like a wrong password.
Rate limits
Enforced per pool on the public surface:
| Action | Budget |
|---|---|
| Login attempts | 10 / min / email |
| Signups | 60 / min |
| Password-recovery emails | 5 / hour / email |
/authorize opens | 120 / min / client IP |
Exceeding a budget returns 429; budgets are counted cluster-wide.
What AppID deliberately does not have
- No API keys or service accounts for app users β machine identities belong to platform IAM. If your server needs to call Dodil products, use a
dk_key or service account from IAM, not a pool token. - No pool-token access to Dodil products β a pool JWT is only meaningful to your app.
- Password policy is minimum length only (8β128, per-pool) β there are no character-class rules today.