Avnology ID
SDKsTypeScript SDKOAuth 2.1

Code Exchange

Exchange authorization codes for tokens with PKCE verification using the TypeScript SDK.

Code Exchange

After the user authorizes your application, the authorization server redirects them back to your redirectUri with an authorization code. Exchange this code for access and refresh tokens.

exchangeCode()

Exchange an authorization code for a token set.

client.oauth.exchangeCode(params: ExchangeCodeParams): Promise<TokenSet>

Parameters

NameTypeRequiredDescription
codestringyesAuthorization code from the callback URL
codeVerifierstringyesPKCE code verifier from buildAuthorizationUrl()
redirectUristringyesMust match the redirectUri used in the authorization request

Returns

interface TokenSet {
  accessToken: string;      // JWT access token
  refreshToken: string;     // Refresh token (if offline_access scope was requested)
  idToken: string;          // OIDC ID token (if openid scope was requested)
  tokenType: "Bearer";      // Always "Bearer"
  expiresIn: number;        // Access token lifetime in seconds (typically 900)
  expiresAt: string;        // ISO 8601 absolute expiry time
  scope: string;            // Space-separated granted scopes
}

Basic usage

import { AvnologyId } from "@avnology/sdk-typescript";

const client = new AvnologyId({
  baseUrl: "https://api.id.avnology.com",
  clientId: "app_abc123",
});

// On your callback page (e.g., /callback?code=AUTH_CODE&state=...)
const url = new URL(window.location.href);
const code = url.searchParams.get("code");
const returnedState = url.searchParams.get("state");

// Validate state to prevent CSRF
const savedState = sessionStorage.getItem("oauth_state");
if (returnedState !== savedState) {
  throw new Error("State mismatch -- possible CSRF attack");
}

// Retrieve the PKCE code verifier saved during authorization
const codeVerifier = sessionStorage.getItem("pkce_code_verifier");
if (!codeVerifier) {
  throw new Error("Missing PKCE code verifier");
}

// Exchange the code for tokens
const tokens = await client.oauth.exchangeCode({
  code: code!,
  codeVerifier,
  redirectUri: "https://myapp.com/callback",
});

console.log(tokens.accessToken);   // "eyJhbGciOiJSUzI1NiIs..."
console.log(tokens.refreshToken);  // "rt_abc123..."
console.log(tokens.idToken);       // "eyJhbGciOiJSUzI1NiIs..."
console.log(tokens.expiresIn);     // 900 (15 minutes)
console.log(tokens.scope);         // "openid profile email offline_access"

// Clean up session storage
sessionStorage.removeItem("pkce_code_verifier");
sessionStorage.removeItem("oauth_state");

Storing tokens securely

// Browser -- use the SDK's built-in token management
client.setTokens(tokens);
// The SDK will auto-refresh when the access token expires

// Server-side -- store in encrypted session
req.session.tokens = {
  accessToken: tokens.accessToken,
  refreshToken: tokens.refreshToken,
  expiresAt: tokens.expiresAt,
};

Decode the ID token

The ID token is a JWT containing user identity claims. The SDK provides a helper to decode it without verification (use server-side verification for security-critical checks).

const claims = client.oauth.decodeIdToken(tokens.idToken);

console.log(claims.sub);   // "usr_abc123" -- user ID
console.log(claims.email); // "[email protected]"
console.log(claims.name);  // "Jane Doe"
console.log(claims.iss);   // "https://id.avnology.com"
console.log(claims.aud);   // "app_abc123"
console.log(claims.nonce); // Matches the nonce from authorization
console.log(claims.org_id);   // "org_abc123" (if organizations scope)
console.log(claims.org_name); // "Acme Corp"

Full callback handler (React Router)

// routes/callback.tsx
import { redirect } from "react-router";
import { client } from "../lib/avnology";

export async function loader({ request }: { request: Request }) {
  const url = new URL(request.url);
  const code = url.searchParams.get("code");
  const state = url.searchParams.get("state");
  const error = url.searchParams.get("error");

  // Handle authorization errors
  if (error) {
    const description = url.searchParams.get("error_description");
    console.error("OAuth error:", error, description);
    return redirect(`/login?error=${encodeURIComponent(description || error)}`);
  }

  if (!code) {
    return redirect("/login?error=missing_code");
  }

  // Exchange code for tokens
  const codeVerifier = sessionStorage.getItem("pkce_code_verifier");
  const tokens = await client.oauth.exchangeCode({
    code,
    codeVerifier: codeVerifier!,
    redirectUri: `${url.origin}/callback`,
  });

  // Set tokens in the client
  client.setTokens(tokens);

  return redirect("/dashboard");
}

Error handling

import {
  InvalidGrantError,
  InvalidClientError,
  ExpiredCodeError,
} from "@avnology/sdk-typescript";

try {
  const tokens = await client.oauth.exchangeCode({
    code,
    codeVerifier,
    redirectUri,
  });
} catch (error) {
  if (error instanceof InvalidGrantError) {
    // Code was already used, expired, or code_verifier doesn't match
    showError("Authorization failed. Please try again.");
    redirectToLogin();
  } else if (error instanceof InvalidClientError) {
    // Client ID or secret is wrong
    console.error("OAuth client misconfigured:", error.message);
  } else if (error instanceof ExpiredCodeError) {
    // Authorization code expired (typically 10 minutes)
    showError("Authorization expired. Please try again.");
    redirectToLogin();
  }
}

Common errors

Error classHTTP statusWhen
InvalidGrantError400Code already used, expired, or PKCE mismatch
InvalidClientError401Wrong client ID or secret
ExpiredCodeError400Authorization code expired
InvalidRedirectUriError400Redirect URI doesn't match

See also

On this page