Authorization code and PKCE Developer

Last updated Aug 16, 2026

Authorization code and PKCE

Apps created in the portal use the Authorization Code + PKCE (S256) flow. The browser only receives a short-lived authorization code. Keep code_verifier in a trusted environment and use it to exchange tokens. Confidential clients also use client_secret on the backend. JWT assertion clients also sign a client_assertion on the backend.

Production Issuer example:

ISSUER=https://api-passport.swaymoon.com

Use the discovery document at {ISSUER}/.well-known/openid-configuration for actual endpoints.

1. Generate PKCE

  1. Generate a high-entropy code_verifier (43–128 URL-safe characters).
  2. code_challenge = BASE64URL(SHA256(code_verifier)) (no padding).
  3. code_challenge_method = S256.

Store code_verifier and state in a server session, an encrypted cookie, or secure session storage available to a public client. Do not leak them to third-party scripts.

2. Send the user to authorize

Build and redirect to the authorize endpoint, for example:

GET /oauth2/authorize?
  response_type=code
  &client_id=swm_xxxxxxxx
  &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
  &scope=openid%20name%20picture%20email
  &state=RANDOM_STATE
  &code_challenge=CHALLENGE
  &code_challenge_method=S256
Host: api-passport.swaymoon.com
ParameterRequirement
response_typeAlways code
client_idIssued by the Developer portal
redirect_uriMust be registered and match exactly
scopeSpace-separated; at least openid
stateCSRF protection; the callback must match the original value
code_challenge / code_challenge_methodRequired, S256

The user signs in on passport.swaymoon.com if needed, then confirms authorization. On success, the browser goes to:

https://app.example.com/callback?code=...&state=...

If the user cancels, the app typically does not receive a usable code. Handle the returned error or cancel parameters.

3. Exchange the authorization code for tokens

POST to the token endpoint (application/x-www-form-urlencoded).

Confidential client

Use HTTP Basic on the backend:

POST /oauth2/token
Host: api-passport.swaymoon.com
Authorization: Basic BASE64(urlencode(client_id):urlencode(client_secret))
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=https://app.example.com/callback
&code_verifier=VERIFIER

Public client

Do not send Basic or client_secret. Include client_id in the form:

POST /oauth2/token
Host: api-passport.swaymoon.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=https://app.example.com/callback
&client_id=swm_xxxxxxxx
&code_verifier=VERIFIER

JWT assertion client

Do not send Basic or client_secret. On the backend, sign client_assertion with the private key that matches the registered public key, and put it in the form:

POST /oauth2/token
Host: api-passport.swaymoon.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=https://app.example.com/callback
&client_id=swm_xxxxxxxx
&code_verifier=VERIFIER
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=SIGNED_JWT

client_assertion is a JWT signed with ES256. The JWS header must include a kid that matches a public key registered in the portal. Claims:

ClaimValue
issYour client_id
subYour client_id (same as iss)
audtoken_endpoint from discovery (production is typically https://api-passport.swaymoon.com/oauth2/token)
jtiA new unique value on every request
iatIssued-at time
expExpiry (a few minutes is enough; must be in the future)

Keep the private key on your server. The portal rejects private JWKs. Runnable curl / Python snippets are in Examples and troubleshooting.

Notes:

  • redirect_uri must match the authorization request.
  • code_verifier must correspond to the code_challenge sent at authorize time.
  • The authorization code is single-use and short-lived.

Example success response (fields depend on the actual response):

{
  "access_token": "...",
  "refresh_token": "...",
  "id_token": "...",
  "token_type": "Bearer",
  "expires_in": 300,
  "scope": "openid name picture email"
}
  • Confidential and JWT assertion clients usually receive refresh_token. Store all tokens securely. Do not send client_secret, a private key, or a Refresh Token to an untrusted frontend.
  • Public clients get no refresh_token. After the Access Token expires, start authorization code + PKCE again.

4. Refresh the access token (confidential and JWT assertion)

After the Access Token expires, confidential clients:

POST /oauth2/token
Authorization: Basic ...
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=REFRESH_TOKEN

JWT assertion clients must not use Basic. Sign a new client_assertion instead:

POST /oauth2/token
Host: api-passport.swaymoon.com
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=REFRESH_TOKEN
&client_id=swm_xxxxxxxx
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=SIGNED_JWT

On success, use the new access_token and any rotated refresh_token in the response. Under token rotation, the old Refresh Token may become invalid immediately. Always use the latest response.

Public clients do not support this step.

5. User-experience notes

  • Users who are not signed in go to the Swaymoon Account sign-in page first, then to authorization confirmation.
  • Sensitive actions may trigger sign-in again (step-up authentication). That is expected.
  • Consent-page copy and optional-scope checkboxes are described in Scopes and consent.

6. Security checklist

  • Always persist and verify code_verifier and state.
  • Confidential client: exchange tokens with client_secret only on the backend.
  • JWT assertion client: sign client_assertion only on the backend; the private key and kid must match a public key registered in the portal.
  • Public client: never invent or hard-code client_secret; rely on PKCE and registered Redirect URIs.
  • Use HTTPS for callbacks and transport.
  • Limit scope to the minimum needed.
  • Handle errors such as invalid_grant, invalid_client, and invalid_request.

Next: read Tokens and user info to validate tokens and create a local user record.