Troubleshooting
This page covers the most common issues developers encounter when integrating with SingleSign, along with their causes and step-by-step fixes.
PKCE Code Verifier Mismatch
Symptom
Token exchange fails with:
{
"error": "invalid_grant",
"error_description": "The code verifier does not match the code challenge."
}
Cause
The code_verifier sent to /oauth/token does not produce the same SHA-256 hash as the code_challenge sent to /oauth/authorize. This happens when:
- The verifier was regenerated instead of being stored and retrieved. A new random value was generated for the token request instead of using the original.
- The verifier was lost during the redirect. Storage was cleared (e.g., browser cleared
sessionStoragebetween pages) or the callback opened in a different tab/window. - Encoding mismatch. The verifier or challenge was incorrectly encoded — for example, using standard base64 instead of base64url (without padding, using
-and_instead of+and/). - Extra whitespace or newline characters were appended to the verifier before hashing.
Fix
- Store the verifier before redirecting:
const codeVerifier = generateCodeVerifier();
sessionStorage.setItem("pkce_code_verifier", codeVerifier);
- Retrieve the exact same value on callback:
const codeVerifier = sessionStorage.getItem("pkce_code_verifier");
- Verify your encoding. Test your implementation by computing the challenge manually:
# Generate challenge from verifier using openssl
echo -n "your-code-verifier-value" | openssl dgst -sha256 -binary | base64 | tr '+/' '-_' | tr -d '='
The output should exactly match the code_challenge you sent to /oauth/authorize.
- Ensure the redirect stays in the same tab (for SPAs using
window.location.href). If usingwindow.open(),sessionStoragewon't be shared.
Redirect URI Mismatch
Symptom
The authorization request fails with:
{
"error": "redirect_uri_mismatch",
"error_description": "The redirect_uri does not match any registered redirect URIs."
}
Or the token exchange fails because the redirect_uri doesn't match the one used in the authorization request.
Cause
SingleSign performs exact string matching on redirect URIs. Even a single character difference will cause a mismatch.
Fix
- Copy the exact URI from your app's authorization request (check the browser address bar or network tab).
- Compare it character by character with the URIs in your developer portal.
- Check for these common differences:
| Issue | Authorization Request | Developer Portal |
|---|---|---|
| Trailing slash | https://app.example.com/callback/ | https://app.example.com/callback |
| Port mismatch | http://localhost:5173/callback | http://localhost:3000/callback |
| Scheme mismatch | http://app.example.com/callback | https://app.example.com/callback |
| Path mismatch | https://app.example.com/auth/callback | https://app.example.com/callback |
| Case difference | https://App.Example.com/callback | https://app.example.com/callback |
- For the token exchange, ensure
redirect_uriis the same value used in the authorization request — even though no redirect actually occurs.
// Both requests MUST use the exact same redirect_uri
const redirectUri = "http://localhost:3000/callback";
// In authorization request:
params.set("redirect_uri", redirectUri);
// In token exchange:
body.set("redirect_uri", redirectUri); // Must be identical
Expired Authorization Code
Symptom
Token exchange fails with:
{
"error": "invalid_grant",
"error_description": "The authorization code has expired."
}
Cause
Authorization codes are valid for 10 minutes after issuance. If the user is slow to complete the flow, or if your callback processing is delayed, the code will expire.
Authorization codes are also single-use. If your app attempts to exchange the same code twice (e.g., due to a page refresh on the callback URL), the second attempt will fail.
Fix
-
Exchange the code immediately when your callback receives it. Don't add unnecessary delays.
-
Prevent double exchanges by checking if you've already processed the callback:
async function handleCallback(): Promise<void> {
// Prevent double processing on page refresh
if (sessionStorage.getItem("callback_processed")) {
return;
}
sessionStorage.setItem("callback_processed", "true");
// ... exchange the code ...
}
- If the code is expired, start a new flow. Redirect the user to
/oauth/authorizeagain:
try {
await exchangeCode(code, codeVerifier);
} catch (error) {
if (error.message.includes("expired")) {
// Start a new authorization flow
await redirectToSingleSign();
}
}
- For server-side apps, ensure your server clock is accurate. A clock skew can make codes appear expired prematurely.
Invalid Scope
Symptom
The authorization request or token response returns:
{
"error": "invalid_scope",
"error_description": "The requested scope is invalid or not registered for this client."
}
Cause
- A scope name is misspelled.
- A scope was requested that is not registered for the application in the developer portal.
- The
openidscope is missing (required for all OIDC requests).
Fix
- Check your scope string for typos:
✅ openid profile email
❌ open_id profile email (underscore in "open_id")
❌ openid proflie email (typo in "proflie")
❌ openid profile e-mail (hyphen in "e-mail")
-
Valid scope names:
openid,profile,email,phone,address,offline_access. -
Always include
openidas the first scope. -
Check the developer portal to confirm the scopes are enabled for your application.
-
Separate scopes with spaces (or
+in URLs), not commas:
✅ scope=openid+profile+email
❌ scope=openid,profile,email
CORS Errors
Symptom
Browser console shows:
Access to fetch at 'https://api.singlesign.com/oauth/token' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.
Cause
Cross-Origin Resource Sharing (CORS) restrictions prevent your browser-based app from calling SingleSign endpoints directly. This typically happens with the /oauth/token endpoint when called from a SPA.
Fix
-
Verify your origin is registered. SingleSign allows CORS requests from origins that match your registered redirect URIs. If your app runs at
http://localhost:3000and your redirect URI ishttp://localhost:3000/callback, CORS should be allowed for that origin. -
Check that the origin matches exactly:
Redirect URI: http://localhost:3000/callback
Origin header: http://localhost:3000 ← derived from redirect URI
If redirect URI is: http://localhost:5173/callback
But app runs on: http://localhost:3000 ← origin mismatch!
-
Don't set custom
Originheaders. Browsers set theOriginheader automatically. If you're using a fetch wrapper or Axios interceptor that modifies headers, ensure it doesn't override theOrigin. -
Check for browser extensions that may be blocking or modifying CORS headers.
-
Use a backend proxy as a workaround (not recommended for production):
// Instead of calling SingleSign directly from the browser,
// proxy through your own server:
const response = await fetch("/api/auth/token", {
method: "POST",
body: JSON.stringify({ code, codeVerifier }),
});
- For the authorize endpoint, you should be redirecting (not using fetch). The authorize endpoint is navigated to via
window.location.href, which is not subject to CORS.
Missing or Invalid platform_guid
Symptom
{
"error": "invalid_request",
"error_description": "The platform_guid parameter is required."
}
Cause
Every SingleSign endpoint requires the platform_guid parameter. It may be missing, empty, or invalid.
Fix
- Find your Platform GUID in the developer portal dashboard.
- Include it in every request:
// For GET requests — as a query parameter:
const url = `https://api.singlesign.com/oauth/authorize?platform_guid=${platformGuid}&...`;
// For POST requests — in the request body:
const body = new URLSearchParams({
platform_guid: platformGuid,
grant_type: "authorization_code",
// ...
});
Token Signature Verification Fails
Symptom
Your JWT library throws a signature verification error when validating an access token or ID token.
Cause
- Wrong algorithm specified. SingleSign uses HS512 (HMAC-SHA-512).
- Wrong signing key. For HS512, the key is typically your client secret.
- Tokens from a different environment or platform.
Fix
- Specify the correct algorithm:
import { jwtVerify } from "jose";
const secret = new TextEncoder().encode("your-client-secret");
const { payload } = await jwtVerify(token, secret, {
algorithms: ["HS512"], // Not RS256, not HS256 — HS512
});
-
Use the correct client secret as the signing key for HMAC-based algorithms.
-
For JWKS-based verification, fetch the keys from the correct platform:
https://api.singlesign.com/oauth/jwks?platform_guid=YOUR_PLATFORM_GUID
Refresh Token Rejected
Symptom
{
"error": "invalid_grant",
"error_description": "The refresh token is invalid or has been revoked."
}
Cause
- The refresh token was already used and rotation issued a new one (the old one is now invalid).
- The refresh token was explicitly revoked.
- The refresh token has expired.
- The user revoked consent.
Fix
- Always store the latest refresh token. When you refresh, the response may include a new
refresh_token. Discard the old one and save the new one.
const tokens = await refreshAccessToken(oldRefreshToken);
// Save the NEW refresh token, not the old one
storage.set("refresh_token", tokens.refresh_token);
- If the refresh token is rejected, redirect to login. The user needs to re-authenticate.
try {
await refreshAccessToken(refreshToken);
} catch {
// Refresh token is no longer valid
clearSession();
redirectToLogin();
}
User Gets Stuck in a Redirect Loop
Symptom
The browser keeps redirecting between your app and SingleSign without completing the login.
Cause
- Your app's callback handler is redirecting back to the login route, which redirects to SingleSign again.
- The authorization code is being consumed before your callback handler runs (e.g., a middleware intercept).
Fix
- Check your callback route logic. Make sure the callback handler exchanges the code before any authentication middleware redirects:
// DON'T protect the callback route with auth middleware
app.get("/auth/callback", handleCallback); // No auth middleware here
// DO protect other routes
app.get("/dashboard", requireAuth, handleDashboard);
- Clear the URL parameters after processing to prevent re-processing on page refresh:
// After successful callback processing
window.history.replaceState({}, "", "/dashboard");
Quick Reference
| Error | Most Likely Cause | Quick Fix |
|---|---|---|
invalid_grant (PKCE) | Verifier regenerated instead of stored | Store verifier in sessionStorage before redirect |
redirect_uri_mismatch | Trailing slash or port difference | Compare URIs character by character |
invalid_grant (expired) | Code older than 10 minutes | Exchange code immediately on callback |
invalid_scope | Typo or missing openid | Check spelling, always include openid |
| CORS error | Origin doesn't match redirect URI | Register matching redirect URI |
invalid_request | Missing platform_guid | Add platform_guid to every request |
| Signature error | Wrong algorithm (not HS512) | Specify HS512 in JWT verification |
| Refresh rejected | Old token used after rotation | Always save the latest refresh token |
Still Stuck?
If you've tried the fixes above and are still experiencing issues:
- Check the Error Reference for detailed error code documentation.
- Enable verbose logging in your application to capture the full request/response cycle.
- Use browser DevTools (Network tab) to inspect the authorization redirect and token exchange.
- Test with curl to isolate whether the issue is in your code or the request itself.
- Contact SingleSign support with your
platform_guid,client_id, and the full error response.
Last updated: 2026-02-16
Related docs
- SingleSign Developer DocumentationLearn how to integrate SingleSign identity and authentication into your applications using OAuth 2.0 and OpenID Connect.
- the Error Reference docsComplete reference of SingleSign OAuth error codes, error response format, and troubleshooting tips for each error.
- SaaS single sign-onSSO for SaaS applications using OAuth 2.0 and OpenID Connect: the flow to pick, the scopes to request, and a working integration path with SingleSign.
- React authentication with PKCEReact authentication done the recommended way: Authorization Code with PKCE, and no client secret in your JavaScript bundle.
- Mobile app authenticationMobile app authentication for iOS and Android: Authorization Code with PKCE in a system browser, custom scheme redirects, and secure token storage on device.
- Identity provider security, by designIdentity provider security at SingleSign: PKCE-protected code exchange, redirect URI allow-lists, MFA, session and device control, and rate limiting.
- identity provider featuresThe identity provider features SingleSign ships: OAuth 2.0 and OpenID Connect endpoints, PKCE, scopes and consent, MFA, and instant revocation.
- free identity providerSingleSign pricing in one line: sign-in is free, with no monthly-active-user meter. See exactly what is included, and what SingleSign Mail costs for businesses.