/ Docs
Volcano DocumentationAuthentication

Managed Hosted Auth Pages

Managed hosted auth pages let you serve customizable authentication pages directly from Volcano for each project.

Managed hosted auth pages let you serve customizable authentication pages directly from Volcano for each project.

Configuration

Use PUT /projects/{projectId}/auth/config to control managed pages:

  • managed_auth_enabled: enables public render endpoints
  • allowed_redirect_urls: strict allowlist for post-auth and logout redirects
  • post_auth_redirect_url: must be present in allowed_redirect_urls
  • post_logout_redirect_url: must be present in allowed_redirect_urls

Page types and URLs

Six page types can store appearance and legacy HTML/CSS independently:

  • PUT /projects/{projectId}/auth/hosted-pages/{pageType} updates HTML/CSS
  • GET /projects/{projectId}/auth/hosted-pages/{pageType} returns the current saved content
  • Supported pageType: login, signup, forgot-password, device, verify-email, reset-password

Public Rendering

Hosted pages are served from:

  • GET /projects/{projectId}/auth/hosted (sign in)
  • GET /projects/{projectId}/auth/hosted/signup
  • GET /projects/{projectId}/auth/hosted/forgot-password
  • GET /projects/{projectId}/auth/hosted/device?user_code=<user_code>
  • GET /projects/{projectId}/auth/hosted/verify-email?token=<token>
  • GET /projects/{projectId}/auth/hosted/reset-password (dedicated reset-password page)

Every URL also needs anon_key=<anon_key>. Old sign-in links using ?action=signup|forgot-password|device remain supported.

The endpoint only serves HTML (Accept: text/html) and applies strict security headers. Invalid or unavailable hosted pages return a branded HTML error page with the original HTTP error status.

To start managed auth from your app, redirect users to:

  • GET /projects/{projectId}/auth/hosted?anon_key=<anon_key>

Built-in pages call the auth API from Volcano's API origin, so they do not need an entry in cors_allowed_origins. Custom auth pages run cross-origin and must register their application origin in the project's CORS configuration.

Built-in Smart Login Experience

When no custom login page is configured, hosted login uses an email-first flow:

  • Ask for email first
  • If email exists, prompt for password and sign in
  • If email does not exist, switch to signup fields (full name, password, password confirmation) and create account
  • Show configured OAuth providers if present
  • If email confirmation is required, show confirmation-pending UX after signup/signin conflict until verified
  • If confirmation is not required and post_auth_redirect_url is configured, redirect there after successful authentication
  • action=signup starts directly in signup UI
  • action=forgot-password starts directly in forgot-password UI
  • action=device&user_code=ABCD-EFGH&anon_key=<anon_key> starts device authorization approval UX (requires login in the hosted page)

Device Authorization Approval

Device-code login (RFC 8628) is approved entirely within the managed pages — the API serves the approval UI itself, with no dependency on any external app.

  • POST /auth/device/authorize returns a verification_uri / verification_uri_complete that point at this project's managed page: GET /projects/{projectId}/auth/hosted/device?user_code=<user_code>&anon_key=<anon_key>.
  • The project's default anon key is embedded in that URL automatically so the page can sign the user in; managed auth must be enabled for the project.
  • The page signs the user in (email/password or OAuth) and calls POST /auth/device/verify to approve or deny the user_code. The device then receives its session by polling POST /auth/device/token.

Custom verification page

