Quick Start

Last updated: December 2025

Get authentication working in your web application in minutes. This guide shows you the fastest path to implementation.

What You Need

Before you start, make sure you have:

  • Tenant ID – Your OAuth2 client ID (login to https://portal.agglestone.com to view your Tenant Id)
  • Base URLhttps://auth.agglestone.com/tenant/{tenantId}. Note: Your actual server URL may differ – check your tenant portal for the correct URL.
  • Redirect URI – Where users return to, in your application, after login (e.g., https://yourapp.com/callback)
  • Client Libraries – It is recommended that you use a compliant OAuth2/OIDC client library (see OAuth2 and OIDC Overview for recommendations)

⚠️ 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

Example

If your:

  • Server URL: https://auth.agglestone.com
  • Tenant ID: your-tenant-id-here

Then your:

  • Base URL: https://auth.agglestone.com/tenant/{your-tenant-id-here}
  • OAuth Authority: https://auth.agglestone.com/tenant/{your-tenant-id-here}/v2.0/Auth
  • Discovery Endpoint: https://auth.agglestone.com/tenant/{your-tenant-id-here}/v2.0/Auth/.well-known/openid-configuration
  • API Endpoints: https://auth.agglestone.com/tenant/{your-tenant-id-here}/api/Users

💡 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 quick start guide.

Implementation Steps

Step 1: Install the Client Library

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

# C# / .NET
dotnet add package IdentityModel.OidcClient

# Python
pip install authlib

Step 2: Configure the OAuth2 Client

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

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

const userManager = new UserManager({
  authority: `${BASE_URL}/v2.0/Auth`,
  client_id: '{your-tenant-id}',
  redirect_uri: 'https://yourapp.com/callback',
  response_type: 'code',
  scope: 'openid profile email',
  automaticSilentRenew: true
});
// JavaScript
import { UserManager } from 'oidc-client-ts';

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

const userManager = new UserManager({
  authority: `${BASE_URL}/v2.0/Auth`,
  client_id: '{your-tenant-id}',
  redirect_uri: 'https://yourapp.com/callback',
  response_type: 'code',
  scope: 'openid profile email',
  automaticSilentRenew: true
});
// C# / .NET
using IdentityModel.OidcClient;

var baseUrl = "https://api.agglestone.com/tenant/{your-tenant-id}";

var options = new OidcClientOptions
{
    Authority = $"{baseUrl}/api/Auth",
    ClientId = "{your-tenant-id}",
    RedirectUri = "https://yourapp.com/callback",
    Scope = "openid profile email"
};

var client = new OidcClient(options);
# Python
from authlib.integrations.requests_client import OAuth2Session

client = OAuth2Session(
    client_id='{your-tenant-id}',
    redirect_uri='https://yourapp.com/callback',
    scope='openid profile email'
)

Step 3: Initiate Login

// TypeScript
// Redirect user to login
await userManager.signinRedirect();
// JavaScript
// Redirect user to login
await userManager.signinRedirect();
// C# / .NET
// Login (opens browser for authentication)
var result = await client.LoginAsync();
var accessToken = result.AccessToken;
# Python
# Get authorization URL and redirect user
auth_url, state = client.authorization_url(
    'https://auth.agglestone.com/tenant/{your-tenant-id}/v2.0/Auth/authorize'
)
# Redirect user to auth_url
print(f"Visit: {auth_url}")

Step 4: Handle the Callback

// TypeScript
// On your callback page (e.g., /callback)
const user = await userManager.signinRedirectCallback();
const accessToken = user.access_token;
// JavaScript
// On your callback page (e.g., /callback)
const user = await userManager.signinRedirectCallback();
const accessToken = user.access_token;
// C# / .NET
// The LoginAsync() method handles the callback automatically
// Access token is available in result.AccessToken
# Python
# After user authenticates and is redirected back
authorization_response = input("Enter full redirect URL: ")
token = client.fetch_token(
    'https://auth.agglestone.com/tenant/{your-tenant-id}/v2.0/Auth/token',
    authorization_response=authorization_response
)
access_token = token['access_token']

Step 5: Make Authenticated API Calls

// TypeScript
const user = await userManager.getUser();
  const response = await fetch(
    `${BASE_URL}/api/Users`,
  {
    headers: {
      'Authorization': `Bearer ${user.access_token}`,
      'Content-Type': 'application/json'
    }
  }
);
const data = await response.json();
// JavaScript
const user = await userManager.getUser();
  const response = await fetch(
    `${BASE_URL}/api/Users`,
  {
    headers: {
      'Authorization': `Bearer ${user.access_token}`,
      'Content-Type': 'application/json'
    }
  }
);
const data = await response.json();
// C# / .NET
using System.Net.Http;

var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = 
    new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);

var response = await httpClient.GetAsync(
    "https://auth.agglestone.com/tenant/{tenantId}/api/Users"
);
var data = await response.Content.ReadFromJsonAsync<UserResponse>();
# Python
import requests

headers = {
    'Authorization': f'Bearer {access_token}',
    'Content-Type': 'application/json'
}

response = requests.get(
    'https://api.agglestone.com/tenant/{tenantId}/api/Users',
    headers=headers
)
data = response.json()

Common Patterns

Check if User is Logged In

// TypeScript
const user = await userManager.getUser();
if (user && !user.expired) {
  // User is authenticated
  console.log('User:', user.profile);
} else {
  // User needs to login
  await userManager.signinRedirect();
}
// JavaScript
const user = await userManager.getUser();
if (user && !user.expired) {
  // User is authenticated
  console.log('User:', user.profile);
} else {
  // User needs to login
  await userManager.signinRedirect();
}
// C# / .NET
var user = await client.GetUserAsync();
if (user != null && !user.IsExpired)
{
    // User is authenticated
    Console.WriteLine($"User: {user.Profile}");
}
else
{
    // User needs to login
    await client.LoginAsync();
}
# Python
user = client.get_user()
if user and not user.is_expired:
    # User is authenticated
    print(f"User: {user.profile}")
else:
    # User needs to login
    auth_url = client.get_authorization_url()
    # Redirect to auth_url

Make Authenticated API Calls

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

async function apiCall(endpoint: string) {
  const user = await userManager.getUser();
  if (!user || user.expired) {
    await userManager.signinSilent();
    return apiCall(endpoint); // Retry
  }
  
  const response = await fetch(
    `${BASE_URL}${endpoint}`,
    {
      headers: {
        'Authorization': `Bearer ${user.access_token}`
      }
    }
  );
  
  return response.json();
}
// JavaScript
const BASE_URL = 'https://auth.agglestone.com/tenant/{tenantId}';

async function apiCall(endpoint) {
  const user = await userManager.getUser();
  if (!user || user.expired) {
    await userManager.signinSilent();
    return apiCall(endpoint); // Retry
  }
  
  const response = await fetch(
    `${BASE_URL}${endpoint}`,
    {
      headers: {
        'Authorization': `Bearer ${user.access_token}`
      }
    }
  );
  
  return response.json();
}
// C# / .NET
var baseUrl = "https://api.agglestone.com/tenant/{tenantId}";

public async Task<T> ApiCallAsync<T>(string endpoint)
{
    var user = await client.GetUserAsync();
    if (user == null || user.IsExpired)
    {
        await client.SigninSilentAsync();
        return await ApiCallAsync<T>(endpoint); // Retry
    }
    
    var httpClient = new HttpClient();
    httpClient.DefaultRequestHeaders.Authorization = 
        new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", user.AccessToken);
    
    var response = await httpClient.GetAsync(
        $"{baseUrl}{endpoint}"
    );
    
    return await response.Content.ReadFromJsonAsync<T>();
}
# Python
BASE_URL = 'https://api.agglestone.com/tenant/{tenant_id}'

async def api_call(endpoint):
    user = client.get_user()
    if not user or user.is_expired:
        await client.signin_silent()
        return await api_call(endpoint)  # Retry
    
    headers = {
        'Authorization': f'Bearer {user.access_token}'
    }
    
    response = requests.get(
        f'{BASE_URL}{endpoint}',
        headers=headers
    )
    
    return response.json()

Logout

// TypeScript
await userManager.signoutRedirect();
// JavaScript
await userManager.signoutRedirect();
// C# / .NET
await client.LogoutAsync();
# Python
client.logout()

Next Steps

Troubleshooting

“Invalid redirect_uri”

  • Make sure your redirect URI exactly matches what’s registered for your tenant
  • Check for trailing slashes and protocol (http vs https)

“Token expired”

  • Enable automaticSilentRenew: true in your UserManager config
  • Or manually refresh tokens before they expire

“CORS errors”

  • Ensure your redirect URI is properly registered
  • Check that you’re using the correct tenant ID

“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}
  

