Integration Guide

Last updated: December 2025

This guide walks you through integrating our OAuth2/OpenID Connect authentication into your web application. We’ll cover the complete authorization code flow with PKCE, which is the recommended flow for web applications.

> ⭐ Recommended Approach: We strongly recommend using a compliant OAuth2/OIDC client library like oidc-client-ts v3.4.1. Client libraries handle all the client-side complexity of OAuth2/OIDC flows, PKCE generation, token management, and automatic discovery endpoint configuration. This makes integration much simpler, more secure, and less error-prone than implementing the flow manually.

Prerequisites

Before you start, make sure you have:

  • Your Tenant ID – Your OAuth2 client ID (login to https://portal.agglestone.com to view your Tenant Id)
  • Your Base URLhttps://auth.agglestone.com/tenant/{tenantId}. Note: Your actual server URL may differ – check your tenant portal for the correct URL.
  • A compliant OAuth2/OIDC client library (see Recommended Client Libraries below)

⚠️ Important: Your base URL must include /tenant/{tenantId}. Once configured, all endpoints are relative to this base URL:

OAuth Authority: {BASE_URL}/v2.0/Auth

Discovery Endpoint: {BASE_URL}/v2.0/Auth/.well-known/openid-configuration

User Management: {BASE_URL}/api/Users

Group Management: {BASE_URL}/api/Groups

> πŸ’‘ Tip: Want to learn more about the discovery endpoint and how it automatically configures your client library? See Understanding the Discovery Endpoint at the end of this guide.

Recommended Client Libraries

We strongly recommend using a compliant OAuth2/OIDC client library rather than implementing the flow manually. Client libraries handle:

  • βœ… Automatic discovery endpoint configuration
  • βœ… PKCE code generation and validation
  • βœ… Token management and automatic refresh
  • βœ… Secure token storage
  • βœ… CSRF protection with state parameters
  • βœ… Error handling and edge cases
  • βœ… Browser compatibility

Recommended: oidc-client-ts v3.4.1

For TypeScript/JavaScript applications, we recommend oidc-client-ts v3.4.1:

npm install oidc-client-ts@^3.4.1

Why oidc-client-ts?

  • βœ… Full OIDC support with automatic discovery
  • βœ… Built-in PKCE support
  • βœ… Excellent TypeScript support
  • βœ… Token management and automatic refresh
  • βœ… Works in browsers and Node.js
  • βœ… Actively maintained and widely used
  • βœ… GitHub: https://github.com/authts/oidc-client-ts

Quick Start with oidc-client-ts

Here’s how simple integration becomes with a client library:

// TypeScript
import { UserManager } from 'oidc-client-ts';

const tenantId: string = 'your-tenant-id-here';
const BASE_URL: string = `https://auth.agglestone.com/tenant/${tenantId}`;

// Configure once - the library handles everything else!
const userManager = new UserManager({
  authority: `${BASE_URL}/v2.0/Auth`,  // Library automatically discovers endpoints
  client_id: tenantId,
  redirect_uri: 'https://yourapp.com/callback',
  response_type: 'code',
  scope: 'openid profile email',
  automaticSilentRenew: true
});

// Login - just one line!
await userManager.signinRedirect();

// Handle callback - just one line!
const user = await userManager.signinRedirectCallback();
const accessToken: string = user.access_token;

That’s it! The library handles PKCE, state management, token exchange, and refresh automatically.

Other Client Libraries

You can also use other OAuth2/OIDC compliant libraries:

  • C# / .NET: IdentityModel.OidcClient or Microsoft.AspNetCore.Authentication.OpenIdConnect
  • Python: authlib or requests-oauthlib
  • JavaScript: @azure/msal-browser

See OAuth2 and OIDC Overview for more recommendations.

> πŸ’‘ Using a client library? You can skip ahead to Using Client Libraries for a complete example. The manual implementation steps below are for reference or if you need to implement the flow without a library.

Understanding the Flow

The authorization code flow with PKCE works like this:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Browser   β”‚         β”‚  Your App    β”‚         β”‚  Our Auth   β”‚
β”‚   (User)    β”‚         β”‚  (Client)    β”‚         β”‚   Server    β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
       β”‚                       β”‚                         β”‚
       β”‚  1. Click "Login"     β”‚                         β”‚
       │──────────────────────>β”‚                         β”‚
       β”‚                       β”‚                         β”‚
       β”‚  2. Redirect to       β”‚                         β”‚
       β”‚     authorization     β”‚                         β”‚
       β”‚     endpoint         β”‚                         β”‚
       β”‚<──────────────────────│                         β”‚
       β”‚                       β”‚                         β”‚
       β”‚  3. Redirect to auth  β”‚                         β”‚
       β”‚     server            β”‚                         β”‚
       │────────────────────────────────────────────────>β”‚
       β”‚                       β”‚                         β”‚
       β”‚  4. User authenticatesβ”‚                         β”‚
       β”‚<────────────────────────────────────────────────│
       β”‚                       β”‚                         β”‚
       β”‚  5. Redirect back     β”‚                         β”‚
       β”‚     with code         β”‚                         β”‚
       │────────────────────────────────────────────────>β”‚
       β”‚                       β”‚                         β”‚
       β”‚  6. Redirect to your  β”‚                         β”‚
       β”‚     app with code     β”‚                         β”‚
       β”‚<────────────────────────────────────────────────│
       β”‚                       β”‚                         β”‚
       β”‚  7. Exchange code     β”‚                         β”‚
       β”‚     for tokens        β”‚                         β”‚
       │──────────────────────>β”‚                         β”‚
       β”‚                       β”‚  8. Exchange code       β”‚
       β”‚                       │────────────────────────>β”‚
       β”‚                       β”‚                         β”‚
       β”‚                       β”‚  9. Return tokens       β”‚
       β”‚                       β”‚<────────────────────────│
       β”‚                       β”‚                         β”‚
       β”‚  10. Receive tokens   β”‚                         β”‚
       β”‚<──────────────────────│                         β”‚
       β”‚                       β”‚                         β”‚
       β”‚  11. Use access token β”‚                         β”‚
       β”‚      for API calls    β”‚                         β”‚
       │──────────────────────>β”‚                         β”‚
       β”‚                       β”‚  12. API request with   β”‚
       β”‚                       β”‚      Bearer token       β”‚
       β”‚                       │────────────────────────>β”‚
       β”‚                       β”‚                         β”‚
       β”‚                       β”‚  13. API response      β”‚
       β”‚                       β”‚<────────────────────────│
       β”‚                       β”‚                         β”‚
       β”‚  14. Display data     β”‚                         β”‚
       β”‚<──────────────────────│                         β”‚

Manual Implementation (Reference Only)

> ⚠️ Note: The following manual implementation steps are provided for reference. We strongly recommend using a client library like oidc-client-ts v3.4.1 instead, as shown in Using Client Libraries above. Manual implementation is error-prone and requires handling many edge cases that client libraries handle automatically.

Step 1: Configure Your Application

First, set up your configuration. You’ll need:

  • Base URL: Your API server URL
  • Tenant ID: Your client_id for OAuth2
  • Redirect URI: Where users should be redirected after authentication (must match what’s registered)
// TypeScript
const tenantId: string = 'your-tenant-id-here';
const BASE_URL: string = `https://auth.agglestone.com/tenant/${tenantId}`;
const redirectUri: string = 'https://yourapp.com/callback';
// JavaScript
const tenantId = 'your-tenant-id-here';
const BASE_URL = `https://auth.agglestone.com/tenant/${tenantId}`;
const redirectUri = 'https://yourapp.com/callback';

Step 2: Generate PKCE Parameters

Before redirecting to the authorization endpoint, generate PKCE parameters:

  1. Code Verifier: A cryptographically random string (43-128 characters)
  2. Code Challenge: SHA256 hash of the code verifier, base64url encoded

Important: Store the code verifier securely (e.g., in session storage) – you’ll need it when exchanging the authorization code.

Step 3: Redirect to Authorization Endpoint

Build the authorization URL with these parameters:

  • response_type=code (authorization code flow)
  • client_id (your tenant ID)
  • redirect_uri (must match registered URI)
  • scope (e.g., openid profile email)
  • state (random string for CSRF protection)
  • code_challenge (from PKCE)
  • code_challenge_method=S256 (SHA256)
  • nonce (random string for OIDC, optional but recommended)

Example URL:

{BASE_URL}/v2.0/Auth/authorize?
  response_type=code&
  client_id=your-tenant-id&
  redirect_uri=https://yourapp.com/callback&
  scope=openid%20profile%20email&
  state=random-state-string&
  code_challenge=base64url-encoded-challenge&
  code_challenge_method=S256&
  nonce=random-nonce-string

Where {BASE_URL} is https://auth.agglestone.com/tenant/{tenantId}

Step 4: Handle the Callback

After the user authenticates, they’ll be redirected back to your redirect_uri with:

  • code: The authorization code
  • state: The state parameter you sent (validate this!)

Important:

  • Validate the state parameter matches what you sent
  • The authorization code expires in 10 minutes
  • The code can only be used once

Step 5: Exchange Code for Tokens

Make a POST request to the token endpoint:

Endpoint: POST {BASE_URL}/v2.0/Auth/token

Headers:

Content-Type: application/x-www-form-urlencoded

Body (form data):

grant_type=authorization_code
&code={authorization_code}
&redirect_uri={redirect_uri}
&client_id={tenant_id}
&code_verifier={code_verifier}

Response:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "opaque-refresh-token-string",
  "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "scope": "openid profile email"
}

