Skip to content

SAML Authentication Workflow & Integration Guide

Overview

This document outlines the complete SAML (Security Assertion Markup Language) authentication workflow integrated with the Bank USSD platform. SAML provides Single Sign-On (SSO) capabilities complementing LDAP authentication.

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Bank USSD Application                   β”‚
β”‚  (Service Provider - SP)                                    β”‚
β”‚                                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ Authentication Routes                                β”‚  β”‚
β”‚  β”‚ β€’ POST /auth/saml/login     β†’ Initiate SSO         β”‚  β”‚
β”‚  β”‚ β€’ POST /auth/saml/acs       β†’ Handle SAML Response  β”‚  β”‚
β”‚  β”‚ β€’ GET  /auth/saml/logout    β†’ Initiate Logout      β”‚  β”‚
β”‚  β”‚ β€’ POST /auth/saml/sls       β†’ Handle Logout Responseβ”‚  β”‚
β”‚  β”‚ β€’ GET  /auth/saml/metadata  β†’ SP Metadata          β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                              β–²                               β”‚
β”‚                              β”‚                               β”‚
β”‚              SAML Assertions  β”‚  SAML Requests              β”‚
β”‚                              β”‚                               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                β”‚                             β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚   Keycloak     β”‚          β”‚  SAML Response    β”‚
         β”‚  IdP Server    │◄────────►│  ACS Callback     β”‚
         β”‚ (Optional)     β”‚          β”‚                   β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                 β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚                β”‚
    β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”
    β”‚  LDAP   β”‚    β”‚  Database   β”‚
    β”‚  Server β”‚    β”‚  (User      β”‚
    β”‚         β”‚    β”‚   Auth)     β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Components

1. Service Provider (SP) Configuration

Location: Bank USSD Backend (/api/v1/auth)

Configuration stored in: - Database: auth.saml_config table - Environment: SAML_* env variables - Runtime: controller.auth._saml_config()

Key Settings:

{
    "sp_entity_id": "http://localhost/api/v1/auth/saml/metadata",
    "acs_url": "http://localhost/api/v1/auth/saml/acs",
    "slo_url": "http://localhost/api/v1/auth/saml/sls",
    "idp_entity_id": "http://keycloak:8080/auth/realms/bank-ussd",
    "idp_sso_url": "http://keycloak:8080/auth/realms/bank-ussd/protocol/saml",
    "idp_x509cert": "..certificate...",
    "nameid_format": "persistent"
}

2. Identity Provider (IdP) Configuration

Using: Keycloak (SAML 2.0 compliant)

Realm: bank-ussd Realm URL: http://keycloak:8080/auth/realms/bank-ussd SAML Endpoint: http://keycloak:8080/auth/realms/bank-ussd/protocol/saml

Client Configuration: - Client ID: bank-ussd-saml - Protocol: SAML - Valid Redirect URIs: - http://localhost:8000/api/v1/auth/saml/acs - http://localhost/api/v1/auth/saml/sls

SAML Authentication Flow

Phase 1: Initiation

User β†’ Browser: Click "Login with SAML"
       β–Ό
[POST /auth/saml/login] OR [Redirect to IdP directly]
       β–Ό
Backend creates AuthnRequest
       β–Ό
Browser redirected to Keycloak (IdP)

Phase 2: Authentication at IdP

Keycloak displays login form
       β–Ό
User enters credentials (username/password)
       β–Ό
Keycloak validates credentials
       β–Ό
Keycloak creates SAMLResponse (signed)
       β–Ό
Browser POST to ACS callback

Phase 3: ACS (Assertion Consumer Service)

[POST /auth/saml/acs]
       β–Ό
Backend extracts SAMLResponse from POST data
       β–Ό
Parse and validate SAML assertion:
  β€’ Check signature (IdP certificate)
  β€’ Verify recipient ACS URL
  β€’ Check NotBefore/NotOnOrAfter conditions
  β€’ Extract NameID and attributes
       β–Ό