Projects that host their own RFC 8628 approval page can set device_verification_url on the auth config (PATCH /auth/config). When set, POST /auth/device/authorize returns that URL instead of the managed page, with the user_code appended to verification_uri_complete (e.g. https://app.acme.com/device?user_code=ABCD-EFGH). The custom page is responsible for authenticating the end user and calling POST /auth/device/verify.

Because approval no longer goes through the managed page, device login works even with managed auth disabled when a custom URL is set, and the anon key is not embedded (the page brings its own). The custom page runs on a different origin, so add that origin to the project's auth CORS allowlist (cors_allowed_origins) — otherwise the cross-origin POST /auth/device/verify call is rejected with 403 CORS: origin not allowed. Clear the field (empty string) to fall back to the managed device page.

Helper endpoints used by the built-in login:

  • GET /projects/{projectId}/auth/hosted/login/options?anon_key=<anon_key>
  • POST /projects/{projectId}/auth/hosted/login/check-email with Authorization: Bearer <anon_key>
  • Both endpoints are rate limited per project + client IP and apply exponential backoff before returning 429 Too Many Requests with Retry-After.

Configure built-in appearance

Use the appearance API when a page should keep Volcano's managed markup and runtime while taking on your brand:

  • GET /projects/{projectId}/auth/pages/appearance returns the saved theme, per-page layouts, accepted options, defaults, and per-page parked state.
  • PUT /projects/{projectId}/auth/pages/theme saves the project-wide colours, font, scale, density, and radius. DELETE restores the live default.
  • PUT /projects/{projectId}/auth/pages/{pageType}/layout saves centered, split-left, or split-right. DELETE restores default tracking.
  • POST /projects/{projectId}/auth/pages/{pageType}/preview accepts an unsaved theme and layout and returns preview_url and expires_at. Navigate an iframe or browser tab to preview_url to render the preview before it expires. The document uses the project's current enabled authentication methods, but mocks every authentication request: typing and moving between form states works, while account creation, sign-in, email delivery, and OAuth redirects do not.

Theme and layout customisation requires SUPERAGENT. A downgrade parks saved values: the public page uses defaults and Volcano branding, while the appearance GET and preview endpoints remain available. Upgrading restores the saved values.

The same configuration round-trips through volcano-config.yaml:

auth:
  managed_pages:
    appearance:
      theme:
        version: 1
        colors:
          background: "#f7f5f2"
          surface: "#ffffff"
          text: "#1a1a1a"
          accent: "#1d4ed8"
          accent_text: "#ffffff"
        font: humanist
        scale: large
        density: spacious
        radius: large
      layouts:
        login: split-left
        signup: split-right
        reset_password: centered

Built-in branding

The built-in pages render a modern, self-contained dark theme (a centered card with the project brand in a top navbar). Each step of the smart login flow reuses this layout.

  • Project name is shown as the brand label in the navbar.
  • Project logo is shown next to the brand when one has been uploaded for the project. It is loaded same-origin from GET /projects/{projectId}/logo, so no extra configuration is required — upload a logo and it appears automatically.
  • Styling is fully inline (no external stylesheets or web fonts) so the pages stay within the strict managed-page Content-Security-Policy. System font stacks are used.
  • The "Powered by Volcano" badge is shown only on the HOBBY plan; it is omitted on paid plans.

Custom hosted pages (configured via PUT /projects/{projectId}/auth/hosted-pages/{pageType}) are rendered as-is and are not affected by the built-in branding.

Security Guardrails

  • HTML validation rejects <script> tags
  • javascript: URLs are rejected
  • Inline event handlers are rejected
  • Embedded content tags (iframe, object, embed) are rejected
  • Head tags (meta, link) are rejected
  • CSS cannot include closing </style or </head tag patterns
  • HTML and CSS are each limited to 256 KiB
  • Redirect URLs are validated as HTTP/HTTPS and must pass allowlist checks

Response headers include:

  • Content-Security-Policy with default-src 'none'. script-src is 'none' for custom pages (which cannot contain scripts) and a single 'sha256-<hash>' for the built-in pages' inline script. style-src is 'unsafe-inline', img-src is 'self' data: (so the same-origin project logo can render), connect-src is 'self', and form-action is 'self'.
  • X-Frame-Options: DENY
  • X-Content-Type-Options: nosniff
  • Cache-Control: no-store, no-cache, must-revalidate

On this page