Liking cljdoc? Tell your friends :D

Custom login pages

The sign-in, device, error and status pages can be your own. The server owns the flow; your page collects input, keeps what it was handed and posts back. A working starter: example-login-page. Every error and status code with its default text: the console, System → Login pages → Page reference.

Bundles

A bundle is a zip with index.html at its root, uploaded in the console under System → Login pages. An upload is inactive until you preview and activate it. Bundle <name> is served at /login/<name>/.

  • default serves every client without its own bundle, and holds the device.html, status.html, error.html, files.html and account.html pages.
  • An app's Login page setting names its own bundle. Inactive or missing falls back to default, then to the built-in page.

Limits: 8 MB zipped, 50 MB unzipped, web assets only. Pages are served with Content-Security-Policy: connect-src 'self'; frame-ancestors 'none'. A file you leave out falls back to the built-in page. The built-in assets (/oauth/css/*, /oauth/js/*, /oauth/images/*) are yours to reuse.

The one rule: send state back

The browser arrives at /login/<name>/?state=<opaque blob>. state is encrypted and is the CSRF and replay protection. Copy it into a hidden field and post it, unchanged, explicitly to /oauth/login — /login/ itself is GET-only.

index.html — sign-in

POST state, username and password to /oauth/login.

When the app passed an OIDC login_hint, the URL carries it too (?state=…&login_hint=…, trimmed, at most 256 characters): use it to pre-fill the username. It's only a hint from the app, so let the person change it.

  • Plain form post: the server redirects to the app on success. On wrong credentials it shows the built-in page, so your branding is lost.
  • fetch with Accept: application/json and credentials: 'include': you stay on your page and drive navigation.
{"ok": true, "redirect": "<redirect_uri>?code=…&iss=…&state=…"}
{"ok": true, "form_post": {"url": "<redirect_uri>", "params": {…}}}
{"ok": false, "error": "credentials"}

On redirect, set window.location; on form_post, build and submit that form. Errors are 400 with a code: credentials (prompt again), expired_code, already_authorized, device_code_expired, ip_address, user_agent, challenge, broken_flow, unknown.

"Continue with …" buttons. GET /oauth/federated/providers lists the active identity providers (name, label). Each button is a link to /oauth/federated/start?provider=<name>&state=<state>, with the same state. No state in your URL means nobody is signing in: show no buttons.

error.html and status.html

Static pages, both in the default bundle. The server redirects to them with everything in the query.

  • error.html — requests the server must not redirect back to the app. ?error=<code> and ?secure=true|false; on secure=true, warn the person not to enter credentials and show no login link. Some codes add context such as client-name.
  • status.html — where a flow ends with no app to return to: a device login, a cancelled sign-in, an expired code. ?value=success|error|canceled, plus error, flow, user, client_name and provider when they apply.

files.html

Also in the default bundle. A browser that opens a private or missing file link (/files/<id>, /files/path/...) is redirected here with ?error=unauthorized and uri, the link it opened. Static and read-only, like error.html. Without it, the built-in "Sign-in required" page answers with 401. Requests that aren't a browser opening a page (no Accept: text/html, or an Authorization header) get the JSON error as before.

account.html

Also in the default bundle. A browser opening /oauth/account is redirected here, with done or error from the last action and the app's client_id and return (when an app sent the user) in the query; without it, the built-in account page answers. The page calls the account API (OAUTH.md) with fetch: it is served from the engine's own origin, so the sign-in cookie goes along and the cross-site check passes. Signed out, GET /oauth/account answers 401 with login_url: send the browser there, and it comes back to /oauth/account once signed in. Pass client_id and return on to GET /oauth/account: when they check out, the answer has back: {name, uri} for a "Back to " link.

device.html

Answers /oauth/device/activate twice:

  • Entry, no code in the URL: POST user_code to /oauth/device/activate.
  • Confirm, ?user_code=XXXX: show the code and POST action=confirm or action=cancel to /oauth/device/activate. The browser carries a short-lived SameSite=Lax cookie that the server set; no token in the page.
<form method="post" action="/oauth/device/activate">
  <p>Confirm code: <b id="code"></b></p>
  <button name="action" value="confirm">Confirm</button>
  <button name="action" value="cancel">Cancel</button>
</form>
<script>document.getElementById('code').textContent =
  new URLSearchParams(location.search).get('user_code');</script>

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close