Extract user attributes:
  β€’ email
  β€’ firstName/lastName
  β€’ branch (custom attribute)
  β€’ roles (SAML roles)
       β–Ό
JIT Provisioning:
  β”œβ”€ User not in DB?
  β”‚  └─ create_ldap_or_saml_user()
  β”‚     β”œβ”€ Create User record
  β”‚     β”œβ”€ Create Profile/Email/Phone
  β”‚     β”œβ”€ Link to Entity (by branch)
  β”‚     └─ Assign roles from SAML
  └─ User exists?
     └─ update_user_from_saml()
        β”œβ”€ Update attributes
        └─ Sync roles if changed
       β–Ό
Create session & return user

Phase 4: Logout Flow

[GET /auth/saml/logout]
       β–Ό
Create LogoutRequest
       β–Ό
Redirect to Keycloak SLO endpoint
       β–Ό
Keycloak processes logout
       β–Ό
Keycloak redirects to SLS callback
       β–Ό
[POST /auth/saml/sls]
       β–Ό
Validate LogoutResponse
       β–Ό
Clear local session

SAML User Provisioning (JIT)

Process:

create_ldap_or_saml_user(
    db=database,
    email="john.smith@bank.local",
    username="john.smith",
    idp="SAML",  # Identity provider type
    groups=["staff"],  # SAML roles
    branch="london",  # From SAML attribute
    kyc_data={
        "first_name": "John",
        "last_name": "Smith",
        "phone": "+447000000000"
    }
) β†’ User

Steps:

  1. Create User Record

    INSERT INTO users.users (id, active, is_verified, status_id)
    VALUES (generated_id, true, true, 'approved')
    

  2. Create Profile

    INSERT INTO customers.profile (identity_id, firstname, lastname)
    VALUES (user_id, 'John', 'Smith')
    

  3. Create Email

    INSERT INTO customers.email (identity_id, email, type_id, is_primary)
    VALUES (user_id, 'john.smith@bank.local', 'email_type_id', true)
    

  4. Link to Entity (Branch)

  5. Extract branch from SAML attribute
  6. Query Entity by code (case-insensitive): Entity.code == 'london'
  7. Create UserEntity link:

    INSERT INTO users.users_entity (user_id, entity_id)
    VALUES (user_id, entity_id)
    

  8. Create UserIdentityProvider Link

    INSERT INTO auth.user_identity_provider (user_id, provider, provider_id, email)
    VALUES (user_id, 'SAML', 'john.smith@keycloak', 'john.smith@bank.local')
    

  9. Assign Roles from SAML Groups

  10. Map SAML roles to database roles: admin β†’ LDAP Admin Role
  11. Create UserRole records:
    INSERT INTO users.user_roles (user_id, role_id)
    VALUES (user_id, role_id)
    

Environment Configuration

Add to .env:

# SAML Settings
SAML_ENABLED=1
SAML_SP_ENTITY_ID=http://localhost/api/v1/auth/saml/metadata
SAML_ACS_URL=http://localhost/api/v1/auth/saml/acs
SAML_SLS_URL=http://localhost/api/v1/auth/saml/sls

# Keycloak IdP URLs (initially blank, will be configured in DB)
SAML_IDP_ENTITY_ID=http://keycloak:8080/auth/realms/bank-ussd
SAML_IDP_SSO_URL=http://keycloak:8080/auth/realms/bank-ussd/protocol/saml
SAML_IDP_X509CERT=

Startup Commands

1. Start Keycloak (Docker Compose)

cd deployment/
docker-compose -f docker-compose-keycloak.yaml up -d

# Wait for Keycloak to be healthy
docker logs -f keycloak

2. Get Keycloak Certificate

# Extract X509 certificate from Keycloak
openssl x509 -in keycloak.crt -text

# Store in database or environment

3. Configure Backend SAML

