Skip to main content
Version: Next

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.