Configure Authentication
This guide covers the operator-facing side of OAuth for JMAP clients: whether external clients may register themselves, what protects that endpoint, and the one deployment mistake that silently breaks discovery.
For how the mechanism works and why, see OAuth Discovery & Dynamic Client Registration.
Enable or disable dynamic client registration
Section titled “Enable or disable dynamic client registration”Dynamic Client Registration (DCR) lets a mail client call POST /oauth/register and obtain its own OAuth client ID without an operator issuing one by hand. It is enabled by default.
CloudFormation deployments
Section titled “CloudFormation deployments”The stack exposes a single parameter:
| Parameter | enableDCR |
| Console label | Dynamic Client Registration |
| Console group | OAuth & Registration |
| Allowed values | enabled, disabled |
| Default | enabled |
Set it when creating or updating the stack. With disabled, external mail clients must be registered manually instead.
CDK deployments
Section titled “CDK deployments”Consumers of the CDK constructs set the equivalent prop:
dynamicClientRegistration: DynamicClientRegistrationMode.ENABLED// ordynamicClientRegistration: DynamicClientRegistrationMode.DISABLEDWhat disabling actually does
Section titled “What disabling actually does”Disabling DCR is not a feature flag checked at request time — no DCR resources are created at all. There is no registration Lambda, no route, and nothing to reach.
The visible consequence for clients is that registration_endpoint is absent from the openid-configuration discovery document. A well-behaved client reads that absence as “self-registration is unavailable here” and falls back to asking the user for a client ID.
Abuse controls
Section titled “Abuse controls”POST /oauth/register is unauthenticated by design — RFC 7591 open registration, which is what lets a client finish setup before the user has signed in. Four controls apply, in this order:
| Order | Control | Detail |
|---|---|---|
| 1 | API Gateway per-route throttle | Burst 5, rate 2 — applied at the edge, before any Lambda runs |
| 2 | Body and redirect-URI validation | Malformed bodies and disallowed redirect URIs rejected without touching Cognito |
| 3 | Permanent dedup | Same redirect URIs + client name returns the existing client_id; no second Cognito client |
| 4 | Per-tenant cap | 20 new registrations per rolling ~24 hours |
Only genuinely new registrations count against the cap. A client that re-registers on every launch, or a user who reinstalls an app, is deduplicated and consumes no slot — which is what keeps the cap from being hit by ordinary use.
A tenant that exceeds the cap receives HTTP 429. The window is rolling, so the condition clears on its own.
Protecting the .well-known namespace
Section titled “Protecting the .well-known namespace”The reserved namespace
Section titled “The reserved namespace”RFC 8615 reserves /.well-known/ as a path prefix for well-known URIs. It is not application-routable space. Nothing in your deployment should ever rewrite, redirect, or catch-all within it.
The trap
Section titled “The trap”A very common SPA hosting pattern maps 403/404 responses to /index.html with a 200 status so client-side routing works on deep links. You will find it in:
- CloudFront
CustomErrorResponses - S3 static-website error documents
- nginx
try_files ... /index.html - Netlify and Vercel catch-all rewrites
If the host serving your .well-known paths sits behind any of these, a discovery request returns 200 with text/html — the SPA shell — instead of the expected JSON.
Why this is worse than a 404
Section titled “Why this is worse than a 404”A 404 makes a client fail fast and unambiguously: discovery is not available here.
A 200 with an HTML body makes the client believe discovery succeeded, and it then fails while parsing the response. The resulting error points at the client — a parse error, a malformed-response message — rather than at the deployment that caused it. Operators chase the wrong component, and client authors receive bug reports for a server misconfiguration.
When this applies to you
Section titled “When this applies to you”Only when you put something custom in front of the JMAP host. In a default deployment the discovery documents are served by explicit API Gateway routes and an unmatched /.well-known/* path returns a genuine 404.
The risk appears when you:
- place a CDN or reverse proxy in front of the JMAP domain,
- serve the webmail SPA and the JMAP API from the same hostname, or
- terminate the JMAP domain on a static-site host.
The rule
Section titled “The rule”Exclude
/.well-known/*from every catch-all rewrite and SPA error-page mapping, and place that exclusion ahead of the fallback rule.
Ordering matters: a fallback rule evaluated first will already have rewritten the request by the time an exclusion further down is considered.
Verify it
Section titled “Verify it”Run both checks against your deployment, replacing <jmap-host> with the hostname serving your JMAP API.
Check 1 — the document is served correctly:
curl -sI https://<jmap-host>/.well-known/openid-configurationExpect HTTP/2 200 and content-type: application/json.
Check 2 — the namespace is not being swallowed:
curl -so /dev/null -w '%{http_code}\n' https://<jmap-host>/.well-known/does-not-existExpect 404.
Related
Section titled “Related”- OAuth Discovery & Dynamic Client Registration — how the mechanism works, endpoint and error reference
- Setup Email Client — the end-user side
- Deployment Checklist — pre-deployment verification