# Option A: Via SQL (set IdP config)
UPDATE auth.saml_config 
SET 
  idp_entity_id = 'http://keycloak:8080/auth/realms/bank-ussd',
  idp_sso_url = 'http://keycloak:8080/auth/realms/bank-ussd/protocol/saml',
  saml_enabled = true
WHERE id = 1;

# Option B: Via API endpoint (if available)
POST /api/v1/auth/saml/config
{
  "idp_entity_id": "http://keycloak:8080/auth/realms/bank-ussd",
  "idp_sso_url": "http://keycloak:8080/auth/realms/bank-ussd/protocol/saml",
  "idp_x509cert": "..cert.."
}

4. Verify SAML Metadata

curl http://localhost:8000/api/v1/auth/saml/metadata
# Should return valid XML metadata

Test Users (Keycloak)

Username Password Role Branch Email
admin.keycloak admin123 admin london admin@keycloak.local
john.smith password123 staff london john.smith@bank.local
sarah.jones password123 customers manchester sarah.jones@bank.local
bob.wilson password123 operators newcastle bob.wilson@bank.local
emma.davis password123 agents birmingham emma.davis@bank.local

Testing SAML Flow

1. Access SAML Login

# Option A: Web UI
http://localhost:5173/login?method=saml

# Option B: Direct API
curl -X POST http://localhost:8000/api/v1/auth/saml/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "john.smith",
    "channel": "web"
  }'

2. Keycloak Admin Console

URL: http://localhost:8080/auth/admin
Username: admin
Password: admin123
Realm: bank-ussd

3. Verify User Provisioning

-- Check if user was provisioned
SELECT u.id, u.email, ue.entity_id, r.name
FROM users.users u
LEFT JOIN users.users_entity ue ON u.id = ue.user_id
LEFT JOIN users.user_roles ur ON u.id = ur.user_id
LEFT JOIN users.roles r ON ur.role_id = r.id
WHERE u.email LIKE '%@bank.local%';

Troubleshooting

Issue: SAML Response validation fails

Solution: - Verify IdP certificate is correct - Check ACS URL matches exactly - Validate assertion signature

Issue: User attributes not extracted

Solution: - Verify attribute names in Keycloak mapper match SAML protocol mappers - Check protocolMappers in realm config - Ensure attributes are included in SAMLResponse

Issue: Branch not found, user not linked to entity

Solution: - Verify Entity records exist with matching codes (london, manchester, etc.) - Extract branch from SAML assertion: Check attribute name in SAMLResponse - Seed entities if missing:

INSERT INTO core.entity (name, code, status_id)
VALUES ('London HQ', 'london', 'approved');

Issue: Roles not assigned

Solution: - Verify role mapping in LDAP_GROUP_ROLE_MAP environment variable - Ensure Keycloak user has realm roles assigned - Check proto col mappers include role list mapper

Integration with LDAP

Both LDAP and SAML support the same: - Branch extraction and entity linking - Role mapping to database roles - JIT user provisioning - KYC data capture

Differences: | Aspect | LDAP | SAML | |--------|------|------| | Server | OpenLDAP | Keycloak (or any SAML IdP) | | User Location | DN/OU structure | SAML attributes | | Branch | Extracted from DN | Custom attribute in SAML | | Roles | LDAP group membership | SAML role attribute | | Password | Validated via LDAP bind | IdP handles authentication | | Groups | Extracted via group search | Included in SAML response |

Security Considerations

  1. Certificate Management
  2. Validate X509 certificate from IdP
  3. Use HTTPS in production
  4. Regular certificate rotation

  5. Assertion Validation

  6. Always verify assertion signature
  7. Check NotBefore/NotOnOrAfter timestamps
  8. Validate recipient ACS URL
  9. Verify issuer matches expected IdP

  10. Session Management

  11. Use secure session cookies
  12. Implement logout (SAML SLO)
  13. Clear session on invalid assertions

  14. Attribute Security

  15. Don't trust client-submitted attributes
  16. Validate all attributes server-side
  17. Use HTTPS for all SAML exchanges

References