Sign in with WAMP
After this page you can take a user from a button in your app to a validated ID token and a usable access token, and you will know which parameters are mandatory, which are rejected, and what each failure means.
WAMP is an OpenID Connect provider with exactly one flow: authorization code
with PKCE. There are no client secrets, no client_credentials, no device
flow, no dynamic client registration, and no token introspection. Every token is
signed EdDSA (Ed25519).
Two values come from the operator of the deployment you integrate with:
| Placeholder | Meaning |
|---|---|
$ISSUER |
The OIDC issuer, e.g. https://account.example.com/oauth2. It always ends in /oauth2 |
$API |
The Account API origin, e.g. https://api.example.com |
What you need before you start
Section titled “What you need before you start”- An app in a publisher organization you administer — see Apps and credentials.
- An OIDC client under that app. Creating one is self-service.
- For each organization whose members will sign in: an active installation of your app in that organization, and an approval of this specific OIDC client against that installation.
Without that approval, consent cannot complete: the consent screen lists only organizations that have an active installation plus a live grant for your client, so the user sees an empty organization list and gets no code.
The flow
Section titled “The flow”-
Read the discovery document. It is the only discovery document WAMP serves, and it lives under the issuer. Take every endpoint URL from it rather than hardcoding paths.
Terminal window curl -s "$ISSUER/.well-known/openid-configuration" | jq .The current release serves these paths, all relative to
$ISSUER:Discovery member Path authorization_endpoint/authtoken_endpoint/tokenuserinfo_endpoint/mejwks_uri/jwksrevocation_endpoint/token/revocationend_session_endpoint/session/endwamp_global_logout_endpointAn absolute URL on Account Center, not under the issuer — a WAMP extension, described under Logout code_challenge_methods_supportedis["S256"]. There is nointrospection_endpoint, noregistration_endpoint, and nodevice_authorization_endpoint; those features are switched off. -
Register a client. Dynamic client registration is disabled, so you create the client through the app API with your own user session. You need
organization.applications.managein the app’s publisher organization.Terminal window curl -X POST "$API/api/apps/acme-bot/oauth-clients" \-H "Authorization: Bearer $USER_ACCESS_TOKEN" \-H 'Content-Type: application/json' \-d '{"name": "Acme Desktop","applicationType": "native","tokenEndpointAuthMethod": "none","redirectUris": ["com.acme.app:/oauth/callback","http://127.0.0.1:7777/callback"],"allowedScopes": ["openid", "profile", "email", "offline_access", "wamp:organization"],"allowRefreshTokens": true}'{"success": true,"client": {"id": "…","clientId": "wamp_3f2a…","name": "Acme Desktop","applicationType": "NATIVE","tokenEndpointAuthMethod": "NONE","redirectUris": ["com.acme.app:/oauth/callback", "http://127.0.0.1:7777/callback"],"postLogoutRedirectUris": [],"allowedScopes": ["openid", "profile", "email", "offline_access", "wamp:organization"],"allowRefreshTokens": true,"status": "ACTIVE"}}The generated
clientIdiswamp_followed by 48 hex characters. Stable readable client ids such aswamp-desktopexist only for first-party clients provisioned by an operator.Request fields:
Field Rules name1–120 characters applicationTypenativeorweb, lowercasetokenEndpointAuthMethodnoneorprivate_key_jwt.nativemust usenone.private_key_jwtrequires at least one unrevoked app key already registered, otherwisesigning_key_requiredredirectUris1–20 URIs, each ≤2048 characters. Rules below postLogoutRedirectUrisOptional, ≤20, validated by the same rules allowedScopesOptional subset of the five scopes. Defaults to all five. openidis always added if you omit itallowRefreshTokensOptional, defaults to trueRedirect URI rules, which differ by application type and reject some things RFC 8252 permits:
Application type Accepted Rejected webhttps://…;http://onlocalhostor127.0.0.1any other http://nativehttp://127.0.0.1[:port]/…;https://on a host you control; a reverse-domain custom scheme with an empty authority, e.g.com.acme.app:/oauth/callbackhttp://localhost;https://localhostandhttps://127.0.0.1; schemes without a dot such asmyapp:;about: blob: chrome: data: file: javascript:Fragments, usernames, and passwords are rejected in any URI. Duplicates are collapsed. A rejected URI answers
400 {"success": false, "error": "invalid_redirect_uri"}with no indication of which URI or which rule failed. -
Send the user to the authorization endpoint. PKCE is mandatory for every client, public or not. Generate a high-entropy
code_verifier, and send its S256 challenge.GET $ISSUER/auth?client_id=wamp_3f2a…&response_type=code&redirect_uri=com.acme.app:/oauth/callback&scope=openid%20profile%20email%20offline_access%20wamp:organization&state=<opaque, bound to this browser>&nonce=<opaque, bound to this request>&code_challenge=<BASE64URL(SHA256(code_verifier))>&code_challenge_method=S256Parameter Required Notes client_idyes response_typeyes codeis the only supported valueredirect_uriyes Must match a registered URI exactly scopeyes Must include openid; every scope must be in the client’sallowedScopescode_challengeyes PKCE is required always code_challenge_methodyes S256onlystatestrongly recommended Your CSRF binding; WAMP returns it unchanged noncestrongly recommended Echoed into the ID token; compare it resourcedo not send Each client has exactly one legal resource identifier, applied automatically. Any other value fails with an unknown-resource-indicator error request,request_urinot supported Request objects and PAR are disabled -
The user signs in and chooses an organization. WAMP redirects the browser to Account Center, which runs login and then a consent screen. Consent requires picking one organization: a grant’s tenant is part of the grant, not a header you send later. Choosing a different organization on a later sign-in replaces the grant rather than mutating it.
Your app is not involved in this leg and must not try to drive it — the interaction API is same-origin-guarded to Account Center. You get control back at your
redirect_uriwithcodeandstate, or with an OAutherrorparameter (access_deniedif the user aborted). -
Exchange the code within 60 seconds. The authorization code TTL is 60 seconds and is not configurable.
Terminal window curl -X POST "$ISSUER/token" \-H 'Content-Type: application/x-www-form-urlencoded' \-d grant_type=authorization_code \-d code=<code> \-d redirect_uri=com.acme.app:/oauth/callback \-d client_id=wamp_3f2a… \-d code_verifier=<code_verifier>{"access_token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImF0K2p3dCIsImtpZCI6ImVkZHNhLTEifQ…","expires_in": 600,"id_token": "eyJhbGciOiJFZERTQSI…","refresh_token": "…","scope": "openid profile email offline_access wamp:organization","token_type": "Bearer"}A
private_key_jwtclient adds two form fields instead of relying on the public-client path:-d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer-d client_assertion=<JWT>The assertion must be signed EdDSA with one of the app’s registered public keys (
kidin the header), withissandsubset to yourclient_idandaudset to$ISSUER.refresh_tokenis present only when the client allows refresh tokens andoffline_accesswas in the granted scope. Its absence is not an error — see Common failures. -
Validate the ID token. Fetch the JWKS from the
jwks_uriin the discovery document and cache it; the endpoint setsCache-Control: public, max-age=600, must-revalidateand allows any origin. Then check, in this order:Check Expected Header algEdDSA. Reject anything else, includingnoneand anyHS*Header kidPresent in the JWKS. Refetch once on a miss, then fail issExactly $ISSUER, string-comparedaudExactly your client_idexp/iatWithin your clock skew allowance nonceEqual to the value you sent subYour user key. See below subis pairwise: the same human has a differentsubfor every client, derived by an HMAC the server keeps secret. It is stable for your client and opaque. Never parse it, never expect an email or UUID, and never use it to join users across two of your own clients.Access tokens are also JWTs (
typ: at+jwt), signed EdDSA against the same JWKS, withaudequal to yourclient_id. Verify them locally: RFC 7662 introspection is disabled, so there is no endpoint that will do it for you. -
Read claims. WAMP releases the claims for each granted scope in both the ID token and at the userinfo endpoint. The access token additionally carries
grant_id, plus theemailand organization claims when those scopes were granted.Terminal window curl -s "$ISSUER/me" -H "Authorization: Bearer $ACCESS_TOKEN"Access tokens are rejected as query parameters, so the header is the only way to present one.
Scope Claims Meaning openidsubRequired; added for you if you omit it profilenameDisplay name. No given_name,picture, or other profile claims existemailemail,email_verifiedSee the warning below offline_access— Not a claim; enables refresh tokens wamp:organizationorg_id,org_slug,membership_id,app_id,installation_idThe tenant this grant is bound to, the user’s membership in it, and your app and installation ids Those five are the only scopes that exist. Any other string is rejected as
invalid_scopeat client registration. -
Refresh. Refresh tokens rotate: each use invalidates the previous token and returns a new one. Store the new one before you use it.
Terminal window curl -X POST "$ISSUER/token" \-H 'Content-Type: application/x-www-form-urlencoded' \-d grant_type=refresh_token \-d refresh_token=<refresh_token> \-d client_id=wamp_3f2a…Every refresh re-resolves the grant against live state: the organization must still be active, the membership still active, the client still active, the app not suspended, and the installation and its client approval still live. If any of that has changed the refresh fails, which is the intended revocation mechanism — there is nothing to poll.
Token lifetimes
Section titled “Token lifetimes”| Artifact | Lifetime | Configurable |
|---|---|---|
| Authorization code | 60 seconds | No |
| Access token | 600 seconds by default | By the operator |
| ID token | Same as the access token | By the operator |
| Refresh token | 30 days by default, rotating | By the operator |
| Grant and browser session | Same as the refresh token | By the operator |
Treat these as defaults you read from expires_in, not constants — an operator
can change all but the code TTL.
Logout
Section titled “Logout”Two endpoints, for two different jobs.
RP-initiated logout (end_session_endpoint) is the standard flow. WAMP shows
an HTML confirmation page before ending the session, so this is a browser
navigation, not a background request:
GET $ISSUER/session/end ?client_id=wamp_3f2a… &post_logout_redirect_uri=https://app.example.com/signed-outThe post_logout_redirect_uri must be registered on the client, or the request
is rejected.
Global logout is the non-standard member wamp_global_logout_endpoint in the
discovery document. Use it when you want the whole browser signed out of WAMP,
including Account Center’s own session cookie, which the standard endpoint cannot
clear because it is bound to a different host. It takes the same two parameters
and redirects into end_session for you.
To drop one token rather than a session, use the revocation endpoint:
curl -X POST "$ISSUER/token/revocation" \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d token=<access or refresh token> \ -d client_id=wamp_3f2a…Common failures
Section titled “Common failures”| What you see | Cause | Fix |
|---|---|---|
400 invalid_redirect_uri when registering a native client |
http://localhost — only 127.0.0.1 is accepted for native |
Use http://127.0.0.1:<port>/… |
400 invalid_redirect_uri for a custom scheme that looks right |
The URI has an authority: com.acme.app://cb |
Drop one slash: com.acme.app:/cb |
400 invalid_redirect_uri for myapp:/cb |
A custom scheme must be reverse-domain and contain a dot | Use com.example.myapp:/cb |
400 invalid_scope when registering |
A scope outside the five supported strings | Send only openid, profile, email, offline_access, wamp:organization |
400 signing_key_required when registering |
private_key_jwt requested with no app key on file |
Register a public key first, then create the client |
400 invalid_auth_method |
applicationType: "native" with private_key_jwt |
Native clients must use none |
| The consent screen shows no organizations to pick | The user’s organizations have no active installation of your app with a live grant for this client, or the installation is limited to assigned members and this user is not one | Get the installation and the client approval in place; check member assignment |
400 scope_not_allowed at consent |
The authorize request asked for a scope outside the client’s allowedScopes |
Patch the client, or narrow the request |
Token response has no refresh_token |
offline_access was not requested, or the client was created with allowRefreshTokens: false — in which case offline_access was silently stripped from allowedScopes at registration and asking for it now fails consent with scope_not_allowed |
Recreate or patch the client with allowRefreshTokens: true, then request offline_access |
invalid_grant at the token endpoint |
The code is older than 60 seconds, already used, issued to a different redirect_uri, or the code_verifier does not match the challenge |
Shorten the round trip; send the identical redirect_uri; check your verifier storage |
| An unknown-resource-indicator error | You sent a resource parameter |
Omit it |
404 at an introspection or registration endpoint |
Both features are disabled | Verify JWTs against the JWKS; create clients through the app API |
| Refresh suddenly fails for one user | Their membership, the installation, the client approval, or the app status changed | Re-run the authorization flow; there is no repair call |
401 {"error": "Invalid or expired token"} from an $API route |
You sent an OIDC access token to a route that wants a WAMP user session, or the reverse | Match the credential to the surface — see Apps and credentials |
Failures at the authorization endpoint arrive as OAuth error parameters on your
redirect_uri. Failures during login and consent are shown by Account Center.
Failures at the token endpoint are standard OAuth JSON error bodies. Failures at
$API routes use one of WAMP’s three error shapes, listed in
Overview.