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_IDandGOOGLE_CLIENT_SECRETenv vars are set. To restrict OAuth to specific email domains, setAUTH_ALLOWED_DOMAINS(comma-separated list); an empty value allows any Google account. - Email + Password. Any user record in the
userstable with a populatedpassword_hashcan log in this way. The first Superadmin is created at first boot (see Installation and Setup). Credentials are verified by the engine viaPOST /api/auth/credentials/verify, gated by theAUTH_INTERNAL_SECRETshared 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:
- The user enters the temporary password.
- The login succeeds, but they are immediately redirected to a dedicated full-screen route at
/password-change. - They cannot navigate away from that page until they pick a new password.
- 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_SECRETenv var. - The JWT lands in the
authjs.session-tokencookie (or__Secure-authjs.session-tokenover HTTPS). - The engine uses the same
AUTH_SECRETto 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
mustChangePasswordflag. - When the token expires (default ~30 days), the user is bounced to
/loginwithcallbackUrlpreserved.
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)
-
Sign in to the admin panel.
-
Open DevTools → Application → Cookies → the panel’s origin (
http://localhost:3000). -
Copy the value of
authjs.session-token(or__Secure-authjs.session-tokenif you are on HTTPS). -
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/instancesThe 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):
- Set
AUTH_ALLOWED_DOMAINSin your environment to a comma-separated list (e.g.acme.com,example.org). - 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
jwttodatabaseand rotate theAUTH_SECRETon every revocation.