Skip to content

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
  1. An app in a publisher organization you administer — see Apps and credentials.
  2. An OIDC client under that app. Creating one is self-service.
  3. 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.

  1. 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 /auth
    token_endpoint /token
    userinfo_endpoint /me
    jwks_uri /jwks
    revocation_endpoint /token/revocation
    end_session_endpoint /session/end
    wamp_global_logout_endpoint An absolute URL on Account Center, not under the issuer — a WAMP extension, described under Logout

    code_challenge_methods_supported is ["S256"]. There is no introspection_endpoint, no registration_endpoint, and no device_authorization_endpoint; those features are switched off.

  2. 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.manage in 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 clientId is wamp_ followed by 48 hex characters. Stable readable client ids such as wamp-desktop exist only for first-party clients provisioned by an operator.

    Request fields:

    Field Rules
    name 1–120 characters
    applicationType native or web, lowercase
    tokenEndpointAuthMethod none or private_key_jwt. native must use none. private_key_jwt requires at least one unrevoked app key already registered, otherwise signing_key_required
    redirectUris 1–20 URIs, each ≤2048 characters. Rules below
    postLogoutRedirectUris Optional, ≤20, validated by the same rules
    allowedScopes Optional subset of the five scopes. Defaults to all five. openid is always added if you omit it
    allowRefreshTokens Optional, defaults to true

    Redirect URI rules, which differ by application type and reject some things RFC 8252 permits:

    Application type Accepted Rejected
    web https://…; http:// on localhost or 127.0.0.1 any other http://
    native http://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/callback http://localhost; https://localhost and https://127.0.0.1; schemes without a dot such as myapp:; 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.

  3. 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=S256
    Parameter Required Notes
    client_id yes
    response_type yes code is the only supported value
    redirect_uri yes Must match a registered URI exactly
    scope yes Must include openid; every scope must be in the client’s allowedScopes
    code_challenge yes PKCE is required always
    code_challenge_method yes S256 only
    state strongly recommended Your CSRF binding; WAMP returns it unchanged
    nonce strongly recommended Echoed into the ID token; compare it
    resource do not send Each client has exactly one legal resource identifier, applied automatically. Any other value fails with an unknown-resource-indicator error
    request, request_uri not supported Request objects and PAR are disabled
  4. 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_uri with code and state, or with an OAuth error parameter (access_denied if the user aborted).

  5. 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_jwt client 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 (kid in the header), with iss and sub set to your client_id and aud set to $ISSUER.

    refresh_token is present only when the client allows refresh tokens and offline_access was in the granted scope. Its absence is not an error — see Common failures.

  6. Validate the ID token. Fetch the JWKS from the jwks_uri in the discovery document and cache it; the endpoint sets Cache-Control: public, max-age=600, must-revalidate and allows any origin. Then check, in this order:

    Check Expected
    Header alg EdDSA. Reject anything else, including none and any HS*
    Header kid Present in the JWKS. Refetch once on a miss, then fail
    iss Exactly $ISSUER, string-compared
    aud Exactly your client_id
    exp / iat Within your clock skew allowance
    nonce Equal to the value you sent
    sub Your user key. See below

    sub is pairwise: the same human has a different sub for 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, with aud equal to your client_id. Verify them locally: RFC 7662 introspection is disabled, so there is no endpoint that will do it for you.

  7. 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 the email and 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
    openid sub Required; added for you if you omit it
    profile name Display name. No given_name, picture, or other profile claims exist
    email email, email_verified See the warning below
    offline_access Not a claim; enables refresh tokens
    wamp:organization org_id, org_slug, membership_id, app_id, installation_id The 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_scope at client registration.

  8. 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.

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.

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-out

The 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:

Terminal window
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…
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.