Skip to content

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.

The stack exposes a single parameter:

ParameterenableDCR
Console labelDynamic Client Registration
Console groupOAuth & Registration
Allowed valuesenabled, disabled
Defaultenabled

Set it when creating or updating the stack. With disabled, external mail clients must be registered manually instead.

Consumers of the CDK constructs set the equivalent prop:

dynamicClientRegistration: DynamicClientRegistrationMode.ENABLED
// or
dynamicClientRegistration: DynamicClientRegistrationMode.DISABLED

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.


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:

OrderControlDetail
1API Gateway per-route throttleBurst 5, rate 2 — applied at the edge, before any Lambda runs
2Body and redirect-URI validationMalformed bodies and disallowed redirect URIs rejected without touching Cognito
3Permanent dedupSame redirect URIs + client name returns the existing client_id; no second Cognito client
4Per-tenant cap20 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.


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.

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.

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.

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.

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.

Run both checks against your deployment, replacing <jmap-host> with the hostname serving your JMAP API.

Check 1 — the document is served correctly:

Terminal window
curl -sI https://<jmap-host>/.well-known/openid-configuration

Expect HTTP/2 200 and content-type: application/json.

Check 2 — the namespace is not being swallowed:

Terminal window
curl -so /dev/null -w '%{http_code}\n' https://<jmap-host>/.well-known/does-not-exist

Expect 404.