Chapter 3Every endpoint is a POST. This chapter covers the ones you need to sign somebody in, keep them signed in, and find out who they are from your own server.
How to call it
The API speaks Connect RPC, which over HTTP is a plain POST with a JSON body. There is no REST path building and no query string: the path is the method, and everything else is in the body.
POST https://api.albos.org/auth.v1.AuthService/GetCurrentUser
Content-Type: application/json
Connect-Protocol-Version: 1
{}
There are three ways to say who is calling.
| How | Who uses it | Notes |
|---|
| Session cookie | A browser | Set by the sign-in call. Send credentials: "include" and call from an allowed origin. |
Authorization: Bearer | A native app | The access token from the sign-in response. |
X-API-Key: ca_... | Your own server | An API key — a different header from the one above, and deliberately not allowed through CORS, so it cannot be used from a browser. See chapter 4. |
The generated clients in chapter 7 do all of this for you. The raw shape is here so that a language without a generated client is still usable.
The endpoints
The ones an integration actually uses. "Code" means the account has to have entered an authenticator code — see chapter 8 — which an API key can never do.
| Path | What it does | Sign-in | Code |
|---|
OAuthService/GetAuthorizationURL | Start a Google or Apple sign-in | — | — |
OAuthService/HandleCallback | Finish it, and get a session | — | — |
AuthService/VerifyToken | Ask whether an access token is still good | API key | — |
AuthService/GetCurrentUser | Who is this | yes | — |
AuthService/RefreshToken | Renew the access token | refresh token | — |
AuthService/Logout | End the session | yes | — |
TenantService/ListMyTenants | Which tenants this account belongs to | yes | — |
TenantService/SelectTenant | Choose the one later calls act on | yes | — |
TenantService/ListTenantMembers | Who else is in it | yes | yes |
UserService/StepUp | Submit a code to get past the line | yes | — |
AuthService/Login | Address and password. Operators only. | — | — |
AuthService/VerifyTwoFactor | The second step of that sign-in | — | — |
Signing somebody in
People who use the service sign in with Google or Apple. Two calls: one to get the URL to send them to, one to turn what comes back into a session.
POST/auth.v1.OAuthService/GetAuthorizationURL
Returns the URL to send the browser to, and a state value to check when it comes back.
| Field | Type | Required | Notes |
|---|
provider | enum | required | 1 for Google, 3 for Apple |
redirectUri | string | required | Must match what is registered for your integration |
useExistingSession | bool | optional | Let the provider use the session the browser already has, instead of always showing an account chooser. For joining from a QR code, where the point is to get through. |
{ "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth?...",
"state": "9f2c..." }
POST/auth.v1.OAuthService/HandleCallback
Exchanges the code the provider sent back for a session. This is the call that signs them in.
| Field | Type | Required | Notes |
|---|
provider | enum | required | The same one you started with |
code | string | required | From the provider's redirect |
state | string | required | Must match what GetAuthorizationURL returned |
redirectUri | string | required | The same one you started with |
| Returns | Type | Notes |
|---|
user | User | See below |
tokens | AuthTokens | accessToken, refreshToken, expiresIn in seconds |
isNewUser | bool | True the first time. Useful for sending somebody to a welcome screen rather than straight in. |
From a browser, the tokens also arrive as cookies and you can ignore the tokens field entirely. A native app keeps them itself.
Checking a token from your server
POST/auth.v1.AuthService/VerifyToken
Your backend receives an access token from your own front end and asks whether it is still good and whose it is. This is what an API key is for.
| Field | Type | Required | Notes |
|---|
accessToken | string | required | The token to check |
| Returns | Type | Notes |
|---|
active | bool | False when the token is malformed, expired, or its session has ended. Check this before anything else. |
user | User | Only when active is true |
sessionId | string | Which session it belongs to |
role | string | The operator role, empty for ordinary users. Not the role inside a tenant. |
expiresAt | int64 | Unix seconds |
Do not decide anything from the token's own contents without this call. Signing out, disabling an account and lowering a role all take effect immediately, and a token that is still within its fifteen minutes will not know.
Keeping the session going
POST/auth.v1.AuthService/GetCurrentUser
Who the caller is. Empty request body.
| Returns | Type | Notes |
|---|
user | User | See below |
POST/auth.v1.AuthService/RefreshToken
Exchanges a refresh token for a new pair. A refresh token is single use: if the same one is presented twice, every token in that session is revoked, on the assumption that one of the two presenters stole it.
| Field | Type | Required | Notes |
|---|
refreshToken | string | optional | Leave it out from a browser: the cookie is used. A native app sends the one it kept. |
POST/auth.v1.AuthService/Logout
Ends the session and clears the cookies. Empty request body, empty response.
Set-Cookie: __Host-ca_access_token=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0
Set-Cookie: __Host-ca_refresh_token=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0
Tenants
POST/auth.v1.TenantService/ListMyTenants
Which tenants the account belongs to, and which one is currently selected. Empty request body.
| Returns | Type | Notes |
|---|
tenants[] | Tenant | id, name, myRole, memberCount, maxMembers, plan, memberLeaveMode, acceptsJoinRequests, suspendedAt, suspendedReason |
selectedTenantId | string | Empty when none is selected |
remainingTenantAllowance | int32 | How many more this account may create |
suspendedAt being set means the tenant has no owner who can act. Its members can still read it and leave it; changes are refused with 412 until somebody takes it over.
POST/auth.v1.TenantService/SelectTenant
Tenant-scoped calls act on the selected tenant rather than taking an id, so that a caller cannot reach a tenant it did not choose by passing a different id. Call this first; the choice is remembered for the session.
| Field | Type | Required | Notes |
|---|
tenantId | string | required | Must be one the account belongs to |
Getting past the code
POST/auth.v1.UserService/StepUp
Submits an authenticator code so that the calls behind the line will go through. Call it when a call comes back 412; the header on that response says which of three situations it is.
| Field | Type | Required | Notes |
|---|
code | string | required | Six digits from the authenticator app, or one backup code |
| Returns | Type | Notes |
|---|
steppedUpAt | string | When it was accepted. Good for the rest of the session, and for 15 minutes for the operations that cannot be undone. |
The three reasons, and what each asks of the person, are in Chapter 6.
Signing in with a password
This is the operators' way in, not your users'. Accounts created with Google or Apple have no password and cannot grow one, so calling Login for them will not work. Registering with an address and password is currently switched off entirely.
POST/auth.v1.AuthService/Login
Takes an address and a password. It does not return a session: an operator must have an authenticator app, so the answer is always a challenge.
| Returns | Type | Notes |
|---|
requiresTwoFactor | bool | True. Pass the token below to the next call. |
twoFactorToken | string | Valid for 5 minutes. Not a session — it cannot be used for anything else. |
POST/auth.v1.AuthService/VerifyTwoFactor
The second step. This is what returns a session.
| Field | Type | Required | Notes |
|---|
twoFactorToken | string | required | From the Login response |
code | string | required | Six digits, or one backup code |
| Returns | Type | Notes |
|---|
user | User | — |
tokens | AuthTokens | — |
usedBackupCode | bool | True when a backup code was spent, so you can warn about it |
remainingBackupCodes | int32 | How many are left |
The User object
| Field | Type | Notes |
|---|
id | string | Stable. Key your own records off this, not off the address. |
email | string | Can change, and can be an Apple relay address |
displayName | string | — |
avatarUrl | string | Empty when none is set |
phoneNumber | string | Empty when none is set |
emailVerified | bool | False until the address is confirmed |
isEnabled | bool | False for a frozen account |
role | string | Operator role, empty for ordinary users. Not the tenant role. |
hasPassword | bool | False for Google and Apple accounts |
expiresAt | string | Empty unless the account is time limited |
lastActiveAt | string | — |
createdAt | string | — |
Values you will see
Two different things are both called a role. The one inside a tenant is tenants[].myRole; the operator one is user.role and is empty for everybody who is not staff.
tenants[].myRole | Means |
|---|
OWNER | Runs the tenant: invites, removes, changes settings, pays |
MEMBER | Belongs to it |
tenants[].plan | Members | API keys | Audit log kept |
|---|
FREE | 10 | 2 | 7 days |
LIGHT | 50 | 3 | 30 days |
PRO | 500 | 20 | 30 days |
tenants[].memberLeaveMode | Means |
|---|
FREE | A member can leave on their own. The default. |
APPROVAL | A member asks, and an owner decides. |
The operator roles and what each can do are in Chapter 5.