Need more help? Check the detailed guides or login to https://portal.agglestone.com.

Understanding the Discovery Endpoint

The .well-known/openid-configuration endpoint is a standardized OIDC 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}/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

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

  1. Automatically Configures Client Libraries: Most OIDC client libraries (like oidc-client-ts, @azure/msal-browser, etc.) can automatically discover and configure themselves by fetching this endpoint. You only need to provide the authority URL, and the library handles the rest.
  1. Provides All Endpoint URLs: The discovery document contains all the endpoint URLs you need:

– Authorization endpoint (authorization_endpoint)
– Token endpoint (token_endpoint)
– UserInfo endpoint (userinfo_endpoint)
– JWKS endpoint (jwks_uri) for token validation
– End session endpoint (end_session_endpoint) for logout

  1. Describes Supported Features: The document tells you:

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

  1. Reduces Manual Configuration: Instead of manually configuring each endpoint URL, you can let the client library fetch this document and configure itself automatically.
  1. Stays Up-to-Date: If we update our endpoints or add new features, the discovery document reflects those changes automatically, so your application doesn’t need code changes.

Example 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"]
}

Using the Discovery Endpoint

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

// The library automatically appends /.well-known/openid-configuration
// to the authority URL and fetches the configuration
const userManager = new UserManager({
  authority: `${BASE_URL}/v2.0/Auth`,  // Library will discover endpoints automatically
  client_id: '{your-tenant-id}',
  // ... other config
});

You can also manually fetch the discovery document if needed:

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