Step 6: Store Tokens Securely

Store the tokens securely:

  • Access Token: Use immediately for API calls (expires in ~15 minutes)
  • Refresh Token: Store securely (e.g., httpOnly cookie) for getting new access tokens
  • ID Token: Contains user identity information

Security Best Practices:

  • Never store tokens in localStorage (XSS risk)
  • Use httpOnly cookies for refresh tokens
  • Store access tokens in memory or secure session storage
  • Implement token refresh before expiry

Step 7: Use Access Token for API Calls

Include the access token in the Authorization header:

Authorization: Bearer {access_token}

Example API Call:

// TypeScript
const BASE_URL: string = 'https://auth.agglestone.com/tenant/{tenantId}';

const response = await fetch(`${BASE_URL}/api/Users`, {
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json'
  }
});
// JavaScript
const BASE_URL = 'https://api.agglestone.com/tenant/{tenantId}';

const response = await fetch(`${BASE_URL}/api/Users`, {
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json'
  }
});

Step 8: Refresh Access Token

Before the access token expires, use the refresh token to get a new one:

Endpoint: POST {BASE_URL}/v2.0/Auth/token

Body (form data):

grant_type=refresh_token
&refresh_token={refresh_token}
&client_id={tenant_id}

Response: Same as Step 5 (new access token, new refresh token)

