Authentication

Personal tokens, OAuth 2.1 with dynamic registration and PKCE, scopes, lifetimes and revocation.

Every request to the REST API and the MCP server carries a bearer token in the Authorization header:

Authorization: Bearer alba_pat_7Wm0…

Nothing else is accepted: no query-string tokens, no cookies, no basic auth. A browser session does not work either, so a page that is logged in to the workspace cannot call the API on its user's behalf by accident.

Tokens work while an administrator has the REST API or the MCP server switched on for the workspace; both are off in a new one (see Overview). A token is the same for both, and is refused with 403 feature_disabled at the one that is off.

A token comes from one of two places. A personal token is made by a member and pasted into your program. An OAuth token is obtained by your program itself, with the member's consent. Both stand for one member and behave the same once you have them.

Personal tokens

A member makes one on their AI agents page (Manage AI agents in account settings) by giving it:

  • a name, shown in the ticket history beside theirs for everything the token does,
  • access: Read, or Read and change,
  • a lifetime: 30, 90 or 365 days, or no expiry.

The token begins alba_pat_ and is shown once, when it is made. The workspace keeps only a hash of it and cannot show it again. If it is lost, make another and revoke the old one.

Use a personal token for scripts, scheduled jobs, CI pipelines and agents that cannot run a browser sign-in. Keep it in a secret store, not in a repository.

OAuth 2.1

OAuth is for applications that connect on behalf of whoever uses them: an AI assistant, an editor, a tool you publish. The member signs in to the workspace, sees what your application asks for, and allows or denies it. Your application never sees their password.

Alba Ticket implements the parts of OAuth 2.1 that MCP clients use:

  • authorisation server metadata (RFC 8414) and protected resource metadata (RFC 9728) for discovery,
  • dynamic client registration (RFC 7591), so there is nothing to set up in advance,
  • the authorisation code flow with PKCE (RFC 7636), S256 only,
  • refresh tokens that rotate on every use,
  • resource indicators (RFC 8707),
  • token revocation (RFC 7009).

Each workspace is its own authorisation server: its address is the issuer. An MCP client does all of this by itself given the MCP address. The steps below are for writing a client of your own.

1. Discover

A request without a token is answered 401 with a header that points at the resource's metadata:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="Alba", resource_metadata="https://your-workspace.albaticket.com/.well-known/oauth-protected-resource"

The protected resource metadata names the authorisation server. The MCP server's own is at /.well-known/oauth-protected-resource/mcp.

GET /.well-known/oauth-protected-resource/mcp
{
  "resource": "https://your-workspace.albaticket.com/mcp",
  "authorization_servers": ["https://your-workspace.albaticket.com"],
  "scopes_supported": ["tickets:read", "tickets:write"],
  "bearer_methods_supported": ["header"],
  "resource_name": "Alba Ticket",
  "resource_documentation": "https://your-workspace.albaticket.com/api/docs"
}

The authorisation server's metadata gives every endpoint:

GET /.well-known/oauth-authorization-server
{
  "issuer": "https://your-workspace.albaticket.com",
  "authorization_endpoint": "https://your-workspace.albaticket.com/oauth/authorize",
  "token_endpoint": "https://your-workspace.albaticket.com/oauth/token",
  "registration_endpoint": "https://your-workspace.albaticket.com/oauth/register",
  "revocation_endpoint": "https://your-workspace.albaticket.com/oauth/revoke",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_post", "client_secret_basic"],
  "scopes_supported": ["tickets:read", "tickets:write"]
}

2. Register

Register once per workspace. No account is needed:

POST /oauth/register
Content-Type: application/json

