Skip to Content
Polyant is open source under AGPL-3.0 — star us on GitHub.
Admin PanelAuthentication

Authentication

Polyant uses an industry-standard authentication stack: Auth.js (formerly NextAuth.js) v5 in the admin panel, with sessions delivered as encrypted JWTs (JWE) carried in cookies. The engine validates the same JWTs on every API call.

Login methods

Two login methods are configured out of the box:

  • Google OAuth. Optional — the Google sign-in button is rendered only when both GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET env vars are set. To restrict OAuth to specific email domains, set AUTH_ALLOWED_DOMAINS (comma-separated list); an empty value allows any Google account.
  • Email + Password. Any user record in the users table with a populated password_hash can log in this way. The first Superadmin is created at first boot (see Installation and Setup). Credentials are verified by the engine via POST /api/auth/credentials/verify, gated by the AUTH_INTERNAL_SECRET shared between the web package and the engine.

Both methods land on the same admin panel after login.

First-login flow for invited users

When a Superadmin invites a new user (see Users), the system generates a temporary password and marks the user record with mustChangePassword = true. On their first login:

  1. The user enters the temporary password.
  2. The login succeeds, but they are immediately redirected to a dedicated full-screen route at /password-change.
  3. They cannot navigate away from that page until they pick a new password.
  4. After saving, the flag clears and they can use the rest of the panel.

Session lifecycle

  • On successful login, Auth.js issues a JWE-encrypted JWT signed with the AUTH_SECRET env var.
  • The JWT lands in the authjs.session-token cookie (or __Secure-authjs.session-token over HTTPS).
  • The engine uses the same AUTH_SECRET to decrypt the JWT on every API call. There is no per-request database lookup for the session.
  • The token’s payload includes the user id, role, and a mustChangePassword flag.
  • When the token expires (default ~30 days), the user is bounced to /login with callbackUrl preserved.

Get a JWT for API calls

Most how-to recipes (skills, secrets, LangSmith, instance export/import) hit the management API with curl and assume you already have a JWT. There is no dedicated “issue token” endpoint — the same JWE that backs the admin panel session is what the engine accepts. Two ways to grab it:

Option A — copy it from the browser (quickest)

  1. Sign in to the admin panel.

  2. Open DevTools → ApplicationCookies → the panel’s origin (http://localhost:3000).

  3. Copy the value of authjs.session-token (or __Secure-authjs.session-token if you are on HTTPS).

  4. Export it as TOKEN:

    export TOKEN='eyJhbGciOi...' # paste the cookie value

The engine accepts the same JWE either as a Bearer header or as a cookie. Both of the following work:

# As an Authorization header curl -H "Authorization: Bearer $TOKEN" http://localhost:4000/api/instances # As a cookie (matches the admin panel verbatim) curl -H "Cookie: authjs.session-token=$TOKEN" http://localhost:4000/api/instances

The Bearer form is the one you want for scripts; the cookie form is what the admin panel sends and is occasionally handier when you are debugging by replaying a request from the browser. The __Secure-authjs.session-token cookie name (HTTPS deployments) is also accepted.

Option B — script the credentials flow

Auth.js’s credentials provider is what powers the email + password login. The handshake is two requests (fetch a CSRF token, then POST credentials), and the JWE comes back in a Set-Cookie header:

BASE=http://localhost:3000 # 1) Grab a CSRF token (Auth.js double-submit cookie pattern) CSRF=$(curl -s -c /tmp/cookies.txt "$BASE/api/auth/csrf" | jq -r .csrfToken) # 2) Submit credentials. The session-token cookie lands in /tmp/cookies.txt. curl -s -b /tmp/cookies.txt -c /tmp/cookies.txt \ -X POST "$BASE/api/auth/callback/credentials" \ -d "csrfToken=$CSRF&email=$EMAIL&password=$PASSWORD&redirect=false" \ -H "Content-Type: application/x-www-form-urlencoded" \ >/dev/null # 3) Extract the JWE. Auth.js sets the session cookie as HttpOnly, which # in Netscape cookie-file format is encoded as a "#HttpOnly_<domain>" # prefix on the same row — it is *not* a comment line, so the line # must be included (don't skip rows that start with '#'). TOKEN=$(awk -F '\t' '$6 == "authjs.session-token" { print $7 }' /tmp/cookies.txt) echo "TOKEN=$TOKEN"

Use Option B in CI or unattended jobs. Tokens issued this way share the same ~30 day lifetime as panel sessions — long-running automations should plan to refresh.

The token is bearer-equivalent — anyone with it can act as the issuing user until expiry (JWT cannot be revoked early without switching session strategy; see the trade-off note below). Treat it like a password: never check it into git, never paste it into shared chats, scope your shell history accordingly.

Self-hosters: changing the Google OAuth domain

To restrict Google OAuth to a specific email domain (or a list of domains):

  1. Set AUTH_ALLOWED_DOMAINS in your environment to a comma-separated list (e.g. acme.com,example.org).
  2. Restart the admin panel.

Without a value, Google OAuth is fully open: any Google user can become a Polyant user — including ones you did not invite. Pair this only with a strict role policy and a Superadmin who reviews accounts. To disable Google sign-in entirely, leave GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET unset — the button disappears.

What logging out does

Clicking Log out deletes the session cookie. The user lands on /login. No server-side session record needs to be invalidated, because there is no server-side session record — the JWT is the session.

Trade-off. With pure JWT sessions there is no way to instantly revoke a stolen token before it expires. If revocation matters for your deployment, switch the Auth.js strategy from jwt to database and rotate the AUTH_SECRET on every revocation.

Last updated on