Login Portal
O2ID ships with an embedded portal that serves both the login page at /login
and the consent page at /consent. 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 the embedded
portal (both the login and consent pages) 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 the consent page URL
from it by swapping the last path segment (…/login → …/consent), so both
pages must be hosted together at those sibling paths — which is exactly how the
exported bundle is laid out (login.html and consent.html side by side).
O2ID then redirects unauthenticated authorization requests to the login URL
(passing a transaction_id), and consent-requiring authorizations to the
consent URL (passing consent_id plus client_name and scope display
hints). The pages post back to O2ID's POST /login and POST /consent
endpoints respectively.
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.