Authonomy as Universal Broker
Complete implementation guide for using Authonomy as your universal identity broker
Authonomy as Universal Broker
Use Authonomy as your central identity broker to handle authentication for all customer IDPs. Your application only needs to integrate with Authonomy, and Authonomy handles all the complexity of routing users to their respective identity providers.
Architecture Overview
graph TB A[Your Application] --> B[Authonomy SAML SP] B --> C[Customer A - Okta] B --> D[Customer B - Azure AD] B --> E[Customer C - Google Workspace] B --> F[Customer D - Generic SAML] subgraph "Authonomy" B --> G[Authentication Routing] G --> H[Login Methods] H --> I[OAuth/OIDC Connectors] end
How it works:
- Your application sends users to Authonomy for authentication
- Authonomy’s routing engine determines which customer IDP to use
- User authenticates with their organization’s IDP
- Authonomy returns standardized user data to your application
Implementation Steps
Step 1: Configure Your Application as SAML Service Provider
Your application acts as a SAML Service Provider (SP) that sends authentication requests to Authonomy (acting as the Identity Provider).
Register SAML Service Provider in Authonomy
# Create SAML Service Provider configuration
curl -X POST "https://your-authonomy.com/api/tenants/{tenant_id}/applications/{app_id}/saml-service-providers" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "My Application",
"description": "Main application SAML integration",
"entity_id": "https://myapp.com/saml/metadata",
"acs_url": "https://myapp.com/saml/acs",
"sls_url": "https://myapp.com/saml/sls",
"name_id_format": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
"assertion_signed": true,
"response_signed": false,
"want_signed_authn_requests": false,
"enabled": true
}'
SAML Integration in Your Application
Node.js with Express and passport-saml:
const express = require('express');
const passport = require('passport');
const SamlStrategy = require('passport-saml').Strategy;
const app = express();
// Configure SAML strategy
passport.use(new SamlStrategy({
entryPoint: 'https://your-authonomy.com/saml/{tenant_id}/sso',
issuer: 'https://myapp.com/saml/metadata',
callbackUrl: 'https://myapp.com/saml/acs',
cert: process.env.AUTHONOMY_SAML_CERT, // Get from Authonomy metadata
signatureAlgorithm: 'sha256'
}, (profile, done) => {
// Process the user profile
const user = {
id: profile.nameID,
email: profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'],
name: profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'],
groups: profile['http://schemas.microsoft.com/ws/2008/06/identity/claims/groups'] || []
};
return done(null, user);
}));
// Authentication routes
app.get('/auth/login', passport.authenticate('saml'));
app.post('/saml/acs',
passport.authenticate('saml', { failureRedirect: '/login' }),
(req, res) => {
// Authentication successful
res.redirect('/dashboard');
}
);
Python with Flask and python3-saml:
from flask import Flask, request, redirect, session, url_for
from onelogin.saml2.auth import OneLogin_Saml2_Auth
from onelogin.saml2.settings import OneLogin_Saml2_Settings
from onelogin.saml2.utils import OneLogin_Saml2_Utils
import os
app = Flask(__name__)
app.secret_key = os.environ.get('SECRET_KEY')
# SAML Settings
def init_saml_auth(req):
settings = {
"sp": {
"entityId": "https://myapp.com/saml/metadata",
"assertionConsumerService": {
"url": "https://myapp.com/saml/acs",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
},
"NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
},
"idp": {
"entityId": "https://your-authonomy.com/saml/{tenant_id}/metadata",
"singleSignOnService": {
"url": "https://your-authonomy.com/saml/{tenant_id}/sso",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
"x509cert": os.environ.get('AUTHONOMY_SAML_CERT')
}
}
return OneLogin_Saml2_Auth(req, settings)
@app.route('/auth/login')
def saml_login():
req = prepare_flask_request(request)
auth = init_saml_auth(req)
return redirect(auth.login())
@app.route('/saml/acs', methods=['POST'])
def saml_acs():
req = prepare_flask_request(request)
auth = init_saml_auth(req)
auth.process_response()
if auth.is_authenticated():
# Store user data in session
session['user'] = {
'id': auth.get_nameid(),
'email': auth.get_attribute('http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress')[0],
'name': auth.get_attribute('http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name')[0],
'groups': auth.get_attribute('http://schemas.microsoft.com/ws/2008/06/identity/claims/groups') or []
}
return redirect('/dashboard')
else:
return redirect('/login?error=auth_failed')
Step 2: Configure Customer Identity Providers
Set up login methods for each customer’s identity provider.
Create OIDC Provider Configuration
# Configure Okta as OIDC Provider
curl -X POST "https://your-authonomy.com/api/tenants/{tenant_id}/oidc-providers" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corp Okta",
"issuer": "https://acme-corp.okta.com",
"client_id": "customer_provided_client_id",
"client_secret": "customer_provided_client_secret",
"scopes": ["openid", "profile", "email", "groups"],
"enabled": true
}'
Create Login Method
# Create login method linking to the OIDC provider
curl -X POST "https://your-authonomy.com/api/tenants/{tenant_id}/login-methods" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"display_name": "Acme Corp",
"oidc_provider_id": "{oidc_provider_id}",
"enabled": true
}'
Step 3: Set Up Authentication Routing
Configure intelligent routing to automatically direct users to the correct IDP.
Create Authentication Routing Configuration
# Create routing configuration for your application
curl -X POST "https://your-authonomy.com/api/tenants/{tenant_id}/applications/{app_id}/authentication-routing" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Customer IDP Routing",
"description": "Route users to their organization IDP",
"routing_type": "simple",
"enabled": true,
"fallback_action": "show_selection"
}'
Create Routing Rules
# Rule 1: Route based on email domain
curl -X POST "https://your-authonomy.com/api/tenants/{tenant_id}/authentication-routing/{routing_id}/rules" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corp Email Domain",
"description": "Route @acme.com emails to Acme Corp Okta",
"priority": 1,
"enabled": true,
"match_type": "request_parameter",
"match_field": "subject",
"match_operator": "contains",
"match_value": "@acme.com",
"action": "redirect_to_idp",
"login_method_id": "{acme_login_method_id}"
}'
# Rule 2: Route based on RelayState parameter
curl -X POST "https://your-authonomy.com/api/tenants/{tenant_id}/authentication-routing/{routing_id}/rules" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Customer ID in RelayState",
"description": "Route based on customer identifier in RelayState",
"priority": 2,
"enabled": true,
"match_type": "request_parameter",
"match_field": "RelayState",
"match_operator": "starts_with",
"match_value": "customer=contoso",
"action": "redirect_to_idp",
"login_method_id": "{contoso_login_method_id}"
}'
Advanced JavaScript-Based Routing
For complex routing logic, use JavaScript-based routing:
# Create JavaScript-based routing
curl -X PUT "https://your-authonomy.com/api/tenants/{tenant_id}/authentication-routing/{routing_id}" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"routing_type": "javascript",
"javascript_script": "
// Extract customer info from various sources
function route(context) {
const relayState = context.relayState || '';
const acsUrl = context.samlRequest?.assertionConsumerServiceURL || '';
// Route based on subdomain in ACS URL
const subdomain = extractSubdomain(acsUrl);
if (subdomain) {
const loginMethod = findLoginMethodByCustomer(subdomain);
if (loginMethod) {
return {
action: 'redirect_to_idp',
loginMethodId: loginMethod.id,
reason: 'Routed based on subdomain: ' + subdomain
};
}
}
// Route based on RelayState parameter
const customerMatch = relayState.match(/customer=([^&]+)/);
if (customerMatch) {
const customerId = customerMatch[1];
const loginMethod = findLoginMethodByCustomer(customerId);
if (loginMethod) {
return {
action: 'redirect_to_idp',
loginMethodId: loginMethod.id,
reason: 'Routed based on customer ID: ' + customerId
};
}
}
// Default to showing selection page
return {
action: 'show_selection',
reason: 'No routing rule matched'
};
}
function extractSubdomain(url) {
try {
const hostname = new URL(url).hostname;
const parts = hostname.split('.');
return parts.length > 2 ? parts[0] : null;
} catch {
return null;
}
}
function findLoginMethodByCustomer(customerId) {
// This would typically lookup against configured login methods
// For demo purposes, using static mapping
const mapping = {
'acme': context.loginMethods.find(lm => lm.displayName.includes('Acme')),
'contoso': context.loginMethods.find(lm => lm.displayName.includes('Contoso')),
'globex': context.loginMethods.find(lm => lm.displayName.includes('Globex'))
};
return mapping[customerId];
}
// Execute routing logic
route(context);
"
}'
Customer Self-Service Configuration
Enable your customers to configure their own identity providers through embedded wizards.
Embed Configuration UI
// Redirect customer to IDP setup wizard
app.get('/setup-idp/:customerId', (req, res) => {
const { customerId } = req.params;
const setupUrl = `https://your-authonomy.com/setup/idp?customer=${customerId}&return_to=${encodeURIComponent(req.get('Referer'))}`;
res.redirect(setupUrl);
});
// Handle completion callback
app.get('/idp-setup-complete', (req, res) => {
const { customer, status } = req.query;
if (status === 'success') {
res.render('idp-setup-success', { customer });
} else {
res.render('idp-setup-error', { customer, error: req.query.error });
}
});
Webhook for Configuration Updates
// Handle webhook when customer configures their IDP
app.post('/webhooks/authonomy/idp-configured', (req, res) => {
const { customer_id, idp_type, status } = req.body;
if (status === 'active') {
// Update your internal records
await db.customers.update(customer_id, {
has_idp_configured: true,
idp_type: idp_type,
idp_configured_at: new Date()
});
// Optionally notify customer success team
await notifyCustomerSuccess({
customerId: customer_id,
message: `Customer ${customer_id} successfully configured ${idp_type} IDP`
});
}
res.status(200).json({ received: true });
});
Advanced Features
Just-In-Time User Provisioning
Automatically create users in your system during their first login:
// Enhanced SAML ACS handler with JIT provisioning
app.post('/saml/acs',
passport.authenticate('saml', { failureRedirect: '/login' }),
async (req, res) => {
const samlUser = req.user;
// Check if user exists in your system
let appUser = await User.findOne({ email: samlUser.email });
if (!appUser) {
// Create new user with IDP attributes
appUser = await User.create({
email: samlUser.email,
name: samlUser.name,
external_id: samlUser.id,
groups: samlUser.groups,
created_via: 'sso',
first_login: new Date()
});
console.log(`Created new user via JIT provisioning: ${appUser.email}`);
} else {
// Update existing user's groups/attributes
await appUser.update({
groups: samlUser.groups,
last_login: new Date()
});
}
// Set application session
req.session.userId = appUser.id;
res.redirect('/dashboard');
}
);
Group/Role Mapping
Map IDP groups to application roles:
# Configure SAML attribute mapping
curl -X PUT "https://your-authonomy.com/api/tenants/{tenant_id}/applications/{app_id}/saml-service-providers/{sp_id}/attribute-mapping" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"attribute_mappings": {
"email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
"name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
"groups": "http://schemas.microsoft.com/ws/2008/06/identity/claims/groups"
},
"group_mappings": {
"Okta_Admins": "admin",
"Okta_Users": "user",
"Okta_Billing": "billing",
"Everyone": "basic"
}
}'
Error Handling
Implement comprehensive error handling:
app.post('/saml/acs', (req, res) => {
passport.authenticate('saml', (err, user, info) => {
if (err) {
console.error('SAML authentication error:', err);
// Handle specific error types
if (err.message.includes('IDP_NOT_CONFIGURED')) {
return res.redirect('/setup-idp?error=not_configured');
} else if (err.message.includes('INVALID_RESPONSE')) {
return res.redirect('/login?error=invalid_response');
}
return res.redirect('/login?error=auth_failed');
}
if (!user) {
console.warn('SAML authentication failed:', info);
return res.redirect('/login?error=auth_failed');
}
// Authentication successful
req.login(user, (loginErr) => {
if (loginErr) {
console.error('Login session error:', loginErr);
return res.redirect('/login?error=session_failed');
}
res.redirect('/dashboard');
});
})(req, res);
});
Testing Your Integration
Development Environment Setup
// Create test customers for different IDP types
const testCustomers = [
{ id: 'test-okta', name: 'Test Okta Customer', idpType: 'okta' },
{ id: 'test-azure', name: 'Test Azure Customer', idpType: 'azure-ad' },
{ id: 'test-google', name: 'Test Google Customer', idpType: 'google-workspace' }
];
// Test routing for each customer
app.get('/test/routing', async (req, res) => {
const results = [];
for (const customer of testCustomers) {
const testUrl = `https://your-authonomy.com/saml/{tenant_id}/sso?RelayState=customer=${customer.id}`;
results.push({
customer: customer.name,
idpType: customer.idpType,
testUrl: testUrl
});
}
res.json({ testCustomers: results });
});
Integration Testing
#!/bin/bash
# Test SAML metadata endpoint
echo "Testing SAML metadata..."
curl -s "https://your-authonomy.com/saml/{tenant_id}/metadata" | xmllint --format -
# Test different routing scenarios
echo "Testing routing with RelayState..."
curl -s -I "https://your-authonomy.com/saml/{tenant_id}/sso?RelayState=customer=acme"
echo "Testing routing with email domain..."
# This would typically be done through actual SAML request with subject
Monitoring and Analytics
Track authentication flows and customer usage:
// Add analytics tracking to your ACS handler
app.post('/saml/acs',
passport.authenticate('saml', { failureRedirect: '/login' }),
async (req, res) => {
const user = req.user;
// Track successful authentication
await analytics.track('sso_login_success', {
userId: user.id,
email: user.email,
idpType: user.idpType || 'unknown',
timestamp: new Date()
});
res.redirect('/dashboard');
}
);
// Track authentication failures
app.get('/login', (req, res) => {
const { error } = req.query;
if (error) {
analytics.track('sso_login_failure', {
error: error,
timestamp: new Date(),
userAgent: req.get('User-Agent')
});
}
res.render('login', { error });
});
Next Steps
- Delegated SSO Overview - Back to deployment model comparison
- Customer IDP as Broker - Alternative deployment model
- Connecting Okta - Specific Okta configuration details
- Connecting Azure AD - Azure AD setup guide
- Identity Visibility - Monitor your Delegated SSO implementation
- API Reference - Complete API documentation
With Authonomy as your universal broker, you’ve eliminated the complexity of supporting multiple customer IDPs while providing a seamless authentication experience for all your customers.