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.

The four ways a request is authenticated

The admin panel session is only one of them. The engine accepts:

MechanismWho it isUsed for
Auth.js session — cookie or Authorization: Bearera userthe admin panel, and scripted management calls
Gateway identityx-amzn-oidc-dataa user authenticated upstreamdeployments fronted by an AWS ALB with authenticate-oidc
Management API keyX-Polyant-Keya service principalmachine-to-machine management calls. See API keys
Per-agent keyAuthorization: Bearera caller of one agent/v1/chat/completions and the HTTP channel

Gateway mode is selected with AUTH_MODE=alb-oidc, and it is a deliberate trade: the header’s signature is not re-verified, so the network boundary is the trust boundary — the engine’s security group must accept traffic only from the ALB, or the header can be spoofed.

Authorization is a separate question

Being signed in says nothing about what you may do. Every management route declares a required permission, resolved against your organization role and — on enterprise deployments — your workspace bindings. See Roles and permissions.

Enforcement is on by default and fails closed: a route that declares no permission is denied rather than allowed. It can be dropped to log-only for local testing with REMOVE_AUTHORIZATION_FILTER_FOR_TESTING=true, and that flag is ignored when NODE_ENV=production — enforcement in production cannot be turned off by configuration.

Platform superadmin

A platform superadmin bypasses role resolution entirely, ahead of every permission check. It is a flag on the user, granted in one of two ways:

  • PLATFORM_ADMIN_EMAIL — the user with that email is promoted on every boot, idempotently. Setting it before that user exists is fine; the promotion applies once they sign in.
  • AUTH_ALB_SUPERADMIN_GROUP (gateway mode only) — the cognito:groups claim value that grants it. Unset, every gateway identity is an ordinary user.

This is a platform-level capability, distinct from the Owner role inside an organization and distinct again from the local superadmin account role used by Settings → Users.

First-login flow for a new local account

When a platform superadmin creates a sign-in account (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.

On enterprise deployments there is a second, better path that never involves an operator handling someone else’s password: an invitation link, where the invitee creates their own credentials and lands directly in the organization with the role you chose.

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.

Note what an open sign-up does not grant: a brand-new account holds no organization binding, and no binding means no access — the permission model denies by default. It is an account, not a foothold. Still, review Members rather than relying on that alone.

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