Important:

  • Refresh tokens are rotated (old one is invalidated)
  • Store the new refresh token
  • Refresh tokens expire after 30 days of inactivity

Complete Workflow Example (Manual Implementation)

> ⚠️ This example shows manual implementation for reference only. For production applications, we strongly recommend using oidc-client-ts v3.4.1 or another OAuth2/OIDC client library as shown in Using Client Libraries.

Here’s a complete example showing the full manual flow:

1. User Initiates Login

// TypeScript
async function initiateLogin(): Promise<void> {
  // Generate PKCE pair
  const codeVerifier: string = generateCodeVerifier();
  const codeChallenge: string = await generateCodeChallenge(codeVerifier);
  
  // Generate state for CSRF protection
  const state: string = generateRandomString();
  
  // Store in session storage
  sessionStorage.setItem('code_verifier', codeVerifier);
  sessionStorage.setItem('state', state);
  
  // Build authorization URL
  const BASE_URL: string = `https://auth.agglestone.com/tenant/${tenantId}`;
  const authUrl = new URL(`${BASE_URL}/v2.0/Auth/authorize`);
  authUrl.searchParams.set('response_type', 'code');
  authUrl.searchParams.set('client_id', tenantId);
  authUrl.searchParams.set('redirect_uri', redirectUri);
  authUrl.searchParams.set('scope', 'openid profile email');
  authUrl.searchParams.set('state', state);
  authUrl.searchParams.set('code_challenge', codeChallenge);
  authUrl.searchParams.set('code_challenge_method', 'S256');
  
  // Redirect to authorization endpoint
  window.location.href = authUrl.toString();
}
// JavaScript
async function initiateLogin() {
  // Generate PKCE pair
  const codeVerifier = generateCodeVerifier();
  const codeChallenge = await generateCodeChallenge(codeVerifier);
  
  // Generate state for CSRF protection
  const state = generateRandomString();
  
  // Store in session storage
  sessionStorage.setItem('code_verifier', codeVerifier);
  sessionStorage.setItem('state', state);
  
  // Build authorization URL
  const BASE_URL = `https://auth.agglestone.com/tenant/${tenantId}`;
  const authUrl = new URL(`${BASE_URL}/v2.0/Auth/authorize`);
  authUrl.searchParams.set('response_type', 'code');
  authUrl.searchParams.set('client_id', tenantId);
  authUrl.searchParams.set('redirect_uri', redirectUri);
  authUrl.searchParams.set('scope', 'openid profile email');
  authUrl.searchParams.set('state', state);
  authUrl.searchParams.set('code_challenge', codeChallenge);
  authUrl.searchParams.set('code_challenge_method', 'S256');
  
  // Redirect to authorization endpoint
  window.location.href = authUrl.toString();
}

