Authentication
Three kinds of principal, three mechanisms, and the one that cannot step up.
Four principal types, three authentication mechanisms.
| Principal | Authenticates with | Reaches |
|---|---|---|
USER |
Password, MFA, or SSO → session | Control plane |
API_KEY |
A bearer key | Control plane |
AGENT |
A signed Ed25519 assertion → short-lived token | Gateway only |
SYSTEM |
Internal | Background jobs |
principal.type is a policy field, so a rule can apply to automation but not to
people, or the reverse.
#Human sessions
httpPOST /v1/auth/signup # creates an organization and its first owner, one transaction
POST /v1/auth/login
POST /v1/auth/mfa/verify
POST /v1/auth/step-up
GET /v1/auth/session
POST /v1/auth/logout
Login returns a session token, or an MFA challenge to complete first.
bashcurl -s $CP/v1/auth/session -H "Authorization: Bearer $SESSION"
Returns the current session, its scope, and the effective permissions — the authoritative answer to "what can I do", and what the console uses to decide what to render.
| Setting | Default |
|---|---|
SESSION_TTL_SECONDS |
43200 (12 hours) |
SESSION_IDLE_TIMEOUT_SECONDS |
3600 |
STEP_UP_TTL_SECONDS |
300 |
MAX_LOGIN_ATTEMPTS |
10 |
LOGIN_LOCKOUT_SECONDS |
900 |
Signup creating the organization and its first owner in one transaction means there is no window in which an organization exists with nobody able to administer it.
#Step-up
httpPOST /v1/auth/step-up
{ "password": "…" }
Thirteen permissions require a fresh re-authentication at the moment of use,
regardless of session age. Without a current step-up they return
403 STEP_UP_REQUIRED.
A twelve-hour session is a reasonable credential for reading dashboards and not for revoking a production agent. → Step-up
#CLI
bashveefore login
Sign-in happens in the browser, never in the terminal. The CLI opens the
console, you sign in there (password, MFA or SSO, exactly as for the console)
and approve the terminal. The CLI receives a session token over a loopback
redirect protected with PKCE (S256), so the terminal never sees your password.
| Situation | What happens |
|---|---|
| A browser on the same machine | Opens it and waits for your approval |
Over SSH, no display, or --device |
Prints a link and a short code to enter in any browser |
--password |
Asks for email and password in the terminal, without echo |
Only the session token is stored, in ~/.veefore/credentials.json at mode
0600. Revoke it from your profile in the console like any other session.
veefore logout ends the session on the server as well as removing the stored
token. Signing out of the console asks whether to also sign out of the Veefore
CLI on your devices, ticked by default; that ends every terminal session you
approved through browser sign-in. A terminal cannot be told the moment that
happens, so the next command there reports that you were signed out, removes
the stored token and asks you to sign in again. Sessions signed in with
VEEFORE_EMAIL and VEEFORE_PASSWORD, as in CI, are not affected.
The CLI connects to Veefore by itself. For a self-hosted Veefore, add
--control-plane https://veefore.your-company.com the first time; later
commands reuse it.
For CI, where nobody can open a browser, prefer an API key. If a pipeline must sign in as a user, set the credentials in the environment and the CLI uses them without prompting:
bashVEEFORE_EMAIL=ci@example.com VEEFORE_PASSWORD=… veefore login
#API keys
httpAuthorization: Bearer aos_sk_…
For a service or a pipeline. Scoped to a role, optionally to a workspace, with an expiry up to 730 days.
bashcurl -X POST $CP/v1/api-keys \
-H "Authorization: Bearer $KEY" -H 'content-type: application/json' \
-d '{ "name": "ci-policy-gate", "role_id": "role_01JAV…", "workspace_id": "wsp_01JAV…", "expires_in_days": 90 }'
Shown exactly once, as `token`
Only a hash is stored and there is no endpoint that returns it again. The field
is token; api_key is the metadata record.
An API key cannot step up
Step-up requires a human re-authenticating, and a key has no human. A key
holding policy:activate still cannot activate a policy.
Which means CI cannot activate a policy, revoke an agent, rotate a credential or execute an emergency control — regardless of its role. Deliberate: activation moves the authorization boundary for every agent in scope. It is a decision, not a build step.
Pick the smallest role. Never OWNER — it holds organization:delete, and a CI
secret is readable by every build step. → API keys
#Agent authentication
Agents authenticate at the gateway, with a signed assertion. No passwords, no API keys, no human credentials.
httpPOST /gateway/v1/token
{
"agent_id": "agt_01JAV…",
"assertion": "eyJ0eXAiOiJhZ2VudG9zLWFzc2VydGlvbit…",
"sdk": { "name": "@veefore/sdk", "version": "1.0.0", "runtime": "node20" }
}
json{
"data": {
"token": "aos_at_…",
"token_type": "Bearer",
"expires_in": 900,
"expires_at": "2026-10-03T12:53:04.000Z",
"agent_id": "agt_01JAV…",
"environment_id": "env_01JAV…",
"organization_id": "org_01JAV…"
}
}
Or POST /gateway/v1/handshake, which also returns the authorized tool list,
the capabilities, the enforcement mode and the passport version.
#What is checked
| Check | Refusal |
|---|---|
| Signature against the stored public key | AGENT_AUTHENTICATION_FAILED |
aud matches the published audience, as a string |
AGENT_AUTHENTICATION_FAILED |
| Timestamp inside the window | TIMESTAMP_OUT_OF_WINDOW |
| Nonce not seen before | NONCE_REPLAYED |
| Key generation current | IDENTITY_GENERATION_STALE |
| Identity not revoked | AGENT_IDENTITY_REVOKED |
The audience is compared as a string
http://127.0.0.1:4010 and http://localhost:4010 are the same host and
different values. This is the most common onboarding failure and it presents as
a bare 401.
bashcurl -s https://gateway.veefore.example.com/.well-known/veefore-platform-key
Use the audience from that response verbatim.
#No refresh token
| Token lifetime | AGENT_TOKEN_TTL_SECONDS, 900 |
| Refresh | None. Sign a fresh assertion |
| On key rotation | Every session dies; old assertions get IDENTITY_GENERATION_STALE |
| On revocation | Every session dies; AGENT_IDENTITY_REVOKED |
There is no refresh token on purpose
A refresh token is a long-lived bearer credential, and the agent already holds something strictly better — the private key. Re-signing costs a signature.
Treat 401 as "mint a new token and retry once". A second 401 is a real
identity problem; stop.
Full wire format: Sign your own assertion.
#Comparison
| Session | API key | Agent token | |
|---|---|---|---|
| Principal | USER |
API_KEY |
AGENT |
| Obtained by | Password / MFA / SSO | Created once | Signed assertion |
| Lifetime | 12h, 1h idle | Up to 730 days | 15 minutes |
| Reaches | Control plane | Control plane | Gateway only |
| Can step up | yes | no | n/a |
| Refreshable | re-login | n/a | re-sign |
| Revocable | logout | revoke | revoke the identity |
#The published values
bashcurl -s https://gateway.veefore.example.com/.well-known/veefore-platform-key
The platform public key, its id, and the gateway audience. No authentication, cached for five minutes.
Publishing the audience is safe
It is the value you must sign for. Knowing it grants nothing without the agent's private key, and every agent that can reach the gateway has to know it to talk at all.
Not publishing it would mean clients guessing, and a wrong guess produces a bare
401 with no explanation — which was a real onboarding failure before this
endpoint carried it.