Integration Guide
Welcome! 👋
This guide is written for people who do not want to guess how things work.
We assume:
- You may be new to OAuth / JWTs
- You may not know how URLs are constructed
- You want copy-paste-safe examples
- You want things explained once, clearly
No hidden magic. No “you probably know this already”.
If something matters, we’ll explain it.
—
🧭 What This Guide Covers
You will learn how to use the Agglestone Authentication & User Management Service with:
- 🌐 Frontend applications (browser / SPA)
- 🧠 Backend APIs
- 🛠 Admin tools and background services
Including:
- Login and token handling
- Making authenticated API calls
- Handling expired tokens (401 responses)
- Managing users and groups
- Understanding how URLs are built (explicitly!)
—
🧱 Key Concepts
Before touching code, let’s align on a few core ideas.
1️⃣ Everything belongs to a tenant
Agglestone is multi-tenant, and so every request must say which tenant it belongs to.
That is done by putting /tenant/{tenantId} into the URL.
When calls are made to a secure endpoint, we double check that tokens or Keys belong to the same tenant.
—
2️⃣ There are two ways to authenticate
| Method | Used by | Typical use |
|---|---|---|
| JWT Token (OAuth / OIDC) | End users | Frontend apps |
| API Key | Systems | Admin tools, scripts, services |
They are used differently and have different rules.
—
3️⃣ URLs are not guessed — they are constructed
Every request follows this exact structure:
https://{service}.agglestone.com/tenant/{tenantId}/{api-endpoint}
Example:
https://auth.agglestone.com/tenant/3f40cd7a-16d1-414a-b15d-b97158d31de9/api/Users
If /tenant/{tenantId} is missing, the request will fail.
—
🔑 Prerequisites
You will need:
- Tenant ID
(Login to the Agglestone Portal to find your Tenant Id)
- Service Base
https://auth.agglestone.com
From these, all URLs are built.
—
🔍 Important URLs for the Authentication and User Management Service
We will refer to these often:
| Purpose | Full URL pattern |
|---|---|
| Tenant base | https://auth.agglestone.com/tenant/{tenantId} |
| OAuth authority | https://auth.agglestone.com/tenant/{tenantId}/v2.0/Auth |
| Discovery document | https://auth.agglestone.com/tenant/{tenantId}/v2.0/Auth/.well-known/openid-configuration |
| Users API | https://auth.agglestone.com/tenant/{tenantId}/api/Users |
| Groups API | https://auth.agglestone.com/tenant/{tenantId}/api/Groups |
—
⭐ Strong Recommendation: Use a Client Library
Authentication is hard and the implications of getting it wrong are pretty big.
Using the managed service from Agglestone, together with a client library can help prevent mistakes.
For browser applications we recommend:
JavaScript / TypeScript
npm install oidc-client-ts
This library:
- Generates PKCE automatically
- Uses discovery documents
- Stores tokens safely
- Refreshes tokens automatically
You should not try to implement OAuth without a client library unless you must.
—
🌐 Frontend Authentication (Browser / SPA)
Step 1: Configure the client
import { UserManager, WebStorageStateStore, User } from 'oidc-client-ts';
const tenantId = 'your-tenant-id';
export const userManager = new UserManager({
authority: 'https://auth.agglestone.com/tenant/' + tenantId + '/v2.0/Auth',
client_id: tenantId,
redirect_uri: window.location.origin + '/callback',
post_logout_redirect_uri: window.location.origin,
response_type: 'code',
scope: 'openid profile email',
automaticSilentRenew: true,
userStore: new WebStorageStateStore({ store: window.localStorage }) // persist tokens in localStorage
});
🔍 What this does:
- Tells the library where your tenant lives
- Enables automatic token refresh
- Uses standard OpenID Connect settings
Step 2: Start login
export async function login() {
await userManager.signinRedirect();
}
Call this function from a login button on your frontend.
Step 3: Handle the callback
export async function handleCallback() {
const user = await userManager.signinRedirectCallback();
console.log('Logged in user:', user.profile);
}
At this point:
- The user is authenticated
- The access token (JWT) and refresh tokens are stored in local storage
🔐 Calling an API Endpoint from the Frontend
The following code…
- Get the current user
- Check if the token is expired
- Refresh if needed
- Call the API endpoint with
Authorization: Bearer
export async function getUsers() {
let user = await userManager.getUser();
if (!user || user.expired) {
await userManager.signinSilent();
user = await userManager.getUser();
}
const response = await fetch(
'https://auth.agglestone.com/tenant/' + tenantId + '/api/Users',
{
headers: {
Authorization: 'Bearer ' + user.access_token,
'Content-Type': 'application/json'
}
}
);
if (!response.ok) {
throw new Error('API error: ' + response.status);
}
return response.json();
}
What Happens if you Get a 401?
A 401 Unauthorized usually means either:
- The token expired
- The token was revoked
- The user logged out elsewhere
What to do:
- Try to refresh the token
- Retry the request
- If refresh fails, send the user to login
async function refreshTokenManually() {
const refreshToken = getStoredRefreshToken();
if (!refreshToken) {
redirectToLogin();
return;
}
const body = new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: tenantId
});
const response = await fetch(
`${authority}/token`,
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body
}
);
if (!response.ok) {
redirectToLogin();
return;
}
const tokens = await response.json();
storeTokens(tokens);
}
—
🔐 Calling an API Endpoint from the Backend using API Keys
API Keys should only be used when making a call from one of your backend services to an Agglestone service. They should not be exposed in any frontend code.
If you accidentally expose your API Key, immediately go to the Agglestone Portal and generate new API Keys.
When sending the message, the API Key should go in the X-API-Key header parameter. Below are some examples of getting a list of users within your tenant.
—
// For use in Node.JS backend code
class ApiKeyClient {
constructor(host, tenantId, apiKey) {
this.tenantBaseUrl = `${host}/tenant/${tenantId}`;
this.apiKey = apiKey;
}
async getUsers() {
const url = `${this.tenantBaseUrl}/api/Users`;
return fetch(url, {
headers: { 'X-API-Key': this.apiKey }
});
}
}
// For use in C# backend code
public class ApiKeyClient
{
private readonly string _tenantBaseUrl;
private readonly HttpClient _httpClient;
public ApiKeyClient(string host, string tenantId, string apiKey)
{
_tenantBaseUrl = $"{host}/tenant/{tenantId}";
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Add("X-API-Key", apiKey);
}
public async Task<HttpResponseMessage> GetAsync(string endpoint)
{
var url = $"{_tenantBaseUrl}{endpoint}";
return await _httpClient.GetAsync(url);
}
}
# For use in Python backend code
class ApiKeyClient:
def __init__(self, host, tenant_id, api_key):
self.tenant_base_url = f"{host}/tenant/{tenant_id}"
self.headers = {
"X-API-Key": api_key,
"Content-Type": "application/json"
}
def get(self, endpoint):
url = f"{self.tenant_base_url}{endpoint}"
return requests.get(url, headers=self.headers)
—
Backend: Validating JWTs
If you have your own backend code, you can use the JWTs generated by the Agglestone Auth Service to verify access rights.
Backends must never trust tokens blindly.
Your backend must:
-Verify the signature
-Verify issuer
-Verify audience
-Verify expiration
Just like with the frontend, we recommend using a recognised library.
// Node.js example using jsonwebtoken and jwks-rsa
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const BASE_URL = 'https://auth.agglestone.com/tenant/{tenantId}';
const tenantId = 'your-tenant-id';
// Create JWKS client
const client = jwksClient({
jwksUri: `${BASE_URL}/.well-known/jwks.json`,
cache: true,
cacheMaxAge: 3600000, // 1 hour
rateLimit: true,
jwksRequestsPerMinute: 10
});
// Get signing key from JWKS
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) {
return callback(err);
}
const signingKey = key.getPublicKey();
callback(null, signingKey);
});
}
// Validate JWT token
function validateToken(token) {
return new Promise((resolve, reject) => {
const options = {
audience: tenantId, // Expected audience (tenant ID)
issuer: `${BASE_URL}/v2.0/Auth`, // Expected issuer
algorithms: ['RS256'] // Only allow RS256
};
jwt.verify(token, getKey, options, (err, decoded) => {
if (err) {
reject(new Error(`Token validation failed: ${err.message}`));
} else {
resolve(decoded);
}
});
});
}
// Express middleware example
function validateTokenMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or invalid authorization header' });
}
const token = authHeader.substring(7); // Remove 'Bearer ' prefix
validateToken(token)
.then(decoded => {
req.user = decoded; // Attach decoded token to request
next();
})
.catch(err => {
res.status(401).json({ error: 'Invalid token', details: err.message });
});
}
// C# example using Microsoft.IdentityModel.Tokens
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using System.Security.Claims;
public class JwtValidationService
{
private readonly string _baseUrl;
private readonly string _tenantId;
private readonly ConfigurationManager<OpenIdConnectConfiguration> _configurationManager;
public JwtValidationService(string baseUrl, string tenantId)
{
_baseUrl = $"{baseUrl}/tenant/{tenantId}";
_tenantId = tenantId;
// Create configuration manager for JWKS endpoint
_configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
$"{_baseUrl}/v2.0/Auth/.well-known/openid-configuration",
new OpenIdConnectConfigurationRetriever(),
new HttpDocumentRetriever()
);
}
public async Task<ClaimsPrincipal> ValidateTokenAsync(string token)
{
// Get configuration (includes signing keys from JWKS)
var configuration = await _configurationManager.GetConfigurationAsync(CancellationToken.None);
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKeys = configuration.SigningKeys,
ValidateIssuer = true,
ValidIssuer = $"{_baseUrl}/v2.0/Auth",
ValidateAudience = true,
ValidAudience = _tenantId,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero // No clock skew tolerance
};
// Validate token
var principal = tokenHandler.ValidateToken(token, validationParameters, out SecurityToken validatedToken);
return principal;
}
}
// ASP.NET Core middleware usage
public class JwtValidationMiddleware
{
private readonly RequestDelegate _next;
private readonly JwtValidationService _jwtValidation;
public JwtValidationMiddleware(RequestDelegate next, JwtValidationService jwtValidation)
{
_next = next;
_jwtValidation = jwtValidation;
}
public async Task InvokeAsync(HttpContext context)
{
var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer "))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Missing or invalid authorization header");
return;
}
var token = authHeader.Substring(7); // Remove "Bearer " prefix
try
{
var principal = await _jwtValidation.ValidateTokenAsync(token);
context.User = principal; // Set validated user
await _next(context);
}
catch (Exception ex)
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync($"Token validation failed: {ex.Message}");
}
}
}
# Python example using PyJWT and cryptography
import jwt
from jwt import PyJWKClient
from functools import wraps
from flask import Flask, request, jsonify
BASE_URL = 'https://auth.agglestone.com/tenant/{tenantId}'
TENANT_ID = 'your-tenant-id'
# Create JWKS client
jwks_client = PyJWKClient(f'{BASE_URL}/.well-known/jwks.json')
def validate_token(token):
"""Validate JWT token and return decoded claims"""
try:
# Get signing key from JWKS
signing_key = jwks_client.get_signing_key_from_jwt(token)
# Decode and validate token
decoded_token = jwt.decode(
token,
signing_key.key,
algorithms=['RS256'],
audience=TENANT_ID,
issuer=f'{BASE_URL}/v2.0/Auth',
options={
'verify_signature': True,
'verify_exp': True,
'verify_aud': True,
'verify_iss': True
}
)
return decoded_token
except jwt.ExpiredSignatureError:
raise ValueError('Token has expired')
except jwt.InvalidAudienceError:
raise ValueError('Invalid token audience')
except jwt.InvalidIssuerError:
raise ValueError('Invalid token issuer')
except jwt.InvalidSignatureError:
raise ValueError('Invalid token signature')
except Exception as e:
raise ValueError(f'Token validation failed: {str(e)}')
# Flask middleware/decorator
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({'error': 'Missing or invalid authorization header'}), 401
token = auth_header[7:] # Remove 'Bearer ' prefix
try:
claims = validate_token(token)
# Attach claims to request context
request.user = claims
except ValueError as e:
return jsonify({'error': str(e)}), 401
return f(*args, **kwargs)
return decorated
🎯 Final Notes
- Build URLs once
- Validate everything
- Use libraries
- Avoid guessing
Auth should feel boring and predictable — if its not, then you may have done something wrong 😄