Loyalty SSO
GuestMaker is a standard OpenID Connect Identity Provider for loyalty guests. Your website integrates as a confidential Relying Party using the Authorization Code flow with PKCE. If your platform already supports “Log in with Google / Apple / any OIDC provider,” this is the same integration pointed at our endpoints.
Prefer code first? A copy-paste, framework-neutral walkthrough of the whole flow.
The flow
Authorization Code + PKCE (S256), confidential client. The guest authenticates once against our branded login; you exchange the code server-side and read member data with the access token.
Access tokens are short-lived (~5 minutes). Re-run the flow when one expires — because the guest’s session persists across all of the group’s properties, the re-login is usually silent (true SSO).
We issue you a client_id and client_secret (shown once) per environment. Guests authenticate against a hotel-branded login we host; you receive an authorization code, exchange it server-side for tokens, and read member data with the access token.
Everything is discoverable at the well-known URL — always load endpoints and signing keys from discovery rather than hard-coding them, because we rotate keys.
id_token; live balance & history come from the resource endpoints.| Scope | Returns | Where |
|---|---|---|
| openid | sub — the member's stable identifier. Required on every request. | id_token |
| profile | name, given_name, family_name, locale, country, updated_at | id_token |
| email (email_verified is only emitted when we have proof of ownership — never assume it for pre-existing members) | id_token | |
| loyalty:read | loyalty_tier, loyalty_tier_name, points_balance, member_number | id_token + GET /api/loyalty/me/balance |
| transactions:read | points earn / redemption history | GET /api/loyalty/me/transactions |
Cash-credit balance is not exposed in v1.
/api/oidc — but resolve them from discovery, not this list.GET /api/oidc/.well-known/openid-configurationDiscovery — every endpoint, scope, and the JWKS URI. Load from here; don't hard-code.GET /api/oidc/authAuthorization endpoint (browser redirect).POST /api/oidc/tokenToken endpoint (server-to-server, client_secret_basic).GET /api/oidc/jwksPublic signing keys (RS256). Cache, but refresh on an unknown kid.GET /api/loyalty/me/balanceMember tier + points balance + credit value (Bearer access token).GET /api/loyalty/me/transactionsMember points / redemption history (Bearer access token).There is no UserInfo endpoint by design: profile and email arrive as id_token claims, and live member data is served by /api/loyalty/me/*. The access token is an RS256 JWT whose aud is our resource indicator — those endpoints verify it and isolate by member + tenant.
- PKCE with S256 (never
plain); a freshcode_verifierper attempt. - Send and verify
state(CSRF) andnonce(replay); verify theissresponse parameter equals our issuer. - Verify the id_token signature against our JWKS with RS256 — check
iss,aud = your client_id,nonce, expiry. Never accept an unverified token; rejectalg: none/ HS256. - redirect_uri is exact-match — HTTPS only, byte-for-byte, no wildcards, no trailing-slash or case variants. Register every value you will use.
- Keep
client_secretserver-side; run the token exchange from your backend only. TLS everywhere. - Cache the JWKS but refresh on an unknown
kid— we rotate keys.
Both access_token and id_token are asymmetric RS256 JWTs you verify against the published JWKS. Access tokens last ~5 minutes.
Introspection is intentionally disabled. Don’t rely on it — verify the JWT signature and call /api/loyalty/me/*, which enforce a server-side revocation epoch (logout, ban, data-erasure and consent withdrawal invalidate outstanding access tokens immediately, even before they expire).
client_id/redirect_uri or when SSO is not enabled for the tenant.| error | Meaning |
|---|---|
| invalid_request | Missing/malformed parameter (e.g. no PKCE challenge, bad scope syntax). |
| invalid_client | Unknown client_id or wrong client_secret. Returned generically for both. |
| invalid_grant | Code expired/replayed, redirect_uri mismatch, or code_verifier failed PKCE. |
| invalid_scope | Requested a scope not granted to your client. |
| access_denied | The guest abandoned or declined the login. |