2. Handle Callback

// TypeScript
async function handleCallback(): Promise<void> {
  const urlParams = new URLSearchParams(window.location.search);
  const code: string | null = urlParams.get('code');
  const state: string | null = urlParams.get('state');
  const error: string | null = urlParams.get('error');
  
  // Check for errors
  if (error) {
    console.error('Authorization error:', error);
    return;
  }
  
  // Validate state
  const storedState: string | null = sessionStorage.getItem('state');
  if (state !== storedState) {
    console.error('State mismatch - possible CSRF attack');
    return;
  }
  
  // Get stored code verifier
  const codeVerifier: string | null = sessionStorage.getItem('code_verifier');
  
  if (!code || !codeVerifier) {
    console.error('Missing code or code verifier');
    return;
  }
  
  // Exchange code for tokens
  const tokens = await exchangeCodeForTokens(code, codeVerifier);
  
  // Store tokens securely
  storeTokens(tokens);
  
  // Clean up session storage
  sessionStorage.removeItem('code_verifier');
  sessionStorage.removeItem('state');
  
  // Redirect to app
  window.location.href = '/dashboard';
}
// JavaScript
async function handleCallback() {
  const urlParams = new URLSearchParams(window.location.search);
  const code = urlParams.get('code');
  const state = urlParams.get('state');
  const error = urlParams.get('error');
  
  // Check for errors
  if (error) {
    console.error('Authorization error:', error);
    return;
  }
  
  // Validate state
  const storedState = sessionStorage.getItem('state');
  if (state !== storedState) {
    console.error('State mismatch - possible CSRF attack');
    return;
  }
  
  // Get stored code verifier
  const codeVerifier = sessionStorage.getItem('code_verifier');
  
  // Exchange code for tokens
  const tokens = await exchangeCodeForTokens(code, codeVerifier);
  
  // Store tokens securely
  storeTokens(tokens);
  
  // Clean up session storage
  sessionStorage.removeItem('code_verifier');
  sessionStorage.removeItem('state');
  
  // Redirect to app
  window.location.href = '/dashboard';
}

3. Exchange Code for Tokens

// TypeScript
interface TokenResponse {
  access_token: string;
  token_type: string;
  expires_in: number;
  refresh_token: string;
  id_token: string;
  scope: string;
}

async function exchangeCodeForTokens(
  code: string,
  codeVerifier: string
): Promise<TokenResponse> {
  const formData = new URLSearchParams();
  formData.append('grant_type', 'authorization_code');
  formData.append('code', code);
  formData.append('redirect_uri', redirectUri);
  formData.append('client_id', tenantId);
  formData.append('code_verifier', codeVerifier);
  
  const response = await fetch(`${BASE_URL}/v2.0/Auth/token`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: formData
  });
  
  if (!response.ok) {
    throw new Error('Token exchange failed');
  }
  
  return await response.json() as TokenResponse;
}
// JavaScript
async function exchangeCodeForTokens(code, codeVerifier) {
  const formData = new URLSearchParams();
  formData.append('grant_type', 'authorization_code');
  formData.append('code', code);
  formData.append('redirect_uri', redirectUri);
  formData.append('client_id', tenantId);
  formData.append('code_verifier', codeVerifier);
  
  const response = await fetch(`${BASE_URL}/v2.0/Auth/token`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: formData
  });
  
  if (!response.ok) {
    throw new Error('Token exchange failed');
  }
  
  return await response.json();
}

4. Make Authenticated API Calls

