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)
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:¶
- Validate Configuration: Load LDAP config from database or environment
- Bind Service Account: Connect to LDAP server using
bind_dnandbind_password - Search for User: Execute
user_filterwith provided username - Extract Attributes: Retrieve
attr_mail,attr_name,group_member_attr - Bind as User: Attempt bind with found DN and provided password
- Check IDP Link: Look up user via
UserIdentityProvidertable (idp_type="LDAP", idp_subject=dn) - JIT Provisioning (if new):
- Create
Userrecord - Create
Credentialrecord (password type) with username - Create
Emailrecord if mail attribute found - Create
UserIdentityProviderlink - Generate Tokens:
- Access token (15 min, configurable)
- 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)
3. Get SP Metadata (for IdP configuration)¶
Endpoint: GET /auth/saml/metadata (Public endpoint - IdP fetches this)
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:
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:¶
- Receive SAML Response: Parse base64-encoded
SAMLResponse - Validate Signature: Verify response signature using IdP certificate
- Extract Attributes:
- NameID (unique identifier) - required
- email, name, groups (from IdP attributes)
- Check IDP Link: Look up user via
UserIdentityProvider(idp_type="SAML", idp_subject=nameid) - JIT Provisioning (if new):
- Check if user exists by email (link existing users)
- If no email match, create new user with
username=email or nameid - Create
Emailrecord if available - Create
Credentialrecord if needed - Create
UserIdentityProviderlink - Generate Tokens: Same as LDAP
- Return Success: Return JSON token response
Response:
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)
IDP Link Creation¶
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:
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¶
- Verify LDAP URI is correct and server is reachable
- Check bind_dn and bind_password are correct
- Verify base_dn and user_filter match your LDAP schema
- Test manually:
ldapsearch -x -H ldap://host:port -D cn=admin,... -w password -b dc=example,dc=com "(uid=testuser)"
SAML Assertion Issues¶
- Verify IdP certificate is accurate
- Check ACS URL matches IdP configuration
- Verify SP entity ID is correctly configured
- Test metadata endpoint: GET /auth/saml/metadata
- 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