Security

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

MethodWhat you sendWhen it applies
Session cookieAn httpOnly cookie the browser holds for youThe 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 OAuthAn OAuth 2.1 access token the client fetches for youAI agents (Claude, Cursor, …) connecting to /api/mcp
Zero-auth loopbackNothing — the request just comes from 127.0.0.1The 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

Settings → Tokens tab, the "Personal Access Tokens" section with the create form and a freshly minted token shown once. (screenshot pending)

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, or admin. 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

  1. 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 set ALAFSERVER_ALLOW_ZERO_AUTH=true.
  2. Loopback only. The request must come from a loopback address (127.0.0.1 / ::1), read from the kernel-reported TCP peer — not the Host header, which a client or a misconfigured reverse proxy could forge. A request from anywhere else is rejected with 401, 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 & codeMeaning
401 INVALID_TOKENThe bearer token is wrong, expired, or revoked.
401 BEARER_NOT_ALLOWED_FROM_BROWSERA bearer token arrived from a dashboard browser origin — send it from a CLI/server instead.
403 TOKEN_READ_ONLYA read-only token tried a mutating request.
403 TOKEN_ORG_SCOPEThe token is bound to a different organization than the one requested.
401 UnauthorizedNo valid session, or a zero-auth request that failed the mode/loopback gates.
503 AUTH_UNAVAILABLEThe session check itself failed — retry; it never silently downgrades to no-auth.

What next?

On this page