Login Portal
O2ID ships with an embedded portal that serves the login page at /login,
the consent page at /consent, the
account chooser at /select-account,
and the device verification page at
/device. No extra configuration is needed — it works out of the box when
you run o2id serve.
Hosting the portal externally
You can decouple the portal from the O2ID binary and host it on any static web server or CDN — no need to build a portal yourself. Two steps:
1. Export the portal's static assets. export-portal writes every page in
the embedded portal to a directory, with asset paths rewritten to be relative
so the files work from any location:
o2id export-portal ./portal --api-base-url https://auth.example.com
--api-base-url is O2ID's own base URL. It's baked into config.js so the
exported pages post back to O2ID. Omit it only when the portal is served from
the same origin as O2ID (e.g. behind a reverse proxy), in which case the pages
fall back to their own origin at runtime.
Host the resulting ./portal directory wherever you like.
2. Point O2ID at the hosted portal. Set external_login_portal under
[server] in o2id.toml:
[server]
external_login_portal = "https://login.example.com/login"
This single setting covers the whole portal. O2ID derives every other page's
URL from it by swapping the last path segment (…/login → …/consent,
…/select-account, …/device), so the pages must be hosted together at those
sibling paths — which is exactly how the exported bundle is laid out.
O2ID then redirects unauthenticated authorization requests to the login URL
(passing a transaction_id), consent-requiring authorizations to the
consent URL (passing consent_id plus client_name and scope display
hints), and prompt=select_account requests to the account chooser (passing
transaction_id). The pages post back to O2ID's POST /login,
POST /consent and POST /select-account endpoints respectively.
The account chooser and the device verification page read the browser's session cookie, so they must be hosted on the same origin as O2ID (behind a reverse proxy, say) — a cross-origin portal can serve the login and consent pages, but the browser won't send the session cookie to O2ID from one.
Because the pages talk to O2ID over stable contracts — POST /login
(transaction_id, email, password) and POST /consent (consent_id,
action) — keep the exported portal in step with your O2ID version when you
upgrade, re-exporting it if either flow changes.
See Authorization Code Grant for how the portal fits into the full authorization flow.
Debugging the login/authorize/consent flow
If sign-in isn't behaving the way you expect — an unexpected access_denied,
a scope that isn't granted, or a redirect that doesn't go where you
thought — start the server with --debug to print verbose logs for every
login, authorize, and consent decision:
o2id serve --debug
Or set it persistently in o2id.toml instead of passing the flag every run:
[server]
debug = true
This prints request-flow details — including email addresses and raw form
data — to help pinpoint exactly which step diverged from what you expected
(e.g. a hosted portal page posting action under a different field name
than O2ID expects), so don't enable it in production.