Public OAuth clients — browser SPAs, mobile apps, desktop apps, CLIs — cannot safely embed a client_secret. Anyone with access to the binary can extract it. PKCE (RFC 7636 ) replaces the static secret with a per-flow one-time secret that is provably tied to the device that started the flow.
If your app has any client-side component, mark its OAuth app as public in Settings → OAuth apps and follow this guide.
How PKCE works
The flow adds two parameters to the standard authorization code flow:
- Your client generates a random
code_verifierand its SHA-256 hash, called thecode_challenge. - The
code_challengeis sent to/oauth/authorize. The server remembers it alongside the issuedcode. - When exchanging the
codeat/oauth/token, the client sends the plaincode_verifier. The server hashes it and compares to the stored challenge. - An attacker who intercepts the redirect (e.g. by registering the same URL scheme on a mobile device) gets the
codebut not thecode_verifier, so they cannot exchange it.
Step 1: Generate verifier and challenge
function base64url(bytes: Uint8Array) {
return btoa(String.fromCharCode(...bytes))
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
}
// 32 random bytes -> 43 char base64url string
const verifierBytes = crypto.getRandomValues(new Uint8Array(32));
const codeVerifier = base64url(verifierBytes);
const challengeBytes = new Uint8Array(
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier)),
);
const codeChallenge = base64url(challengeBytes);
sessionStorage.setItem("revroute_pkce_verifier", codeVerifier);code_verifier must be 43–128 characters, URL-safe (A-Z a-z 0-9 - . _ ~). The verifier never leaves the device until step 3.
Step 2: Redirect to /oauth/authorize
https://app.revroute.ru/oauth/authorize
?client_id=dub_app_xxx
&redirect_uri=https%3A%2F%2Fyourapp.com%2Fcallback
&response_type=code
&scope=links.read+links.write
&state=<csrf>
&code_challenge=<codeChallenge>
&code_challenge_method=S256code_challenge_method=S256 is required. The legacy plain method is not supported.
Step 3: Exchange the code
Note: there is no client_secret in this request, but there is a code_verifier.
curl -X POST https://api.revroute.ru/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "client_id=dub_app_xxx" \
--data-urlencode "code=AbCd1234..." \
--data-urlencode "redirect_uri=https://yourapp.com/callback" \
--data-urlencode "code_verifier=$CODE_VERIFIER"Step 4: Refresh tokens
Refresh requests are also unauthenticated by client_secret for public clients — pass the client_id instead:
curl -X POST https://api.revroute.ru/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=refresh_token" \
--data-urlencode "client_id=dub_app_xxx" \
--data-urlencode "refresh_token=yyy"Refresh tokens are rotated on every use. If your app runs on a device the user might re-open after a long time, persist the latest refresh token in secure storage (Keychain / EncryptedSharedPreferences / IndexedDB behind a passphrase).
Mobile redirect URIs
Mobile apps usually use a custom URL scheme as the redirect_uri:
yourapp://oauth/callbackRegister this scheme in your app and the exact same URI in your RevRoute OAuth app’s allow-list. RevRoute treats redirect_uri as an opaque exact-match string — yourapp://callback/ and yourapp://callback are different URIs.
On iOS prefer Universal Links (https://yourapp.com/oauth/callback) over custom schemes — they cannot be hijacked by another app.
Checklist
code_challenge_methodisS256(notplain)code_verifieris 43–128 URL-safe characters, freshly generated per flow- The verifier is never sent to
/oauth/authorize, only to/oauth/token - The same
redirect_uriis used at both the authorize and token endpoints - The OAuth app in RevRoute is marked public
- Tokens are stored in platform-native secure storage, not plain files or
localStorage
For the basic mechanics shared with confidential clients, see the OAuth quickstart.