Integration Guide
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 URL –
https://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.OidcClientorMicrosoft.AspNetCore.Authentication.OpenIdConnect - Python:
authliborrequests-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_idfor 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:
- Code Verifier: A cryptographically random string (43-128 characters)
- 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 codestate: The state parameter you sent (validate this!)
Important:
- Validate the
stateparameter 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-configurationendpoint - 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
- Automatic Discovery: The library automatically fetches
.well-known/openid-configurationand configures all endpoints - PKCE Built-in: No need to manually generate code verifiers and challenges
- Token Management: Automatically handles token storage, refresh, and expiry
- Security: Built-in CSRF protection, state validation, and secure storage
- Type Safety: Full TypeScript support with type definitions
- Error Handling: Handles edge cases and error scenarios automatically
- 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:
- 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!
}
}, []);
- 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_idor 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
stateparameter 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
useEffectguards)
Next Steps
- Check out Code Examples for complete implementations
- Learn about User and Group Management
- Understand API Keys vs JWT Tokens
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:
- 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-configurationand fetches the configuration, eliminating the need to manually configure each endpoint.
- 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
- 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.)
- 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.
- 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.
- 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:
- Take your authority URL (
{BASE_URL}/v2.0/Auth) - Append
/.well-known/openid-configuration - Fetch the discovery document
- Extract all endpoint URLs and configuration
- 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}");