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
- Generate a high-entropy
code_verifier(43–128 URL-safe characters). code_challenge = BASE64URL(SHA256(code_verifier))(no padding).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
| Parameter | Requirement |
|---|---|
response_type | Always code |
client_id | Issued by the Developer portal |
redirect_uri | Must be registered and match exactly |
scope | Space-separated; at least openid |
state | CSRF protection; the callback must match the original value |
code_challenge / code_challenge_method | Required, 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:
| Claim | Value |
|---|---|
iss | Your client_id |
sub | Your client_id (same as iss) |
aud | token_endpoint from discovery (production is typically https://api-passport.swaymoon.com/oauth2/token) |
jti | A new unique value on every request |
iat | Issued-at time |
exp | Expiry (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_urimust match the authorization request.code_verifiermust correspond to thecode_challengesent 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 sendclient_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_verifierandstate. - Confidential client: exchange tokens with
client_secretonly on the backend. - JWT assertion client: sign
client_assertiononly on the backend; the private key andkidmust 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
scopeto the minimum needed. - Handle errors such as
invalid_grant,invalid_client, andinvalid_request.
Next: read Tokens and user info to validate tokens and create a local user record.