Advanced SSO Integration

SCIM Provisioning, MFA/FIDO2, Risk-Based Auth & Enterprise Features

SCIM 2.0 MFA/FIDO2 Risk-Based LDAP Sync Monitoring

Introduction

Welcome to the Advanced SSO Integration lab. This guide extends your Zero-Trust SSO Gateway (Project A) with enterprise-grade features including automated user provisioning, phishing-resistant MFA, adaptive authentication, directory synchronization, and production-ready observability.

Enterprise Skill Transfer

The features in this lab directly mirror premium capabilities in Okta Workforce Identity, Microsoft Entra ID P2, Ping Identity, and ForgeRock. Organizations pay $6-$18 per user per month for these features — you'll implement them for free while building interview-ready skills.

Prerequisite: Project A Required

This lab assumes you have completed the Zero-Trust SSO Gateway (Project A) and have a working Authentik + Traefik deployment. If you haven't completed Project A, do that first.

Lab Objectives

Upon successful completion of this lab, you will be able to:

Features Overview

SCIM Provisioning
Automated user lifecycle to apps
MFA/FIDO2
Hardware keys, TOTP, WebAuthn
Risk-Based Auth
Conditional access policies
LDAP Sync
AD/OpenLDAP integration
Monitoring
Prometheus & Grafana
HA Setup
Multi-node deployment

Enterprise Feature Comparison

Feature Okta / Azure AD Authentik (This Lab)
SCIM Provisioning Built-in Native support
FIDO2/WebAuthn Built-in Full support
Conditional Access Advanced policies Flow-based policies
LDAP/AD Sync AD Connect LDAP Source
Cost (1000 users) $6,000-$18,000/yr $0 (self-hosted)

Time Investment

Phase Focus Time Machine
Phase 1 SCIM Provisioning 2 hours SERVER + BROWSER
Phase 2 MFA/FIDO2 Configuration 1.5 hours BROWSER
Phase 3 Risk-Based Authentication 2 hours BROWSER
Phase 4 LDAP/AD Synchronization 1.5 hours SERVER + BROWSER
Phase 5 Prometheus + Grafana Monitoring 1.5 hours SERVER
Phase 6 High Availability Setup 2 hours SERVER

Total: 10.5 hours (builds on Project A foundation)


Table of Contents

1

Phase 1: SCIM Provisioning

Automate user lifecycle management across applications — no more manual account creation!

2 hours SERVER + BROWSER

SCIM (System for Cross-domain Identity Management) is a standard protocol that allows identity providers to automatically provision, update, and deprovision user accounts in connected applications. When you add a user in Authentik, they automatically appear in GitHub, Slack, AWS, and any other SCIM-enabled app.

Why SCIM Matters

Without SCIM, administrators manually create accounts in every application. For a company with 500 employees and 20 apps, that's 10,000 manual account operations. SCIM automates this entirely, reducing onboarding time from hours to minutes and eliminating orphaned accounts that create security risks.

Task 1.1: Deploy Mock SCIM Server for Testing

SERVER Machine — Via SSH Connection

Before connecting to production applications, we'll deploy a mock SCIM server to test our configuration safely.

1
Create the Mock SCIM Server

This Python Flask application implements SCIM 2.0 endpoints and stores users in SQLite for testing.

Bash SERVER
# Navigate to project directory
cd ~/identity-stack

# Create directory for SCIM mock server
mkdir -p scim-mock
cd scim-mock

# Create Python virtual environment
python3 -m venv venv
source venv/bin/activate

# Install required packages
pip install flask gunicorn
2
Create the SCIM Server Application
Python — scim_server.py SERVER
#!/usr/bin/env python3
"""
============================================
MOCK SCIM 2.0 SERVER
Test Authentik SCIM provisioning locally
============================================
"""

from flask import Flask, request, jsonify
import sqlite3
import uuid
from datetime import datetime
from functools import wraps

app = Flask(__name__)

# ============================================
# CONFIGURATION
# ============================================
SCIM_TOKEN = "your-secure-token-here"  # Change this!

