Skip to content

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:

  1. 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.
  2. The verifier was lost during the redirect. Storage was cleared (e.g., browser cleared sessionStorage between pages) or the callback opened in a different tab/window.
  3. Encoding mismatch. The verifier or challenge was incorrectly encoded — for example, using standard base64 instead of base64url (without padding, using - and _ instead of + and /).
  4. Extra whitespace or newline characters were appended to the verifier before hashing.

Fix

  1. Store the verifier before redirecting:
const codeVerifier = generateCodeVerifier();
sessionStorage.setItem("pkce_code_verifier", codeVerifier);
  1. Retrieve the exact same value on callback:
const codeVerifier = sessionStorage.getItem("pkce_code_verifier");
  1. 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.

  1. Ensure the redirect stays in the same tab (for SPAs using window.location.href). If using window.open(), sessionStorage won'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

  1. Copy the exact URI from your app's authorization request (check the browser address bar or network tab).
  2. Compare it character by character with the URIs in your developer portal.
  3. Check for these common differences:
IssueAuthorization RequestDeveloper Portal
Trailing slashhttps://app.example.com/callback/https://app.example.com/callback
Port mismatchhttp://localhost:5173/callbackhttp://localhost:3000/callback
Scheme mismatchhttp://app.example.com/callbackhttps://app.example.com/callback
Path mismatchhttps://app.example.com/auth/callbackhttps://app.example.com/callback
Case differencehttps://App.Example.com/callbackhttps://app.example.com/callback
  1. For the token exchange, ensure redirect_uri is 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

  1. Exchange the code immediately when your callback receives it. Don't add unnecessary delays.

  2. 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 ...
}
  1. If the code is expired, start a new flow. Redirect the user to /oauth/authorize again:
try {
  await exchangeCode(code, codeVerifier);
} catch (error) {
  if (error.message.includes("expired")) {
    // Start a new authorization flow
    await redirectToSingleSign();
  }
}
  1. 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 openid scope is missing (required for all OIDC requests).

Fix

  1. 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")
  1. Valid scope names: openid, profile, email, phone, address, offline_access.

  2. Always include openid as the first scope.

  3. Check the developer portal to confirm the scopes are enabled for your application.

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

  1. Verify your origin is registered. SingleSign allows CORS requests from origins that match your registered redirect URIs. If your app runs at http://localhost:3000 and your redirect URI is http://localhost:3000/callback, CORS should be allowed for that origin.

  2. 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!
  1. Don't set custom Origin headers. Browsers set the Origin header automatically. If you're using a fetch wrapper or Axios interceptor that modifies headers, ensure it doesn't override the Origin.

  2. Check for browser extensions that may be blocking or modifying CORS headers.

  3. 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 }),
});
  1. 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

  1. Find your Platform GUID in the developer portal dashboard.
  2. 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

  1. 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
});
  1. Use the correct client secret as the signing key for HMAC-based algorithms.

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

  1. 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);
  1. 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

  1. 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);
  1. Clear the URL parameters after processing to prevent re-processing on page refresh:
// After successful callback processing
window.history.replaceState({}, "", "/dashboard");

Quick Reference

ErrorMost Likely CauseQuick Fix
invalid_grant (PKCE)Verifier regenerated instead of storedStore verifier in sessionStorage before redirect
redirect_uri_mismatchTrailing slash or port differenceCompare URIs character by character
invalid_grant (expired)Code older than 10 minutesExchange code immediately on callback
invalid_scopeTypo or missing openidCheck spelling, always include openid
CORS errorOrigin doesn't match redirect URIRegister matching redirect URI
invalid_requestMissing platform_guidAdd platform_guid to every request
Signature errorWrong algorithm (not HS512)Specify HS512 in JWT verification
Refresh rejectedOld token used after rotationAlways save the latest refresh token

Still Stuck?

If you've tried the fixes above and are still experiencing issues:

  1. Check the Error Reference for detailed error code documentation.
  2. Enable verbose logging in your application to capture the full request/response cycle.
  3. Use browser DevTools (Network tab) to inspect the authorization redirect and token exchange.
  4. Test with curl to isolate whether the issue is in your code or the request itself.
  5. Contact SingleSign support with your platform_guid, client_id, and the full error response.

Last updated: 2026-02-16