OAuth applications
Questi contenuti non sono ancora disponibili nella tua lingua.
Every GuestCraft instance is an OAuth 2.1 authorization server. A third-party application, or an MCP client such as an AI assistant, registers itself, sends a member of a workspace through the authorization code flow with PKCE, and receives tokens that act for that member in the one workspace they chose, with the permissions and properties they allowed. This guide is the contract of that flow for application developers and operators. API keys, organization tokens and client sessions, the credentials of integrations’ own servers, are described in API keys and webhooks.
Status: authorization code with PKCE, dynamic client registration, consent, refresh, revocation,
introspection and the workspace’s list of authorized applications work on every instance. OpenID
Connect (ID tokens, openid, UserInfo), the client credentials and device grants, Client ID
Metadata Documents, private_key_jwt and DPoP are not offered.
Discovery
Section titled “Discovery”Both documents are served at the instance’s public origin, to any origin (CORS *, without
credentials), and may be cached for five minutes. The examples use https://hotel.example.com.
| Document | Address |
|---|---|
| Authorization server metadata (RFC 8414) | /.well-known/oauth-authorization-server/api/auth |
| The API as a protected resource (RFC 9728) | /.well-known/oauth-protected-resource/api/v1 |
| The MCP server as a protected resource (RFC 9728) | /.well-known/oauth-protected-resource/api/mcp |
The issuer is https://hotel.example.com/api/auth, and authorization responses carry it as iss
(RFC 9207). The metadata names the real endpoints below, S256 as the only PKCE method, the
authorization_code and refresh_token grants, and none, client_secret_basic and
client_secret_post client authentication.
| Endpoint | Path |
|---|---|
| Authorization | GET /api/auth/oauth2/authorize |
| Token | POST /api/auth/oauth2/token |
| Registration (RFC 7591) | POST /api/auth/oauth2/register |
| Revocation (RFC 7009) | POST /api/auth/oauth2/revoke |
| Introspection (RFC 7662) | POST /api/auth/oauth2/introspect |
The token, registration, revocation and introspection endpoints answer CORS preflights and can be read from any origin, errors included, for browser-based applications; they never accept cookie credentials across origins.
Protected resources and tokens
Section titled “Protected resources and tokens”An instance has two protected resources, named from its configured GUESTCRAFT_BASE_URL, never
from a request’s Host or forwarded headers:
https://hotel.example.com/api/v1, the HTTP API;https://hotel.example.com/api/mcp, the MCP server built into the instance.
Every registered application may ask for both. Send the resource you will call as resource
(RFC 8707) on the authorization request and on the token request. A token is accepted only at the
resource it was issued for: an API token at paths below /api/v1, an MCP token at /api/mcp. A
token issued without a resource is accepted nowhere. A refused token answers 401 and is audited as
oauth_application.denied with the reason resource.
Access tokens (gc_oat_…) are opaque, valid for an hour, and looked up on every request, so a
revocation or a member’s lost access applies to the very next request on every instance. Send them
as Authorization: Bearer gc_oat_…; a request with one is never authenticated by a cookie and never
reaches a browser-only operation (team, keys, webhooks, Channex administration, authorized
applications). Requests are admitted like an API key’s (see the apiKey security scheme of the
OpenAPI document), in a share of the instance’s remembered keys of their
own: at most 100 access tokens of one workspace used within a minute, apart from its API keys, so
that its members’ applications never take the places of its integrations, and within the
organization’s 1,000 places.
Refresh tokens (gc_ort_…) are issued only with the offline_access scope and last 30 days. The
instance stores only the SHA-256 of every token.
Scopes
Section titled “Scopes”The scopes are offline_access and every permission an API key may hold, such as
reservations.read or folios.post (the list is scopes_supported in the metadata). Browser-only
permissions are never scopes. An authorization request without scope asks for every scope.
What a token may do on a request is the intersection of:
- the permissions the member allowed on the consent page (the grant),
- the token’s own scopes (a refresh may ask for fewer, never more),
- what the member holds in that workspace at that moment, and the properties they reach.
A member who no longer reaches the workspace, or who holds nothing of the grant any more, makes the
token answer 401 (audited as oauth_application.denied, reason creator_access); a permission the
grant lacks answers 403. A grant limited to some properties whose properties the member no longer
reaches reaches nothing, never every property. To add permissions, send the member through the
authorization again: a new consent replaces the grant’s permissions and properties, and tokens
issued before keep their own, narrower scopes.
The OpenAPI document names the permissions of each operation a token may call in
x-guestcraft-permissions; a call needs one of them at least, and the operation’s description
says what a particular call needs besides. The built-in MCP server checks them before a tool runs:
a token whose scopes and grant name none answers 403 with
WWW-Authenticate: Bearer error="insufficient_scope", scope="…", the scopes to ask for in the next
authorization (MCP). The REST API answers that case, like every
other permission it refuses, with 403 auth.forbidden.
The authorization code flow
Section titled “The authorization code flow”-
Register (once). For a native or desktop client with a loopback callback:
POST /api/auth/oauth2/registerContent-Type: application/json{"client_name": "Channel Assistant","redirect_uris": ["http://127.0.0.1:8123/callback"],"token_endpoint_auth_method": "none","grant_types": ["authorization_code", "refresh_token"],"response_types": ["code"]}The answer (201) holds the
client_id, and a one-timeclient_secretfor a confidential client (client_secret_basicorclient_secret_post). -
Authorize. Create a random
code_verifierand send the member’s browser to:https://hotel.example.com/api/auth/oauth2/authorize?response_type=code&client_id=…&redirect_uri=http%3A%2F%2F127.0.0.1%3A8123%2Fcallback&scope=reservations.read%20properties.read%20offline_access&state=…&code_challenge=…&code_challenge_method=S256&resource=https%3A%2F%2Fhotel.example.com%2Fapi%2Fv1PKCE with
S256is required for every client. The redirect URI must be one the client registered, exactly; only the port of a registeredhttploopback URI (127.0.0.1,[::1],localhost) may differ. A request without PKCE is answered at the redirect URI withinvalid_request; an unregistered redirect URI is never redirected to. -
Sign-in and consent. A member without a session signs in first, with a password or passkey or with their organization’s single sign-on (enterprise identity). Single sign-on leaves for the identity provider, so the instance verifies the authorization request’s signature before it starts and returns the browser to the request at
/api/auth/oauth2/authorizeafterwards, verifying it again; an address it returns to is built on the server from the verified request only. A request withprompt=logincontinues only with a session that signed in, with every factor the person’s organizations require, after the request was signed; with an older session, or none, the instance asks for a sign-in again. A request whose signature expired meanwhile sends the browser to the sign-in page witherror=OAUTH_REQUEST_EXPIRED, which says to start again from the application. Then the consent page shows the name the application registered (as unverified text: anyone may register any name), its client ID, and where the browser goes back to. The member chooses one workspace they reach, all their properties or some of them, and the permissions to allow among those the application asked for that they hold there, all ticked at first. Allow returns the browser to the redirect URI withcode,stateandiss; Deny returnserror=access_denied. Every authorization shows the consent page again: no earlier consent is reused silently. -
Exchange the code (within ten minutes, once):
POST /api/auth/oauth2/tokenContent-Type: application/x-www-form-urlencodedgrant_type=authorization_code&code=…&code_verifier=…&client_id=…&redirect_uri=http%3A%2F%2F127.0.0.1%3A8123%2Fcallback&resource=https%3A%2F%2Fhotel.example.com%2Fapi%2Fv1The answer holds
access_token,token_typeBearer,expires_in,scopeand, withoffline_access, arefresh_token. -
Call the API with
Authorization: Bearer gc_oat_…, in the workspace the member chose.
Refresh, revocation and introspection
Section titled “Refresh, revocation and introspection”- Refresh with
grant_type=refresh_token. Each refresh rotates the refresh token: the old one stops working. Presenting a rotated refresh token again ends the whole family of that application’s refresh tokens for that member, without issuing anything (RFC 9700, section 4.14). A refresh under a revoked authorization, or one whose member holds nothing of it any more, answersinvalid_grant. Token requests are limited to 20 a minute per client address (429 beyond), before the instance looks any refresh token up. - Revoke a token at the revocation endpoint with
tokenand the client’s authentication (justclient_idfor a public client). Revoking an access token ends it at once; revoking a refresh token also ends the access tokens it issued. - Introspect with a confidential client’s credentials; a client introspects only its own
tokens. A token whose authorization was revoked, or whose member holds nothing of it any more,
answers
{ "active": false }.
The workspace’s authorized applications
Section titled “The workspace’s authorized applications”Settings → Authorized applications lists the applications every member authorized in the workspace, with the registered name and home page as unverified text, who authorized it, what it may do, when it was authorized and last used, and how much of it the member still holds. Revoking one (after a confirmation) ends its access and refresh tokens in this workspace from their next request, on every instance, and deletes its consent; the application stays registered, and the member’s authorizations in other workspaces, and other members’ authorizations, stay. The member can authorize it again later.
The same operations exist in the API, browser-only, for members holding oauth_applications.manage
(administrators have it; never members limited to selected properties):
GET /v1/authorized_applications, GET /v1/authorized_applications/{id} and
POST /v1/authorized_applications/{id}/revoke with the application’s ETag in If-Match. Consents
are audited as oauth_application.authorized, revocations as oauth_application.revoked, and
actions an application takes with the actor type oauth_application, naming its authorization and
the member it acts for.
Registration policy and limits
Section titled “Registration policy and limits”Open registration (RFC 7591 without an initial access token) is on by default, because MCP clients register themselves. Registration metadata is self-asserted and treated as untrusted text: GuestCraft never fetches a logo or any other URL it names.
- Better Auth limits registrations to 5 a minute per client address, and an instance accepts 30 a minute from all addresses together.
- An instance keeps at most 10,000 applications that never completed an authorization; past that,
registration answers 429 (
temporarily_unavailable) until older ones expire. - An application that completes no authorization within a day is deleted, and one unused for 90 days is deleted with its authorizations. The worker runs a bounded pass of these rules every hour (on Vercel, at each daily drain), and every registration runs a small batch first. An application with an authorization in an archived workspace is kept while the workspace is archived.
- A registration has at most 30 fields, 16 KiB and 10 redirect URIs; names, URIs and lists have
bounded lengths. It may not set
jwks,jwks_uri,software_statement, logout URIs,dpop_bound_access_tokens,subject_type,skip_consent,require_pkceor server-assigned values;token_endpoint_auth_methodisnone,client_secret_basicorclient_secret_post. The client credentials grant is refused. A client that omitsapplication_typeis a native client when it registers only loopback or private-use redirect URIs, otherwise a web client, whose redirect URIs must use HTTPS.
GUESTCRAFT_OAUTH_OPEN_REGISTRATION=false turns open registration off: a registration without a
session then answers 401, and only an owner or administrator of the organization active in their
session registers an application, signed in to the instance; a member who neither owns nor
administers it is refused 401 too. No screen sends that registration yet: send it from the browser
where you are signed in, for example from the developer console of a page of the instance, so that
it carries your session cookie and the instance’s Origin, which the cookie origin policy requires:
const answer = await fetch("/api/auth/oauth2/register", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_name: "Channel Assistant", redirect_uris: ["http://127.0.0.1:8123/callback"], token_endpoint_auth_method: "none", grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], }),});console.log(answer.status, await answer.json());The same metadata rules apply. A 201 answer is the application’s registration (RFC 7591): give
its client_id, and its client_secret for a confidential client, to the application, which then
runs the authorization code flow above like any other. See the
installation guide.