{
  "client_name": "Release Notes Bot",
  "redirect_uris": ["http://127.0.0.1:8765/callback"]
}
{
  "client_id": "alba_client_Qm9…",
  "client_id_issued_at": 1790000000,
  "client_name": "Release Notes Bot",
  "redirect_uris": ["http://127.0.0.1:8765/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "scope": "tickets:read tickets:write"
}
Field
client_name Shown to the member on the consent page, as your application's own claim. Up to 100 characters.
redirect_uris One to ten addresses the member may be sent back to: https anywhere, http only on localhost, 127.0.0.1 or [::1], or your application's own scheme (myapp://callback). A loopback address may be used with any port.
token_endpoint_auth_method none (the default) for a public client, such as anything running on the user's machine. client_secret_post or client_secret_basic for a confidential client that can keep a secret; the answer then includes client_secret, shown once.
client_uri, logo_uri, software_id, software_version Kept, optional.

Registration is limited to 30 an hour from one address. A client that has no connection and no pending code thirty days after it registered may be removed; if a later request answers invalid_client, register again.

Make a PKCE verifier (43 to 128 characters from A-Z a-z 0-9 - . _ ~) and its S256 challenge, then open the authorisation endpoint in the member's browser:

https://your-workspace.albaticket.com/oauth/authorize
  ?response_type=code
  &client_id=alba_client_Qm9…
  &redirect_uri=http://127.0.0.1:8765/callback
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
  &scope=tickets:read%20tickets:write
  &state=af0ifjsldkj
  &resource=https://your-workspace.albaticket.com/mcp
Parameter
response_type code
client_id From registration
redirect_uri One you registered. It may be left out if you registered exactly one.
code_challenge, code_challenge_method Required. The method must be S256.
scope tickets:read, or tickets:read tickets:write. Left out, write access is asked for.
state Returned unchanged. Use it to tie the answer to the request.
resource Optional. If given, it must be on the workspace's own host.

The member logs in if they need to and sees your application's name, the host it will return them to, and a choice of Read or Read and change (only Read if that is all you asked for). They may give you less than you asked for, so read scope in the token response.

A request that names an unknown client or an address you did not register is shown to the member as an error and never redirected. Other problems come back to your redirect_uri as error and error_description: invalid_request (no PKCE, or not S256), invalid_scope, invalid_target (a resource elsewhere), unsupported_response_type, or access_denied when the member says no.

When they allow it, the browser goes to your address with a code:

http://127.0.0.1:8765/callback?code=H8Kz…&state=af0ifjsldkj

The code works once, within one minute.

4. Exchange the code

POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=H8Kz…
&redirect_uri=http://127.0.0.1:8765/callback
&client_id=alba_client_Qm9…
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
{
  "access_token": "alba_oat_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "alba_ort_…",
  "scope": "tickets:read tickets:write"
}

A confidential client authenticates here with HTTP Basic (client_secret_basic) or with client_id and client_secret in the body (client_secret_post). JSON bodies are accepted as well as form-encoded ones.

The redirect_uri must be the one the code was issued for. A code that is expired, already used, issued to another client or presented with the wrong verifier gives invalid_grant.

5. Refresh

The access token lasts an hour. Refresh it before or after it expires:

POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=alba_ort_…&client_id=alba_client_Qm9…

The answer has the same shape, with a new refresh token: the old access and refresh tokens stop working at once, so store the new pair before using it. A refresh token lasts thirty days from its last use, so a connection used at least monthly never needs the member again. After that, send them through the consent page again.

6. Revoke

When your application disconnects, revoke the grant with either of its tokens:

POST /oauth/revoke
Content-Type: application/x-www-form-urlencoded

token=alba_ort_…&client_id=alba_client_Qm9…

The answer is 200 with an empty body whether or not the token existed.

Token endpoint errors

Errors from /oauth/token, /oauth/register and /oauth/revoke follow OAuth's shape:

{"error": "invalid_grant"}
Status error
400 invalid_grant The code or refresh token is unknown, expired, used, another client's, or the verifier is wrong; or the member can no longer use agents
400 unsupported_grant_type Only authorization_code and refresh_token
400 invalid_redirect_uri, invalid_client_metadata Registration refused, with error_description
401 invalid_client Unknown client, or a wrong secret
403 access_denied Registration while the REST API and the MCP server are both switched off for the workspace (an exchange or refresh then gives invalid_grant)
429 slow_down Too many requests from one address (60 token requests a minute, 30 registrations an hour)

Scopes and access

Scope Access What the token may do
tickets:read read See what the member can see. Every change is refused with 403.
tickets:read tickets:write write Everything the member may do through the API.

A personal token has the same two levels, Read and Read and change. The level only ever takes away: a write token held by a member who may only comment in a project can only comment there. GET /api/v1/me tells you which level you have.

The scopes cover everything the API does, boards and sprints included.

Token formats

Prefix What
alba_pat_ Personal token
alba_oat_ OAuth access token
alba_ort_ OAuth refresh token
alba_client_ OAuth client id (public, not a secret)
alba_cs_ OAuth client secret

Treat everything after the prefix as opaque. Secret scanners can use the prefixes to find leaked tokens.

When a token stops working

A request answered 401 invalid_token means the token is unknown, expired or revoked, or its member can no longer use agents: they were deactivated, lost their email address, or stopped being a member. A 403 with the code feature_disabled means an administrator has that way in switched off for the workspace: the REST API for a request to /api/v1, the MCP server for one to /mcp. The two are switched separately, so a token refused at one may work at the other. Nothing is deleted while either is off, and the same token works again once it is switched back on.

Members see every personal token and every connected application on their AI agents page, with when it was last used, and can revoke any of them at once. Changing a password does not revoke tokens.