# ============================================
# DATABASE INITIALIZATION
# ============================================
def init_db():
    """Create SQLite database for storing provisioned users"""
    conn = sqlite3.connect('scim_users.db')
    cursor = conn.cursor()
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS users (
            id TEXT PRIMARY KEY,
            userName TEXT UNIQUE NOT NULL,
            givenName TEXT,
            familyName TEXT,
            email TEXT,
            active INTEGER DEFAULT 1,
            created_at TEXT,
            updated_at TEXT
        )
    ''')
    conn.commit()
    conn.close()
    print("Database initialized")

# ============================================
# AUTHENTICATION DECORATOR
# ============================================
def require_auth(f):
    """Verify Bearer token for SCIM requests"""
    @wraps(f)
    def decorated(*args, **kwargs):
        auth_header = request.headers.get('Authorization', '')
        if not auth_header.startswith('Bearer '):
            return jsonify({'error': 'Missing Authorization header'}), 401
        
        token = auth_header[7:]
        if token != SCIM_TOKEN:
            return jsonify({'error': 'Invalid token'}), 401
        
        return f(*args, **kwargs)
    return decorated

# ============================================
# SCIM 2.0 ENDPOINTS
# ============================================

@app.route('/scim/v2/Users', methods=['POST'])
@require_auth
def create_user():
    """
    SCIM Create User
    Called when a user is added to a provisioned group in Authentik
    """
    data = request.get_json()
    user_id = str(uuid.uuid4())
    now = datetime.utcnow().isoformat()
    
    conn = sqlite3.connect('scim_users.db')
    cursor = conn.cursor()
    
    try:
        cursor.execute('''
            INSERT INTO users (id, userName, givenName, familyName, email, active, created_at, updated_at)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        ''', (
            user_id,
            data.get('userName'),
            data.get('name', {}).get('givenName', ''),
            data.get('name', {}).get('familyName', ''),
            data.get('emails', [{}])[0].get('value', ''),
            1 if data.get('active', True) else 0,
            now,
            now
        ))
        conn.commit()
        
        print(f" User created: {data.get('userName')}")
        
        return jsonify({
            'schemas': ['urn:ietf:params:scim:schemas:core:2.0:User'],
            'id': user_id,
            'userName': data.get('userName'),
            'meta': {
                'resourceType': 'User',
                'created': now,
                'lastModified': now
            }
        }), 201
        
    except sqlite3.IntegrityError:
        return jsonify({'error': 'User already exists'}), 409
    finally:
        conn.close()

@app.route('/scim/v2/Users/<user_id>', methods=['PATCH'])
@require_auth
def update_user(user_id):
    """
    SCIM Update User
    Called when user attributes change in Authentik
    """
    data = request.get_json()
    now = datetime.utcnow().isoformat()
    
    conn = sqlite3.connect('scim_users.db')
    cursor = conn.cursor()
    
    # Process SCIM patch operations
    for op in data.get('Operations', []):
        if op.get('path') == 'active':
            active = 1 if op.get('value') else 0
            cursor.execute(
                'UPDATE users SET active = ?, updated_at = ? WHERE id = ?',
                (active, now, user_id)
            )
            status = "activated" if active else "deactivated"
            print(f" User {user_id} {status}")
    
    conn.commit()
    conn.close()
    
    return jsonify({'status': 'updated'}), 200

@app.route('/scim/v2/Users/<user_id>', methods=['DELETE'])
@require_auth
def delete_user(user_id):
    """
    SCIM Delete User
    Called when user is removed from provisioned group
    """
    conn = sqlite3.connect('scim_users.db')
    cursor = conn.cursor()
    cursor.execute('DELETE FROM users WHERE id = ?', (user_id,))
    conn.commit()
    conn.close()
    
    print(f" User deleted: {user_id}")
    return '', 204

@app.route('/scim/v2/Users', methods=['GET'])
@require_auth
def list_users():
    """List all provisioned users"""
    conn = sqlite3.connect('scim_users.db')
    cursor = conn.cursor()
    cursor.execute('SELECT * FROM users')
    users = cursor.fetchall()
    conn.close()
    
    return jsonify({
        'schemas': ['urn:ietf:params:scim:api:messages:2.0:ListResponse'],
        'totalResults': len(users),
        'Resources': [{
            'id': u[0],
            'userName': u[1],
            'active': bool(u[5])
        } for u in users]
    })

if __name__ == '__main__':
    init_db()
    app.run(host='0.0.0.0', port=5000, debug=True)
3
Run the Mock SCIM Server
Bash SERVER
# Ensure virtual environment is active
cd ~/identity-stack/scim-mock
source venv/bin/activate

# Start the SCIM server in the background
# Use screen or tmux to keep it running
python scim_server.py &

# Or use gunicorn for production-like setup
gunicorn -w 2 -b 0.0.0.0:5000 scim_server:app &
Verification

Test the SCIM server is running:

Bash SERVER
# Test the SCIM endpoint
curl -X GET http://localhost:5000/scim/v2/Users \
  -H "Authorization: Bearer your-secure-token-here" \
  -H "Content-Type: application/json"
Expected Output:
{"Resources":[],"schemas":["urn:ietf:params:scim:api:messages:2.0:ListResponse"],"totalResults":0}

Task 1.2: Configure SCIM Provider in Authentik

HOST Machine — Authentik Admin Interface
4
Create SCIM Provider

Navigate to Applications Providers Create in Authentik:

Setting Value Explanation
Name Mock SCIM Provider Descriptive name for this provider
Protocol SCIM Select SCIM provider type
URL http://YOUR_SERVER_IP:5000/scim/v2 SCIM endpoint base URL
Token your-secure-token-here Must match SCIM_TOKEN in server
Filter Group scim-provisioned Only users in this group are provisioned
5
Create Provisioning Group

Navigate to Directory Groups Create:

  • Name: scim-provisioned
  • Description: Users in this group are provisioned to connected apps
6
Test SCIM Provisioning
  1. Create a test user in Authentik
  2. Add the user to the scim-provisioned group
  3. Check the mock SCIM server logs for the CREATE event
  4. Remove the user from the group and verify DELETE event
Verification

After adding a user to the provisioning group, check the SCIM server:

Bash SERVER
# List all provisioned users
curl -X GET http://localhost:5000/scim/v2/Users \
  -H "Authorization: Bearer your-secure-token-here"
Expected Output (after adding user):
{"Resources":[{"id":"uuid-here","userName":"testuser","active":true}],"totalResults":1}
Phase 1 Complete!

You've successfully implemented SCIM 2.0 provisioning! Users added to the provisioning group are automatically created in connected applications. This same pattern works with GitHub Enterprise, Slack, AWS SSO, and 100+ other SCIM-enabled apps.


2

Phase 2: MFA/FIDO2 Configuration

Implement phishing-resistant authentication with hardware security keys and TOTP.

1.5 hours BROWSER (Authentik Admin)

Multi-Factor Authentication (MFA) requires users to provide two or more verification factors to gain access. FIDO2/WebAuthn represents the gold standard — using hardware security keys (YubiKey, Titan) or platform authenticators (Touch ID, Windows Hello) for phishing-resistant authentication.

MFA Methods Comparison

FIDO2/WebAuthn

Hardware keys (YubiKey), biometrics

Highest Security
TOTP

Google/Microsoft Authenticator

High Security
Email/SMS OTP

One-time codes via email/SMS

Medium Security
Why FIDO2 is Enterprise Standard

FIDO2/WebAuthn is phishing-resistant because authentication is cryptographically bound to the origin (website domain). Even if a user clicks a phishing link, the security key won't authenticate to a fake domain. Google reported eliminating account takeovers after requiring hardware keys for employees.

Task 2.1: Configure WebAuthn/FIDO2 Stage

HOST Machine — Authentik Admin Interface
1
Create WebAuthn Setup Stage

Navigate to Flows & Stages Stages Create:

Setting Value Explanation
Stage Type Authenticator WebAuthn Setup For enrolling security keys
Name webauthn-setup Stage identifier
User Verification preferred Prefer biometric/PIN verification
Authenticator Attachment any Allow hardware keys + platform auth
2
Create TOTP Setup Stage

Navigate to Flows & Stages Stages Create:

Setting Value Explanation
Stage Type Authenticator TOTP Setup For authenticator apps
Name totp-setup Stage identifier
Digits 6 Standard 6-digit codes
Period 30 Seconds per code rotation
3
Create MFA Validation Stage

Navigate to Flows & Stages Stages Create:

Setting Value Explanation
Stage Type Authenticator Validation Validates MFA during login
Name mfa-validation Stage identifier
Device Classes WebAuthn, TOTP Accepted MFA methods
Not Configured Action Force user to configure Require MFA enrollment
4
Add MFA to Authentication Flow

Navigate to Flows & Stages Flows default-authentication-flow Stage Bindings:

  1. Click "Bind existing stage"
  2. Select mfa-validation
  3. Set Order: 20 (after password stage at order 10)
  4. Enable "Evaluate on plan"
  5. Click "Create"
Verification

Test the MFA flow:

  1. Open an incognito browser window
  2. Navigate to https://authentik.yourdomain.com
  3. Log in with username and password
  4. You should be prompted to set up MFA (WebAuthn or TOTP)
  5. Complete MFA enrollment and verify subsequent logins require MFA
Phase 2 Complete!

You've implemented enterprise-grade MFA with FIDO2/WebAuthn and TOTP support. All users must now authenticate with two factors — something they know (password) and something they have (security key or phone).


Troubleshooting Guide

Common issues and their solutions for advanced SSO features.

SCIM provisioning not triggering
Cause: User not in provisioning group, incorrect endpoint URL, or token mismatch.
Solutions:
  1. Verify user is member of the SCIM filter group
  2. Check SCIM provider URL matches target endpoint exactly
  3. Verify Bearer token matches on both sides
  4. Check Authentik logs: docker logs authentik-server | grep SCIM
WebAuthn registration fails
Cause: HTTPS not configured, browser doesn't support WebAuthn, or domain mismatch.
Solutions:
  1. WebAuthn requires HTTPS — verify TLS is working
  2. Use a modern browser (Chrome, Firefox, Edge, Safari)
  3. Verify the relying party domain matches your Authentik URL
  4. Try a different authenticator (platform vs. roaming)
LDAP sync imports no users
Cause: Incorrect bind DN, search base, or network connectivity issues.
Solutions:
  1. Test LDAP connectivity: ldapsearch -x -H ldap://server -D "binddn" -w pass -b "basedn"
  2. Verify search base DN includes user containers
  3. Check firewall allows port 389 (LDAP) or 636 (LDAPS)
  4. Review Authentik logs for LDAP errors
Prometheus not scraping metrics
Cause: Incorrect target URL, network isolation, or metrics endpoint not exposed.
Solutions:
  1. Verify metrics endpoint: curl http://authentik-server:9300/metrics
  2. Ensure containers are on the same Docker network
  3. Check Prometheus targets: http://localhost:9090/targets
  4. Verify prometheus.yml has correct scrape config

Cleanup Instructions

Remove lab components when finished to free resources.

SERVER Machine — Via SSH Connection

Option A: Stop Services (Preserve Configuration)

Bash SERVER
# Stop SCIM mock server
pkill -f scim_server

# Stop monitoring stack (if deployed)
cd ~/identity-stack
docker compose -f docker-compose.monitoring.yml down

# Main stack continues running

Option B: Complete Removal

Warning: Data Loss

This will remove all configurations from this lab. Your Project A base installation remains intact.

Bash SERVER
# Remove SCIM mock server
rm -rf ~/identity-stack/scim-mock

# Remove monitoring stack and data
docker compose -f docker-compose.monitoring.yml down -v
rm -rf ~/identity-stack/prometheus.yml

# Remove HA configuration files (if created)
rm -rf ~/identity-stack/ha-config

Skills Acquired

Congratulations!

You've completed the Advanced SSO Integration lab and acquired enterprise-level IAM skills.

Interview Ready

Interview Question Your Answer
"How do you automate user provisioning?" SCIM 2.0 protocol. The IdP pushes user lifecycle events (create/update/delete) to applications via REST API. I implemented this with Authentik SCIM providers.
"What's the most secure MFA method?" FIDO2/WebAuthn with hardware security keys. It's phishing-resistant because authentication is cryptographically bound to the origin — the key won't work on fake domains.
"How do you implement conditional access?" Risk-based policies that evaluate context: device trust, location, time, and application sensitivity. Higher risk triggers stronger authentication requirements.
"How do you ensure SSO availability?" Multi-node deployment with shared PostgreSQL and Redis. Load balancer distributes traffic across nodes with automatic failover if one node fails.

Continue Your Journey