Skip to Content
We are live but in Staging πŸŽ‰
Auth and Access

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:

RoleCan
appid.adminEverything β€” create/delete pools, rotate keys, manage connections, policies, users, user stores
appid.viewerRead β€” get/list pools, connections, users
org ownerAlways 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:

  1. 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.
  2. Direct grants β€” POST {issuer}/token with grant_type=password or refresh_token, for apps that own their login UI. JSON and form-encoded both accepted.
  3. Self-service β€” {issuer}/signup, {issuer}/recover (+ email link to /reset), {issuer}/verify for 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:

ActionBudget
Login attempts10 / min / email
Signups60 / min
Password-recovery emails5 / hour / email
/authorize opens120 / 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.