Skip to content

Authentication & authorization

Jmaple separates the two concerns cleanly:

  • Authentication — "who is calling?". Pluggable, multiple providers can run in a chain.
  • Authorization — "what can they reach?". One unified accounts/grants layer in the database; admin is just a grant on the special __system__ account.

Providers

Three are bundled:

Provider When to use
bearer Opaque DB-backed tokens minted via jmaple token issue. Easiest for service-to-service and bootstrap.
jwt Validate JWTs signed by an external IdP. Works with static keys, public keys, or JWKS endpoints.
oidc Discovery-based OpenID Connect. Validates access tokens and optionally fetches userinfo.

Chain them in .env:

JMAPLE_AUTH__PROVIDERS=["bearer", "jwt"]

The chain walks providers in order; the first one to return a Principal wins. A Principal is the verified identity — issuer + subject + provider name + verified claims.

Identity resolution

The IdentityResolver sits between the auth chain and authorization. Given a Principal, it:

  1. Looks up external_identities WHERE issuer = ? AND subject = ?.
  2. If found, returns the linked jmaple User.
  3. If not found, consults the adapter's auto-provisioning policy:
  4. off — refuse (returns None, surfaces as 403)
  5. by-subject — create a new user with a personal account
  6. by-verified-email — link to an existing user with the same email if email_verified is true; otherwise create new

One jmaple User can hold many ExternalIdentity rows. A user logged in via workforce SSO can later authenticate via personal Google as long as the verified emails match — they end up as the same User with two identities.

Bootstrap admins

Set JMAPLE_AUTH__ADMIN_SUBJECTS to a list of <issuer>|<subject> strings. Anyone whose composite id matches gets auto-provisioned with the admin role on the system account, even when auto_provision is otherwise off.

JMAPLE_AUTH__ADMIN_SUBJECTS=["https://idp.example.com/|alice"]

This is the only way to bootstrap the first admin from an external IdP without writing SQL.

Accounts and grants

A JMAP Account is "a collection of data" (RFC 8620 §2). Every capability's data is scoped to an account. A User reaches an account through a Grant that gives them a role (owner, read-write, read-only, or admin for the system account).

The session resource's accounts map shows every account a principal can reach. The primaryAccounts map says which account is "the main one" for each capability — used by clients that want a single default.

Bearer-token rotation

Opaque tokens carry a non-secret prefix (first 12 chars of the secret) stored cleartext in the tokens table. The provider filters by prefix before doing the expensive Argon2 verify, making lookup O(few) instead of O(all tokens).

Legacy tokens issued before this feature have token_prefix = NULL and fall back to the full-scan path. Run jmaple token rotate <id> to re-issue any legacy token with a populated prefix.