// TypeScript
async function fetchUsers(): Promise<any> {
  const accessToken: string | null = getAccessToken();
  
  if (!accessToken) {
    throw new Error('No access token available');
  }
  
  const response = await fetch(`${BASE_URL}/api/Users`, {
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json'
    }
  });
  
  if (response.status === 401) {
    // Token expired, refresh it
    await refreshAccessToken();
    return fetchUsers(); // Retry
  }
  
  return await response.json();
}
// JavaScript
async function fetchUsers() {
  const accessToken = getAccessToken();
  
  const response = await fetch(`${BASE_URL}/api/Users`, {
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json'
    }
  });
  
  if (response.status === 401) {
    // Token expired, refresh it
    await refreshAccessToken();
    return fetchUsers(); // Retry
  }
  
  return await response.json();
}

5. Refresh Token

// TypeScript
async function refreshAccessToken(): Promise<void> {
  const refreshToken: string | null = getRefreshToken();
  
  if (!refreshToken) {
    redirectToLogin();
    return;
  }
  
  const formData = new URLSearchParams();
  formData.append('grant_type', 'refresh_token');
  formData.append('refresh_token', refreshToken);
  formData.append('client_id', tenantId);
  
  const response = await fetch(`${BASE_URL}/v2.0/Auth/token`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: formData
  });
  
  if (!response.ok) {
    // Refresh token expired, user needs to login again
    redirectToLogin();
    return;
  }
  
  const tokens = await response.json() as TokenResponse;
  storeTokens(tokens);
}
// JavaScript
async function refreshAccessToken() {
  const refreshToken = getRefreshToken();
  
  const formData = new URLSearchParams();
  formData.append('grant_type', 'refresh_token');
  formData.append('refresh_token', refreshToken);
  formData.append('client_id', tenantId);
  
  const response = await fetch(`${BASE_URL}/v2.0/Auth/token`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: formData
  });
  
  if (!response.ok) {
    // Refresh token expired, user needs to login again
    redirectToLogin();
    return;
  }
  
  const tokens = await response.json();
  storeTokens(tokens);
}

Using Client Libraries

> ⭐ This is the recommended approach! Using a client library like oidc-client-ts v3.4.1 makes integration much simpler and more secure.

Client libraries handle all the client-side complexity of OAuth2/OIDC flows automatically:

  • Discovery: Automatically fetches and uses the .well-known/openid-configuration endpoint
  • PKCE: Generates code verifiers and challenges automatically
  • Token Management: Handles token storage, refresh, and expiry
  • Security: Implements CSRF protection, state validation, and secure storage
  • Error Handling: Manages edge cases and error scenarios

Installation

First, install the client library:

# TypeScript/JavaScript
npm install oidc-client-ts@^3.4.1

Complete Integration Example with oidc-client-ts v3.4.1

Here’s a complete example showing how simple integration becomes with oidc-client-ts:

// TypeScript - auth.ts
import { UserManager } from 'oidc-client-ts';

const tenantId: string = 'your-tenant-id-here';
const BASE_URL: string = `https://auth.agglestone.com/tenant/${tenantId}`;

// Configure the UserManager - it handles everything automatically!
export const userManager = new UserManager({
  authority: `${BASE_URL}/v2.0/Auth`,  // Automatically discovers endpoints via .well-known/openid-configuration
  client_id: tenantId,
  redirect_uri: `${window.location.origin}/callback`,
  response_type: 'code',
  scope: 'openid profile email',
  post_logout_redirect_uri: window.location.origin,
  automaticSilentRenew: true,  // Automatically refreshes tokens before expiry
  loadUserInfo: true  // Automatically loads user profile
});

// Login - just redirect!
export async function login(): Promise<void> {
  await userManager.signinRedirect();
}

// Handle callback - library handles PKCE, state validation, token exchange automatically
export async function handleCallback(): Promise<void> {
  const user = await userManager.signinRedirectCallback();
  console.log('User authenticated:', user.profile);
}

// Get current user and access token
export async function getCurrentUser() {
  const user = await userManager.getUser();
  if (user && !user.expired) {
    return user;
  }
  // Token expired - library will automatically refresh if possible
  return null;
}

// Make authenticated API calls
export async function fetchUsers() {
  const user = await userManager.getUser();
  if (!user || user.expired) {
    // Try silent renewal
    await userManager.signinSilent();
    return fetchUsers(); // Retry
  }

  const response = await fetch(`${BASE_URL}/api/Users`, {
    headers: {
      'Authorization': `Bearer ${user.access_token}`,
      'Content-Type': 'application/json'
    }
  });

  if (!response.ok) {
    throw new Error(`API error: ${response.status}`);
  }

  return await response.json();
}

