Skip to content

LDAP & SAML Integration Workflow

This document outlines the complete LDAP and SAML authentication workflows in the bank_ussd platform.

Overview

Both LDAP and SAML use Just-In-Time (JIT) user provisioning — users are automatically created on their first successful authentication. All endpoints use POST requests for end-to-end encryption compliance.


LDAP Authentication Flow

1. Configure LDAP Server

Endpoint: PUT /auth/ldap/config

curl -X PUT http://localhost:8000/auth/ldap/config \
  -H "Content-Type: application/json" \
  -d '{
    "uri": "ldap://ldap.example.com:389",
    "bind_dn": "cn=admin,dc=example,dc=com",
    "bind_password": "admin_password",
    "base_dn": "dc=example,dc=com",
    "user_filter": "(uid={username})",
    "attr_mail": "mail",
    "attr_name": "cn",
    "group_member_attr": "memberOf"
  }'

2. Retrieve LDAP Configuration

Endpoint: POST /auth/ldap/config (POST for E2E encryption)

curl -X POST http://localhost:8000/auth/ldap/config

Response:

{
  "uri": "ldap://ldap.example.com:389",
  "bind_dn": "cn=admin,dc=example,dc=com",
  "base_dn": "dc=example,dc=com",
  "user_filter": "(uid={username})",
  "attr_mail": "mail",
  "attr_name": "cn",
  "group_member_attr": "memberOf"
}

Note: bind_password is never leaked in responses.

3. LDAP Login

Endpoint: POST /auth/ldap/login

curl -X POST http://localhost:8000/auth/ldap/login \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d 'username=john.doe&password=ldap_password&method=ldap'

Authentication Flow:

  1. Validate Configuration: Load LDAP config from database or environment
  2. Bind Service Account: Connect to LDAP server using bind_dn and bind_password
  3. Search for User: Execute user_filter with provided username
  4. Extract Attributes: Retrieve attr_mail, attr_name, group_member_attr
  5. Bind as User: Attempt bind with found DN and provided password
  6. Check IDP Link: Look up user via UserIdentityProvider table (idp_type="LDAP", idp_subject=dn)
  7. JIT Provisioning (if new):
  8. Create User record
  9. Create Credential record (password type) with username
  10. Create Email record if mail attribute found
  11. Create UserIdentityProvider link
  12. Generate Tokens:
  13. Access token (15 min, configurable)
  14. Refresh token (7 days, configurable)

Response:

{
  "access_token": "eyJhbGc...",
  "token_type": "bearer",
  "user": { "id": 123, "active": true, ... },
  "refresh_token": "eyJhbGc...",
  "permissions": ["user.read", "user.write"],
  "actions": ["view_dashboard"],
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "user_profile": { ... }
}

SAML Authentication Flow

1. Configure SAML Server

Endpoint: PUT /auth/saml/config

curl -X PUT http://localhost:8000/auth/saml/config \
  -H "Content-Type: application/json" \
  -d '{
    "sp_entity_id": "http://app.example.com/metadata/",
    "acs_url": "http://app.example.com/auth/saml/acs",
    "slo_url": "http://app.example.com/auth/saml/sls",
    "idp_entity_id": "https://idp.example.com/metadata/",
    "idp_sso_url": "https://idp.example.com/sso",
    "idp_x509cert": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
    "nameid_format": "persistent"
  }'

2. Retrieve SAML Configuration

Endpoint: POST /auth/saml/config (POST for E2E encryption)

curl -X POST http://localhost:8000/auth/saml/config

3. Get SP Metadata (for IdP configuration)

Endpoint: GET /auth/saml/metadata (Public endpoint - IdP fetches this)

curl http://localhost:8000/auth/saml/metadata

Returns SAML XML metadata with SP entity ID, ACS URL, etc.

4. Initiate SAML Login

Endpoint: POST /auth/saml/login (Returns redirect URL instead of HTTP redirect)

curl -X POST http://localhost:8000/auth/saml/login \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d 'relay_state=http://app.example.com/dashboard'

Response:

{
  "access_token": "https://idp.example.com/sso?SAMLRequest=...",
  "token_type": "saml_redirect"
}

Client Usage: Redirect browser to the URL in access_token field.

5. Handle SAML Assertion Consumer Service (ACS)

Endpoint: POST /auth/saml/acs

After authentication, IdP POSTs SAML response to this endpoint.

<form method="POST" action="http://app.example.com/auth/saml/acs">
  <input type="hidden" name="SAMLResponse" value="base64_encoded_response" />
  <input type="hidden" name="RelayState" value="dashboard" />
  <input type="submit" value="Submit" />
</form>

SAML ACS Processing Flow:

  1. Receive SAML Response: Parse base64-encoded SAMLResponse
  2. Validate Signature: Verify response signature using IdP certificate
  3. Extract Attributes:
  4. NameID (unique identifier) - required
  5. email, name, groups (from IdP attributes)
  6. Check IDP Link: Look up user via UserIdentityProvider (idp_type="SAML", idp_subject=nameid)
  7. JIT Provisioning (if new):
  8. Check if user exists by email (link existing users)
  9. If no email match, create new user with username=email or nameid
  10. Create Email record if available
  11. Create Credential record if needed
  12. Create UserIdentityProvider link
  13. Generate Tokens: Same as LDAP
  14. Return Success: Return JSON token response

Response:

