Chapter 2

Quickstart

The shortest path from nothing to a signed-in user your own server can trust.

The shape of it

Two halves that never mix. Your users sign in with Google or Apple in their browser and come away with an access token. Your server holds an API key, and uses it to ask whether a token it was handed is still good.

The user's browserCloud AuthYour server1. sign in with Google / Apple2. accessToken3. your own API, with the token4. VerifyToken, with the API key
Signing in happens in the browser; your server never sees a password and never holds one. Its only job is step 4 — asking, with its API key, whether the token it was handed is still good.
Registering with an address and a password is switched off. New accounts are created by signing in with Google or Apple. Password sign-in still exists, but only for the operator accounts that run the service — see chapter 3. What is switched on right now is reported by GET /capabilities.

1. Issue an API key

Select a tenant, then issue a key from the API keys screen with a name that says what it is for. The secret is shown once, at the moment it is created, and never again. A key is pinned to the tenant it was issued for, so a service handling several tenants needs one per tenant.

A key belongs on a server and nowhere else. Embedded in a page or an app, it is readable by anyone who has the app. The key header is deliberately not allowed through CORS, so a browser cannot send one even by accident.
New keys can only read unless read-write was chosen when issuing, they expire in 30 days by default, and every tenant can hold at least two at once so that replacing one needs no downtime.

2. Sign the user in

Every endpoint is a POST that takes JSON and returns JSON. Ask for the URL to send the browser to, then exchange what comes back.

curl -X POST https://api.albos.org/auth.v1.OAuthService/GetAuthorizationURL \
  -H "Content-Type: application/json" \
  -d '{"provider":1,"redirectUri":"https://your.app/callback"}'

# {"authorizationUrl":"https://accounts.google.com/o/oauth2/v2/auth?...","state":"9f2c..."}
# provider: 1 = Google, 3 = Apple

Send the browser there. Google or Apple sends it back to your redirect URI with a code and a state; check the state matches, then exchange the code.

curl -X POST https://api.albos.org/auth.v1.OAuthService/HandleCallback \
  -H "Content-Type: application/json" \
  -d '{"provider":1,"code":"...","state":"9f2c...","redirectUri":"https://your.app/callback"}'

{
  "user": { "id": "...", "email": "[email protected]", "role": "" },
  "tokens": { "accessToken": "eyJhbGci...", "refreshToken": "...", "expiresIn": "900" },
  "isNewUser": false
}
isNewUser is true the first time, which is your chance to show a welcome screen rather than dropping somebody straight into an empty account.

3. Check the token on your server

When your server is handed an access token, check it with your API key. This goes further than the signature and the expiry: it also answers whether the session has ended and whether the account is still usable.

curl -X POST https://api.albos.org/auth.v1.AuthService/VerifyToken \
  -H "Content-Type: application/json" \
  -H "X-API-Key: ca_..." \
  -d '{"accessToken":"eyJhbGci..."}'

{
  "active": true,
  "user": { "id": "...", "email": "[email protected]", "role": "" },
  "sessionId": "...",
  "role": "",
  "expiresAt": "1789000000"
}
active: false means do not let this through. The reason — expired, signed out, account disabled — is not distinguished, because telling an unauthenticated caller which one it was helps nobody but an attacker.
role is the operator role and is an empty string for everybody who is not staff. Which tenant somebody belongs to, and as what, comes from myRole in ListMyTenants. Keying your own permissions off role by mistake gives every one of your users the same answer.

4. Keep the token fresh

An access token lasts fifteen minutes. Renew it with the refresh token. From a browser the cookie is sent automatically, so the body can be empty; a mobile app sends the value it kept.

curl -X POST https://api.albos.org/auth.v1.AuthService/RefreshToken \
  -H "Content-Type: application/json" \
  -d '{"refreshToken":"..."}'
A refresh token is single use. Presenting the same one twice revokes every token in that session, on the assumption that one of the two holders stole it — so always replace your stored value with the one that comes back.

5. Handle the code request

Calls that reach personal data, and calls that cannot be undone, come back with 412 until the account has entered an authenticator code. The response carries an X-Step-Up-Reason header saying which of three situations it is; ask for a code, send it to StepUp, then retry the original call.

# the refusal
HTTP/1.1 412 Precondition Failed
X-Step-Up-Reason: step_up_required

# then
curl -X POST https://api.albos.org/auth.v1.UserService/StepUp \
  -H "Content-Type: application/json" \
  -d '{"code":"123456"}'
An API key cannot do this — there is nobody to ask for a code — so a key that reaches one of these gets 403 instead. Keep your server-to-server work to verifying tokens and reading which tenants an account belongs to.

Calling from a browser

To authenticate by cookie, your origin has to be on the allowlist: the cookie is SameSite=Lax and the Origin header is checked as well. Ask whoever operates the service to add it. You also need credentials: "include" for the cookie to be sent at all.

const res = await fetch("https://api.albos.org/auth.v1.AuthService/GetCurrentUser", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  credentials: "include",
  body: "{}",
});