// Logout
export async function logout(): Promise<void> {
  await userManager.signoutRedirect();
}
// JavaScript - auth.js
import { UserManager } from 'oidc-client-ts';

const tenantId = 'your-tenant-id-here';
const BASE_URL = `https://auth.agglestone.com/tenant/${tenantId}`;

// Configure the UserManager - it handles everything automatically!
export const userManager = new UserManager({
  authority: `${BASE_URL}/v2.0/Auth`,  // Automatically discovers endpoints via .well-known/openid-configuration
  client_id: tenantId,
  redirect_uri: `${window.location.origin}/callback`,
  response_type: 'code',
  scope: 'openid profile email',
  post_logout_redirect_uri: window.location.origin,
  automaticSilentRenew: true,  // Automatically refreshes tokens before expiry
  loadUserInfo: true  // Automatically loads user profile
});

// Login - just redirect!
export async function login() {
  await userManager.signinRedirect();
}

// Handle callback - library handles PKCE, state validation, token exchange automatically
export async function handleCallback() {
  const user = await userManager.signinRedirectCallback();
  console.log('User authenticated:', user.profile);
}

// Get current user and access token
export async function getCurrentUser() {
  const user = await userManager.getUser();
  if (user && !user.expired) {
    return user;
  }
  // Token expired - library will automatically refresh if possible
  return null;
}

// Make authenticated API calls
export async function fetchUsers() {
  const user = await userManager.getUser();
  if (!user || user.expired) {
    // Try silent renewal
    await userManager.signinSilent();
    return fetchUsers(); // Retry
  }

  const response = await fetch(`${BASE_URL}/api/Users`, {
    headers: {
      'Authorization': `Bearer ${user.access_token}`,
      'Content-Type': 'application/json'
    }
  });

  if (!response.ok) {
    throw new Error(`API error: ${response.status}`);
  }

  return await response.json();
}

// Logout
export async function logout() {
  await userManager.signoutRedirect();
}

Key Benefits of Using oidc-client-ts

  1. Automatic Discovery: The library automatically fetches .well-known/openid-configuration and configures all endpoints
  2. PKCE Built-in: No need to manually generate code verifiers and challenges
  3. Token Management: Automatically handles token storage, refresh, and expiry
  4. Security: Built-in CSRF protection, state validation, and secure storage
  5. Type Safety: Full TypeScript support with type definitions
  6. Error Handling: Handles edge cases and error scenarios automatically
  7. Less Code: Reduces your integration code by 80%+ compared to manual implementation

React Integration Example

For React applications, using oidc-client-ts is especially simple:

// LoginButton.tsx
import { userManager } from './auth';

export function LoginButton() {
  const handleLogin = async () => {
    await userManager.signinRedirect();
  };
  
  return <button onClick={handleLogin}>Login</button>;
}

// Callback.tsx
import { useEffect } from 'react';
import { userManager } from './auth';
import { useNavigate } from 'react-router-dom';

export function Callback() {
  const navigate = useNavigate();
  
  useEffect(() => {
    userManager.signinRedirectCallback().then(() => {
      navigate('/dashboard');
    });
  }, [navigate]);
  
  return <div>Completing login...</div>;
}

That’s it! The library handles all the complexity.

Error Handling

Common errors and how to handle them:

invalid_grant

  • Authorization code expired or already used
  • Solution: Redirect user to login again

⚠️ Important: Single-Use Tokens

Authorization codes and refresh tokens can only be used once. If you attempt to use the same token twice, you’ll receive an invalid_grant error. This is a common issue in React applications where useEffect hooks might run multiple times.

Common React Pitfalls:

  1. Strict Mode Double Execution: React’s Strict Mode (in development) intentionally runs effects twice, which can cause token exchange to happen twice:
   // ❌ Problem: Effect runs twice in Strict Mode
   useEffect(() => {
     const code = new URLSearchParams(window.location.search).get('code');
     if (code) {
       exchangeCodeForTokens(code); // Called twice = error!
     }
   }, []);
   
  1. Missing Dependency Guards: Effects without proper guards can run multiple times:
   // ❌ Problem: No guard against multiple executions
   useEffect(() => {
     handleCallback(); // Could run multiple times
   }, []);
   

Solutions:

// βœ… Solution 1: Use a ref to track if already processed
const processedRef = useRef(false);

useEffect(() => {
  if (processedRef.current) return;
  
  const code = new URLSearchParams(window.location.search).get('code');
  if (code) {
    processedRef.current = true;
    exchangeCodeForTokens(code);
  }
}, []);

// βœ… Solution 2: Remove the code from URL after processing
useEffect(() => {
  const urlParams = new URLSearchParams(window.location.search);
  const code = urlParams.get('code');
  
  if (code) {
    // Remove code from URL immediately
    urlParams.delete('code');
    window.history.replaceState({}, '', window.location.pathname);
    
    // Now safe to exchange
    exchangeCodeForTokens(code);
  }
}, []);

// βœ… Solution 3: Use a state flag
const [tokenExchanged, setTokenExchanged] = useState(false);

useEffect(() => {
  if (tokenExchanged) return;
  
  const code = new URLSearchParams(window.location.search).get('code');
  if (code) {
    setTokenExchanged(true);
    exchangeCodeForTokens(code);
  }
}, [tokenExchanged]);

For Refresh Tokens:

The same principle applies to refresh tokens – they’re rotated on each use, so ensure your refresh logic doesn’t run concurrently:

// βœ… Use a lock to prevent concurrent refresh attempts
let isRefreshing = false;

async function refreshAccessToken() {
  if (isRefreshing) {
    // Wait for existing refresh to complete
    return new Promise((resolve) => {
      const checkInterval = setInterval(() => {
        if (!isRefreshing) {
          clearInterval(checkInterval);
          resolve(getAccessToken());
        }
      }, 100);
    });
  }
  
  isRefreshing = true;
  try {
    // Refresh token logic
    const tokens = await exchangeRefreshToken();
    storeTokens(tokens);
    return tokens.access_token;
  } finally {
    isRefreshing = false;
  }
}

invalid_client

  • Invalid client_id or client authentication failed
  • Solution: Check your tenant ID configuration

Bad Request (400): Request path must follow the structure: /tenant/{tenantId}/...

  • Cause: The base URL is missing the /tenant/{tenantId} path prefix.
  • Solution:

