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:

  1. Your application sends users to Authonomy for authentication
  2. Authonomy’s routing engine determines which customer IDP to use
  3. User authenticates with their organization’s IDP
  4. 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

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.