{
  "access_token": "eyJhbGc...",
  "token_type": "bearer"
}


User Provisioning Logic

New User Creation

When a user authenticates via LDAP/SAML for the first time:

user = create_ldap_or_saml_user(
    db, 
    username=email_or_identifier,  # Required for credential lookup
    email=ldap_mail_or_saml_email    # Optional
)
# Creates:
# - User record with generated identity
# - Credential record (so get_current_user can decrypt JWT subject)
# - Email record (if email provided)
link_user_idp(
    db,
    user_id=user.id,
    idp_type="LDAP" | "SAML",
    idp_subject=ldap_dn | saml_nameid,
    idp_issuer=optional_saml_issuer
)

Subsequent logins match on the IDP link to retrieve the user.


Database Tables

UserIdentityProvider (auth.user_identity_providers)

Links user accounts to external identity providers:

Column Type Description
id Integer Primary key
user_id Integer FK Reference to user
idp_type Enum "LDAP" or "SAML"
idp_subject String LDAP DN or SAML NameID
idp_issuer String (SAML only) IdP entity ID
active Boolean Whether link is active
created_at DateTime Creation timestamp

LDAPConfig (auth.ldap_config)

Singleton configuration for LDAP server (only one record):

Column Type Description
singleton_key String Always "SINGLETON" (unique constraint)
uri String LDAP server URI (ldap://host:port)
bind_dn String Service account DN
bind_password String Service account password (encrypted)
base_dn String Search base
user_filter String Search filter with {username} placeholder
attr_mail String Email attribute name
attr_name String Full name attribute
group_member_attr String Group membership attribute

SAMLConfig (auth.saml_config)

Singleton configuration for SAML (only one record):

Column Type Description
singleton_key String Always "SINGLETON"
sp_entity_id String Service Provider entity ID
acs_url String Assertion Consumer Service endpoint
slo_url String Single Logout endpoint
idp_entity_id String Identity Provider entity ID
idp_sso_url String IdP SSO endpoint
idp_x509cert Text IdP signing certificate
nameid_format String NameID format (e.g., "persistent")

Error Handling

LDAP Errors

Status Reason
400 Missing username/password
401 Invalid LDAP credentials
401 LDAP server unreachable
401 User not found in LDAP

SAML Errors

Status Reason
400 Invalid SAML configuration
401 Invalid SAML response signature
401 SAML response validation failed
401 User not authenticated by IdP
500 SAML metadata generation error

Testing

Run all tests:

pytest test_ldap_saml_integration.py -v

Run specific test class:

pytest test_ldap_saml_integration.py::TestLDAPWorkflow -v
pytest test_ldap_saml_integration.py::TestSAMLWorkflow -v
pytest test_ldap_saml_integration.py::TestE2EWorkflows -v

Test Coverage

  • ✅ LDAP configuration CRUD
  • ✅ SAML configuration CRUD
  • ✅ LDAP authentication (success/failure)
  • ✅ SAML ACS processing
  • ✅ JIT user provisioning
  • ✅ IDP link creation
  • ✅ Token generation
  • ✅ E2E flows

Environment Variables

LDAP

LDAP_URI=ldap://ldap.example.com:389
LDAP_BIND_DN=cn=admin,dc=example,dc=com
LDAP_BIND_PASSWORD=admin_password
LDAP_BASE_DN=dc=example,dc=com
LDAP_USER_FILTER=(uid={username})
LDAP_ATTR_MAIL=mail
LDAP_ATTR_NAME=cn
LDAP_GROUP_MEMBER_ATTR=memberOf

SAML

SAML_SP_ENTITY_ID=http://app.example.com/metadata/
SAML_ACS_URL=http://app.example.com/auth/saml/acs
SAML_SLS_URL=http://app.example.com/auth/saml/sls
SAML_IDP_ENTITY_ID=https://idp.example.com/metadata/
SAML_IDP_SSO_URL=https://idp.example.com/sso
SAML_IDP_X509CERT="-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"

Note: Database configs (PUT /auth/*/config) override environment variables.


Key Features

JIT User Provisioning - Users created automatically on first login
Email-based User Linking - SAML users linked to existing email accounts
Singleton Configuration - Only one LDAP and one SAML config at a time
Password Protection - Bind password never leaked in API responses
POST-only Endpoints - All config endpoints use POST for E2E encryption
Token Generation - Full JWT access + refresh token support
User Profile Integration - Permissions and actions populated from roles
Session Management - UserSession records for audit trail


Troubleshooting

LDAP Connection Issues

  1. Verify LDAP URI is correct and server is reachable
  2. Check bind_dn and bind_password are correct
  3. Verify base_dn and user_filter match your LDAP schema
  4. Test manually: ldapsearch -x -H ldap://host:port -D cn=admin,... -w password -b dc=example,dc=com "(uid=testuser)"

SAML Assertion Issues

  1. Verify IdP certificate is accurate
  2. Check ACS URL matches IdP configuration
  3. Verify SP entity ID is correctly configured
  4. Test metadata endpoint: GET /auth/saml/metadata
  5. Check SAML response timestamps haven't expired

Token Expiry

  • Access tokens: 15 minutes (configurable via ACCESS_TOKEN_EXPIRE_MINUTES)
  • Refresh tokens: 7 days (configurable via REFRESH_TOKEN_EXPIRE_MINUTES)
  • Use refresh endpoint: POST /auth/refresh