– Ensure your base URL includes /tenant/{tenantId} (e.g., https://auth.agglestone.com/tenant/{tenantId})
– Verify your tenant ID is correct

  • Example Fix:
  // ❌ Wrong
  const BASE_URL = 'https://auth.agglestone.com';
  authority: `${BASE_URL}/v2.0/Auth`  // Missing /tenant/{tenantId}
  
  // βœ… Correct
  const BASE_URL = 'https://auth.agglestone.com/tenant/{tenantId}';
  authority: `${BASE_URL}/v2.0/Auth`  // Now includes /tenant/{tenantId}
  

invalid_request

  • Missing required parameters or invalid format
  • Solution: Validate all required parameters are present

access_denied

  • User denied authorization
  • Solution: Inform user and allow them to try again

server_error

  • Server-side error
  • Solution: Retry after a delay, login to https://portal.agglestone.com to report if persistent

Security Checklist

  • βœ… Always use HTTPS in production
  • βœ… Validate state parameter on callback
  • βœ… Use PKCE for all authorization code flows
  • βœ… Store refresh tokens in httpOnly cookies
  • βœ… Implement token refresh before expiry
  • βœ… Clear tokens on logout
  • βœ… Validate ID token signature and claims
  • βœ… Check token expiry before use
  • βœ… Never expose tokens in URLs or logs
  • βœ… Prevent duplicate token usage (especially in React with useEffect guards)

Next Steps

Understanding the Discovery Endpoint

The .well-known/openid-configuration endpoint is a standardized OpenID Connect discovery document that provides all the configuration information your client application needs to interact with our authentication server.

Location

The discovery endpoint is located at:

{baseUrl}/tenant/{tenantId}/v2.0/Auth/.well-known/openid-configuration

Example:

https://auth.agglestone.com/tenant/79199f3a-6669-4a21-ac5f-5dec93d90b57/v2.0/Auth/.well-known/openid-configuration

Why It’s Useful for Your Client Application

The discovery endpoint is extremely valuable for your client application because it:

  1. Automatically Configures Client Libraries: Most OIDC client libraries (like oidc-client-ts, @azure/msal-browser, IdentityModel.OidcClient, etc.) can automatically discover and configure themselves by fetching this endpoint. When you provide the authority URL (e.g., {BASE_URL}/v2.0/Auth), the library automatically appends /.well-known/openid-configuration and fetches the configuration, eliminating the need to manually configure each endpoint.
  1. Provides All Endpoint URLs: The discovery document contains all the endpoint URLs you need:

Authorization endpoint (authorization_endpoint) – Where to redirect users for login
Token endpoint (token_endpoint) – Where to exchange authorization codes for tokens
UserInfo endpoint (userinfo_endpoint) – Where to fetch user profile information
JWKS endpoint (jwks_uri) – Public keys for validating JWT token signatures
End session endpoint (end_session_endpoint) – Where to redirect users for logout

  1. Describes Supported Features: The document tells you what features are available:

Supported grant types (authorization_code, refresh_token, etc.)
Supported scopes (openid, profile, email, etc.)
Supported response types (code, etc.)
Supported code challenge methods (S256 for PKCE)
Token signing algorithms (RS256, etc.)

  1. Reduces Manual Configuration: Instead of manually hardcoding each endpoint URL in your application, you can let the client library fetch this document and configure itself automatically. This makes your code cleaner and more maintainable.
  1. Stays Up-to-Date Automatically: If we update our endpoints, add new features, or change supported algorithms, the discovery document reflects those changes automatically. Your application doesn’t need code changes – the client library will pick up the new configuration on the next discovery fetch.
  1. Enables Dynamic Configuration: You can build more flexible applications that adapt to different environments or tenant configurations by reading the discovery document at runtime.

Example Discovery Document Response

When you fetch the discovery endpoint, you’ll receive a JSON response like this:

{
  "issuer": "https://auth.agglestone.com/tenant/{tenantId}/v2.0/Auth",
  "authorization_endpoint": "https://auth.agglestone.com/tenant/{tenantId}/v2.0/Auth/authorize",
  "token_endpoint": "https://auth.agglestone.com/tenant/{tenantId}/v2.0/Auth/token",
  "userinfo_endpoint": "https://auth.agglestone.com/tenant/{tenantId}/v2.0/Auth/userinfo",
  "jwks_uri": "https://auth.agglestone.com/tenant/{tenantId}/v2.0/Auth/.well-known/jwks.json",
  "end_session_endpoint": "https://auth.agglestone.com/tenant/{tenantId}/v2.0/Auth/logout",
  "scopes_supported": ["openid", "profile", "email"],
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "id_token_signing_alg_values_supported": ["RS256"]
}

How Client Libraries Use It

Most client libraries automatically use the discovery endpoint when you configure the authority option. For example:

// TypeScript/JavaScript with oidc-client-ts
const userManager = new UserManager({
  authority: `${BASE_URL}/v2.0/Auth`,  // Library automatically discovers endpoints
  client_id: tenantId,
  // ... other config
});
// C# with IdentityModel.OidcClient
var baseUrl = "https://auth.agglestone.com/tenant/{tenantId}";

var options = new OidcClientOptions
{
    Authority = $"{baseUrl}/v2.0/Auth",  // Library automatically discovers endpoints
    ClientId = tenantId,
    // ... other config
};

The library will:

  1. Take your authority URL ({BASE_URL}/v2.0/Auth)
  2. Append /.well-known/openid-configuration
  3. Fetch the discovery document
  4. Extract all endpoint URLs and configuration
  5. Use that information for all OAuth2/OIDC operations

Manual Discovery (If Needed)

You can also manually fetch the discovery document if you need to inspect it or build custom integration logic:

// TypeScript/JavaScript
const response = await fetch(`${BASE_URL}/v2.0/Auth/.well-known/openid-configuration`);
const config = await response.json();
console.log('Authorization endpoint:', config.authorization_endpoint);
console.log('Token endpoint:', config.token_endpoint);
console.log('Supported scopes:', config.scopes_supported);
// C#
using System.Net.Http;
using System.Text.Json;

var baseUrl = "https://auth.agglestone.com/tenant/{tenantId}";
var httpClient = new HttpClient();
var response = await httpClient.GetAsync($"{baseUrl}/v2.0/Auth/.well-known/openid-configuration");
var json = await response.Content.ReadAsStringAsync();
var config = JsonSerializer.Deserialize<OpenIdConfiguration>(json);
Console.WriteLine($"Authorization endpoint: {config.AuthorizationEndpoint}");