Authentication
How you prove who you are to Alaf Server — session cookies, personal access tokens, MCP OAuth, and desktop loopback mode.
Every request to Alaf Server has to answer one question first: who are you? Alaf Server accepts four different ways to answer it, and which one applies depends on where the request comes from — a browser tab, a script, an AI agent, or the desktop app talking to itself. This page walks through each one.
Base path
Every API route lives under /api on your instance — e.g. https://your-host/api/projects. The
same authentication rules apply to all of them; there is no unauthenticated back door. Access checks
after login are covered separately in Permission model.
The four ways in
| Method | What you send | When it applies |
|---|---|---|
| Session cookie | An httpOnly cookie the browser holds for you | The dashboard, in a normal browser |
| Personal access token (PAT) | Authorization: Bearer opsh_pat_… | The CLI, scripts, server-to-server, and MCP clients without OAuth |
| MCP OAuth | An OAuth 2.1 access token the client fetches for you | AI agents (Claude, Cursor, …) connecting to /api/mcp |
| Zero-auth loopback | Nothing — the request just comes from 127.0.0.1 | The desktop app, or a local single-user instance you opt into |
A single request uses exactly one of these. Alaf Server checks for a Bearer token first; if there isn't
one, it looks for a session cookie; if there's neither, it falls back to the loopback path (and only if
that mode is turned on).
Session cookies (the dashboard)
When you log in to the dashboard, Alaf Server gives your browser a signed, httpOnly session cookie. "httpOnly" means JavaScript running in the page cannot read it — so even if a bad script somehow ran on the page, it couldn't steal your session. The browser sends the cookie automatically on every request; you never handle it yourself.
The cookie name is prefixed with your instance mode — alaf server.session_token on a self-hosted install,
alaf server-cloud.session_token on Alaf Server Cloud.
Bearer tokens are refused from the dashboard's own origin
If a request arrives with a Bearer header and an Origin matching one of the dashboard's trusted
browser origins, Alaf Server rejects it (401 BEARER_NOT_ALLOWED_FROM_BROWSER). Bearer tokens are for
CLIs and servers; a token showing up from a browser tab is the signature of a stolen credential being
replayed, so the door is closed. The CLI sends no Origin header at all, which is why its tokens are
accepted.
Personal access tokens (Bearer)
A personal access token (PAT) is a long string you send in the Authorization header to prove who
you are without a browser:
Authorization: Bearer opsh_pat_…Every token looks like opsh_pat_ followed by a 43-character random secret (256 bits of entropy).
Alaf Server stores only a SHA-256 hash of it — the plaintext is shown to you exactly once, at creation,
and can never be retrieved again. A token always acts as the person who created it and carries that
person's role, unless you narrow it (see below).
Create one
In the dashboard, open Settings → Tokens and use the Personal Access Tokens section. Copy the token when it appears — that's the only time you'll see it.
Screenshot
From the terminal:
# A full-access token that acts as you
alaf server token create "my laptop"
# A read-only token that expires in 90 days
alaf server token create "ci-readonly" --read-only --expires 90
# A token scoped to one project (read + write only on that project)
alaf server token create "deploy-bot" --grant project:proj_123:read,write
alaf server token list # your tokens (never shows secrets)
alaf server token revoke <id>Use a token from the CLI
alaf server login stores a token so the CLI can send it on every call. Run it with --token opsh_pat_…
for a non-interactive login (handy in CI), or run it bare to open the token settings page and paste one in.
The stored token is only ever sent as a Bearer header — never as a cookie.
The request body behind the create call:
Prop
Type
Two ways to narrow a token
- Read-only — the token can call read endpoints but any mutating request is refused with
403 TOKEN_READ_ONLY. Great for dashboards, monitoring, and CI that only needs to look. - Scoped — instead of acting with your full role, the token is confined to a specific list of
resources you grant it (a project, a server, a repository, …), each with
read,write, oradmin. A scoped token acts as a restricted principal: it can do less than you, never more. You can only grant access you already hold yourself — the server checks this before minting.
A token is tied to one organization
Each token is bound to the organization it was created in. If a request carries an X-Organization-Id
header naming a different org, it's rejected with 403 TOKEN_ORG_SCOPE. An invalid or revoked token
returns 401 INVALID_TOKEN.
MCP OAuth (AI agents)
Alaf Server is a standards-compliant OAuth 2.1 server for the Model Context Protocol (MCP) — the
way AI agents call tools. An MCP-capable client (Claude, Cursor, …) points at POST /api/mcp, and the
handshake is automatic: the first unauthenticated request returns a 401 pointing at
/.well-known/oauth-protected-resource, the client registers itself, runs the PKCE authorize flow
(Proof Key for Code Exchange — a way to complete OAuth safely without a stored client secret), and you
approve it on a consent screen in your browser.
That consent screen is where you set the agent's limits — read-only and/or a specific set of projects, servers, and repositories. Your choices are saved as the client's binding and enforced through the exact same grant model as a scoped PAT. An OAuth token that somehow skips consent has no binding, and is therefore denied everything — there is no "authenticated but unscoped" state.
You manage connected agents under Settings → MCP, where you can see each client and disconnect it (which revokes its tokens immediately). Clients without OAuth support can instead authenticate with a plain PAT as a bearer credential.
Full MCP setup
Endpoint URLs, client config snippets, and verification steps live on the MCP page.
Desktop / zero-auth loopback mode
The desktop app runs an API and a dashboard on your own machine, just for you. Forcing a login there
would be pointless friction, so it uses zero-auth mode: the API auto-provisions a single local admin
user (Local User, owner of a personal workspace) and treats local traffic as that user. No password,
no cookie to manage.
This is safe only because it is gated twice, and both gates must pass:
Both conditions are required
- Opt-in. The instance's auth mode must be
none. This is the default for the desktop app; on any other deployment it also requires the operator to setALAFSERVER_ALLOW_ZERO_AUTH=true. - Loopback only. The request must come from a loopback address (
127.0.0.1/::1), read from the kernel-reported TCP peer — not theHostheader, which a client or a misconfigured reverse proxy could forge. A request from anywhere else is rejected with401, even in zero-auth mode.
On a normal self-hosted install this path is never even reached: the auth mode defaults to local, so a
missing session is a plain 401. Turning it on is a deliberate operator choice — auth mode none and
ALAFSERVER_ALLOW_ZERO_AUTH=true — and even then the loopback check keeps it usable only from the machine
itself. A request arriving through a network reverse proxy carries the proxy's address as its TCP peer,
not loopback, so it is refused.
Alaf Server recognizes three auth modes overall:
none— zero-auth loopback, described above. Default for desktop.local— standard login required (email + password via Better Auth). Default for a fresh self-hosted install.cloud— the desktop app signs in with an Alaf Server Cloud account instead of a local one.
One failure that never falls through
If the session machinery itself errors (a database outage, a decryption failure), the request returns
503 AUTH_UNAVAILABLE. It does not quietly drop to the zero-auth path — a broken session check can
never be mistaken for "no login required."
Cloud sign-in
Choosing Continue with Cloud (or connecting a self-hosted instance to Alaf Server Cloud) runs a PKCE handshake that mints a cloud session for the connecting owner. That session is stored encrypted, server-side only — the browser never sees it — and org-scoped cloud calls act on the owner's behalf through it. How that boundary is kept clean is covered in The local ↔ cloud boundary.
Where credentials live
Alaf Server keeps secrets where they're safest instead of copying them around:
- PAT secrets are stored only as a SHA-256 hash; the plaintext exists just once, in your clipboard.
- Cloud sessions are encrypted at rest on the instance and never exposed to the browser.
- The GitHub App private key lives only on Alaf Server Cloud; self-hosted instances mint short-lived App tokens through the cloud rather than holding the key.
- Per-project git tokens and environment secrets are encrypted at rest.
Revocation
- A token:
alaf server token revoke <id>, or the Revoke button in Settings → Tokens. It stops working on its next request. - An MCP client: disconnect it under Settings → MCP — its issued tokens are revoked and the consent is dropped, so reconnecting re-prompts.
- A person: removing a member ends their access; their session stops working on its next check.
- A cloud link: disconnecting an instance revokes its cloud session.
Errors you might see
| Status & code | Meaning |
|---|---|
401 INVALID_TOKEN | The bearer token is wrong, expired, or revoked. |
401 BEARER_NOT_ALLOWED_FROM_BROWSER | A bearer token arrived from a dashboard browser origin — send it from a CLI/server instead. |
403 TOKEN_READ_ONLY | A read-only token tried a mutating request. |
403 TOKEN_ORG_SCOPE | The token is bound to a different organization than the one requested. |
401 Unauthorized | No valid session, or a zero-auth request that failed the mode/loopback gates. |
503 AUTH_UNAVAILABLE | The session check itself failed — retry; it never silently downgrades to no-auth. |
What next?
Cloud boundary
How a self-hosted instance talks to Alaf Server Cloud without leaking local identity — proxied requests carry the owner's cloud session and nothing else.
Troubleshooting
Where Alaf Server shows you what went wrong — the build screen, the Logs tab, and the CLI — and how to read an error before you try to fix it.