Identity Platform Migration
Identity platform migration transfers user accounts, group memberships, authentication configurations, and application integrations from a source identity provider to a target identity provider. Perform this migration when changing identity platforms due to cost optimisation, capability requirements, data sovereignty concerns, or organisational consolidation. A successful migration results in all users authenticating against the new platform with no loss of access to integrated applications.
Prerequisites
| Requirement | Detail |
|---|---|
| Target platform | Identity provider deployed, configured, and tested with at least 10 pilot users |
| Source platform access | Global administrator or equivalent role with read access to all users, groups, and audit logs |
| Target platform access | Global administrator or equivalent role with write access to create users, groups, and applications |
| Migration tooling | Platform-specific migration tool installed and authenticated to both platforms |
| Application inventory | Complete list of applications integrated with source IdP, including authentication protocol and owner |
| Rollback window | Minimum 72-hour period where rollback remains possible before source platform decommissioning |
| Communication plan | Approved messaging for users covering timeline, expected impact, and support channels |
| MFA recovery | Backup authentication method available for all users during MFA re-enrollment |
Verify administrative access to both platforms before proceeding:
# For Keycloak target - verify admin CLI access/opt/keycloak/bin/kcadm.sh config credentials \ --server https://idp.example.org/auth \ --realm master \ --user admin \ --password "${KEYCLOAK_ADMIN_PASSWORD}"
# List realms to confirm access/opt/keycloak/bin/kcadm.sh get realms --fields realm# Expected output: list of configured realms# For Microsoft Entra ID - verify Graph API accessConnect-MgGraph -Scopes "User.ReadWrite.All","Group.ReadWrite.All","Application.ReadWrite.All"Get-MgOrganization | Select-Object DisplayName, Id# Expected output: organisation details confirming tenant access# For Authentik target - verify API accesscurl -s -H "Authorization: Bearer ${AUTHENTIK_TOKEN}" \ https://idp.example.org/api/v3/core/users/ | jq '.pagination.count'# Expected output: current user count (should be minimal for new instance)Procedure
Phase 1: Discovery and mapping
Export the complete user inventory from the source identity provider. Capture all attributes that applications depend on for authorisation decisions.
For Microsoft Entra ID source:
# Export all users with relevant attributes Get-MgUser -All -Property Id,UserPrincipalName,Mail,DisplayName,GivenName,Surname, Department,JobTitle,OfficeLocation,MobilePhone,AccountEnabled, OnPremisesSyncEnabled,CreatedDateTime,SignInActivity | Export-Csv -Path "users_export.csv" -NoTypeInformation
# Count for verification $users = Import-Csv "users_export.csv" Write-Host "Exported $($users.Count) users" # Expected: matches user count in admin consoleFor Keycloak source:
# Export realm containing users /opt/keycloak/bin/kc.sh export \ --dir /tmp/export \ --realm production \ --users realm_file
# Verify export jq '.users | length' /tmp/export/production-realm.json # Expected: user count matching admin consoleFor Okta source:
# Export users via API (paginated) curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \ "https://example.okta.com/api/v1/users?limit=200" \ > users_page1.json
# Continue pagination using 'next' link in response headers # Combine all pages into single export jq -s 'add' users_page*.json > users_export.json jq 'length' users_export.json # Expected: total user count- Export group memberships with their associated permissions and application assignments. Groups form the basis of access control in the target platform.
# Microsoft Entra ID - export groups and members $groups = Get-MgGroup -All -Property Id,DisplayName,Description,GroupTypes, SecurityEnabled,MailEnabled
foreach ($group in $groups) { $members = Get-MgGroupMember -GroupId $group.Id -All $group | Add-Member -NotePropertyName "MemberCount" -NotePropertyValue $members.Count $group | Add-Member -NotePropertyName "Members" -NotePropertyValue ($members.Id -join ";") }
$groups | Export-Csv -Path "groups_export.csv" -NoTypeInformation Write-Host "Exported $($groups.Count) groups" # Keycloak - groups are included in realm export jq '.groups | length' /tmp/export/production-realm.json jq '.groups[].name' /tmp/export/production-realm.jsonCreate the attribute mapping between source and target platforms. Attribute names differ between providers, and applications depend on specific claim names in tokens.
Document the mapping in a configuration file that migration scripts consume:
user_attributes: # source_attribute: target_attribute userPrincipalName: username mail: email givenName: firstName surname: lastName displayName: displayName department: attributes.department jobTitle: attributes.jobTitle mobilePhone: attributes.mobile officeLocation: attributes.location
group_mapping: # source_group_name: target_group_name "All Staff": "all-staff" "Finance Team": "finance" "IT Administrators": "it-admins" "Field Staff": "field-operations"
# Attributes required by specific applications application_claims: grants-system: - email - displayName - attributes.department - groups hr-portal: - email - firstName - lastName - attributes.employeeId- Inventory all applications integrated with the source identity provider. Record the authentication protocol, redirect URIs, and any custom claims each application requires.
# Microsoft Entra ID - export enterprise applications Get-MgServicePrincipal -All -Property Id,DisplayName,AppId, SignInAudience,PreferredSingleSignOnMode,LoginUrl | Where-Object { $_.PreferredSingleSignOnMode -ne $null } | Export-Csv -Path "applications_export.csv" -NoTypeInformation # Keycloak - list clients (applications) /opt/keycloak/bin/kcadm.sh get clients -r production \ --fields id,clientId,protocol,enabled,directAccessGrantsEnabled \ > clients_export.json
jq -r '.[] | select(.enabled==true) | .clientId' clients_export.jsonFor each application, document:
| Application | Protocol | Redirect URIs | Custom claims | Owner |
|---|---|---|---|---|
| Grants Management | OIDC | https://grants.example.org/callback | department, groups | Finance |
| HR Portal | SAML | https://hr.example.org/sso/saml | employeeId, manager | HR |
| Field Data Collection | OIDC | https://collect.example.org/auth | location, role | Programmes |
| Wiki | OIDC | https://wiki.example.org/oauth/callback | displayName, email | IT |
Phase 2: Target platform configuration
Create the organisational structure in the target platform. This includes realms or tenants, administrative roles, and base configuration.
For Keycloak:
# Create production realm /opt/keycloak/bin/kcadm.sh create realms \ -s realm=production \ -s enabled=true \ -s displayName="Production" \ -s loginTheme=organisation-theme \ -s accountTheme=organisation-theme \ -s internationalizationEnabled=true \ -s supportedLocales='["en","fr","es","ar"]' \ -s defaultLocale=en
# Configure token lifespans /opt/keycloak/bin/kcadm.sh update realms/production \ -s accessTokenLifespan=300 \ -s ssoSessionIdleTimeout=1800 \ -s ssoSessionMaxLifespan=36000 \ -s offlineSessionIdleTimeout=2592000
# Configure password policy /opt/keycloak/bin/kcadm.sh update realms/production \ -s 'passwordPolicy="length(12) and digits(1) and upperCase(1) and specialChars(1) and notUsername"'For Authentik:
# Create tenant via API curl -X POST -H "Authorization: Bearer ${AUTHENTIK_TOKEN}" \ -H "Content-Type: application/json" \ https://idp.example.org/api/v3/core/tenants/ \ -d '{ "domain": "example.org", "default": true, "branding_title": "Organisation SSO", "branding_logo": "/media/branding/logo.png", "flow_authentication": "default-authentication-flow", "flow_invalidation": "default-invalidation-flow" }'- Create groups in the target platform matching the source group structure. Preserve group hierarchies where the target platform supports nesting.
# Keycloak - create groups from mapping while IFS=: read -r source_name target_name; do /opt/keycloak/bin/kcadm.sh create groups -r production \ -s name="${target_name}" echo "Created group: ${target_name}" done < <(yq '.group_mapping | to_entries | .[] | .key + ":" + .value' attribute_mapping.yaml) # Authentik - create groups for group_name in "all-staff" "finance" "it-admins" "field-operations"; do curl -X POST -H "Authorization: Bearer ${AUTHENTIK_TOKEN}" \ -H "Content-Type: application/json" \ https://idp.example.org/api/v3/core/groups/ \ -d "{\"name\": \"${group_name}\", \"is_superuser\": false}" done- Configure authentication flows including MFA requirements. Match or improve upon source platform security policies.
# Keycloak - configure browser authentication flow with MFA # Copy built-in browser flow /opt/keycloak/bin/kcadm.sh create authentication/flows/browser/copy \ -r production \ -s newName="browser-with-otp"
# Get the flow ID FLOW_ID=$(/opt/keycloak/bin/kcadm.sh get authentication/flows \ -r production --fields id,alias | \ jq -r '.[] | select(.alias=="browser-with-otp") | .id')
# Add OTP execution /opt/keycloak/bin/kcadm.sh create authentication/flows/browser-with-otp/executions/execution \ -r production \ -s provider=auth-otp-form \ -s requirement=REQUIRED
# Bind flow to browser /opt/keycloak/bin/kcadm.sh update realms/production \ -s browserFlow="browser-with-otp"Phase 3: User migration
- Transform user data from source format to target format using the attribute mapping. Generate import files or API payloads.
#!/usr/bin/env python3 import csv import json import yaml import hashlib from datetime import datetime
# Load attribute mapping with open('attribute_mapping.yaml') as f: mapping = yaml.safe_load(f)
# Load source users with open('users_export.csv') as f: source_users = list(csv.DictReader(f))
target_users = [] for user in source_users: if user['AccountEnabled'] != 'True': continue # Skip disabled accounts
target_user = { 'username': user['UserPrincipalName'].split('@')[0].lower(), 'email': user['Mail'], 'firstName': user['GivenName'], 'lastName': user['Surname'], 'enabled': True, 'emailVerified': True, 'attributes': { 'department': [user['Department']] if user['Department'] else [], 'jobTitle': [user['JobTitle']] if user['JobTitle'] else [], 'sourceId': [user['Id']], # Preserve source ID for correlation 'migratedAt': [datetime.utcnow().isoformat()] }, 'requiredActions': ['CONFIGURE_TOTP'] # Force MFA setup on first login } target_users.append(target_user)
# Write Keycloak import format realm_import = { 'realm': 'production', 'users': target_users }
with open('users_import.json', 'w') as f: json.dump(realm_import, f, indent=2)
print(f"Transformed {len(target_users)} users for import")Run the transformation:
python3 transform_users.py # Expected output: Transformed 847 users for import- Import users into the target platform. For large migrations (over 1000 users), batch the import to avoid timeouts.
# Keycloak - import users via partial import /opt/keycloak/bin/kc.sh import \ --file users_import.json \ --override false
# Verify import count /opt/keycloak/bin/kcadm.sh get users -r production --fields username | \ jq 'length' # Expected: matches source enabled user count (847) # Authentik - import via API (batched) jq -c '.users[]' users_import.json | while read -r user; do curl -X POST -H "Authorization: Bearer ${AUTHENTIK_TOKEN}" \ -H "Content-Type: application/json" \ https://idp.example.org/api/v3/core/users/ \ -d "${user}" sleep 0.1 # Rate limiting donePassword handling
Passwords cannot be migrated between most identity providers due to incompatible hashing algorithms. Users must reset passwords or use an alternative first-login mechanism. See Step 11 for password handling strategies.
- Assign users to groups based on source group memberships. This restores access control after migration.
# Keycloak - assign group memberships # Load group membership mapping from source export while IFS=, read -r username group_name; do # Get user ID USER_ID=$(/opt/keycloak/bin/kcadm.sh get users -r production \ -q username="${username}" --fields id | jq -r '.[0].id')
# Get group ID GROUP_ID=$(/opt/keycloak/bin/kcadm.sh get groups -r production \ -q search="${group_name}" --fields id | jq -r '.[0].id')
if [ -n "${USER_ID}" ] && [ -n "${GROUP_ID}" ]; then /opt/keycloak/bin/kcadm.sh update users/${USER_ID}/groups/${GROUP_ID} \ -r production -s realm=production -s userId=${USER_ID} -s groupId=${GROUP_ID} -n fi done < group_memberships.csvConfigure password handling for migrated users. Three strategies exist, each with different user experience and security implications.
Strategy A: Forced password reset on first login
Users receive a password reset link and must set a new password before accessing any application. This approach provides the cleanest security posture but requires all users to take action.
# Keycloak - add required action to all migrated users /opt/keycloak/bin/kcadm.sh get users -r production --fields id | \ jq -r '.[].id' | while read -r user_id; do /opt/keycloak/bin/kcadm.sh update users/${user_id} -r production \ -s 'requiredActions=["UPDATE_PASSWORD"]' doneStrategy B: Password synchronisation during coexistence
During the coexistence period, authenticate against the source platform and capture passwords to set in the target. This requires a custom authentication flow and should only be used when forced reset is operationally unacceptable.
Strategy C: Temporary passwords with immediate reset
Generate unique temporary passwords, distribute securely to each user, and require immediate change. Suitable for small migrations under 100 users.
import secrets import string import csv
def generate_password(length=16): alphabet = string.ascii_letters + string.digits + "!@#$%^&*" return ''.join(secrets.choice(alphabet) for _ in range(length))
with open('users_import.json') as f: users = json.load(f)['users']
with open('temp_passwords.csv', 'w', newline='') as f: writer = csv.writer(f) writer.writerow(['username', 'email', 'temp_password']) for user in users: temp_pw = generate_password() writer.writerow([user['username'], user['email'], temp_pw])
# Encrypt the file before distribution # gpg -c temp_passwords.csvPhase 4: Application integration
- Configure each application in the target identity provider. Create client registrations with matching redirect URIs and claim configurations.
# Keycloak - create OIDC client for grants management system /opt/keycloak/bin/kcadm.sh create clients -r production \ -s clientId=grants-management \ -s enabled=true \ -s protocol=openid-connect \ -s publicClient=false \ -s 'redirectUris=["https://grants.example.org/callback","https://grants.example.org/silent-refresh"]' \ -s 'webOrigins=["https://grants.example.org"]' \ -s standardFlowEnabled=true \ -s directAccessGrantsEnabled=false \ -s serviceAccountsEnabled=false
# Get client ID for secret generation CLIENT_UUID=$(/opt/keycloak/bin/kcadm.sh get clients -r production \ -q clientId=grants-management --fields id | jq -r '.[0].id')
# Generate client secret /opt/keycloak/bin/kcadm.sh create clients/${CLIENT_UUID}/client-secret -r production
# Retrieve secret for application configuration /opt/keycloak/bin/kcadm.sh get clients/${CLIENT_UUID}/client-secret -r production # Keycloak - create SAML client for HR portal /opt/keycloak/bin/kcadm.sh create clients -r production \ -s clientId=https://hr.example.org \ -s enabled=true \ -s protocol=saml \ -s 'attributes={"saml.assertion.signature":"true","saml.server.signature":"true","saml.signature.algorithm":"RSA_SHA256","saml.force.post.binding":"true","saml_name_id_format":"email"}' \ -s 'redirectUris=["https://hr.example.org/sso/saml/*"]' \ -s baseUrl=https://hr.example.org \ -s adminUrl=https://hr.example.org/sso/saml- Configure custom claim mappings for applications requiring non-standard attributes.
# Keycloak - add department claim to grants-management client CLIENT_UUID=$(/opt/keycloak/bin/kcadm.sh get clients -r production \ -q clientId=grants-management --fields id | jq -r '.[0].id')
/opt/keycloak/bin/kcadm.sh create clients/${CLIENT_UUID}/protocol-mappers/models \ -r production \ -s name=department \ -s protocol=openid-connect \ -s protocolMapper=oidc-usermodel-attribute-mapper \ -s 'config={"claim.name":"department","user.attribute":"department","id.token.claim":"true","access.token.claim":"true","userinfo.token.claim":"true","jsonType.label":"String"}'
# Add group membership claim /opt/keycloak/bin/kcadm.sh create clients/${CLIENT_UUID}/protocol-mappers/models \ -r production \ -s name=groups \ -s protocol=openid-connect \ -s protocolMapper=oidc-group-membership-mapper \ -s 'config={"claim.name":"groups","full.path":"false","id.token.claim":"true","access.token.claim":"true","userinfo.token.claim":"true"}'Update application configurations to use the target identity provider. Perform this during a maintenance window for each application.
For OIDC applications, update these configuration values:
# Application configuration - before migration oidc: issuer: https://login.microsoftonline.com/tenant-id/v2.0 client_id: old-client-id client_secret: old-secret authorization_endpoint: https://login.microsoftonline.com/tenant-id/oauth2/v2.0/authorize token_endpoint: https://login.microsoftonline.com/tenant-id/oauth2/v2.0/token
# Application configuration - after migration oidc: issuer: https://idp.example.org/realms/production client_id: grants-management client_secret: new-generated-secret authorization_endpoint: https://idp.example.org/realms/production/protocol/openid-connect/auth token_endpoint: https://idp.example.org/realms/production/protocol/openid-connect/tokenFor SAML applications, update the IdP metadata URL:
<!-- New IdP metadata location --> <md:EntityDescriptor entityID="https://idp.example.org/realms/production"> <!-- Application imports metadata from --> <!-- https://idp.example.org/realms/production/protocol/saml/descriptor --> </md:EntityDescriptor>Phase 5: Coexistence and cutover
Configure federation between source and target platforms for the coexistence period. This allows users to authenticate via either platform while applications migrate.
The coexistence architecture routes authentication based on application migration status:
+----------------------------------------------------------------+ | USER AUTHENTICATION REQUEST | +----------------------------------------------------------------+ | +---------------v---------------+ | Application checks | | migration status | +---------------+---------------+ | +---------------------+---------------------+ | | v v +---------+---------+ +---------+---------+ | Not migrated | | Migrated | | | | | | Redirect to | | Redirect to | | SOURCE IdP | | TARGET IdP | +---------+---------+ +---------+---------+ | | v v +---------+---------+ +---------+---------+ | Source IdP | | Target IdP | | authenticates | | authenticates | +---------+---------+ +---------+---------+ | | +---------------------+---------------------+ | v +---------------+---------------+ | User accesses | | application | +-------------------------------+For Keycloak target with Entra ID source, configure identity brokering:
# Create identity provider for source platform /opt/keycloak/bin/kcadm.sh create identity-provider/instances -r production \ -s alias=azure-ad \ -s providerId=oidc \ -s enabled=true \ -s 'config={"authorizationUrl":"https://login.microsoftonline.com/tenant-id/oauth2/v2.0/authorize","tokenUrl":"https://login.microsoftonline.com/tenant-id/oauth2/v2.0/token","clientId":"broker-client-id","clientSecret":"broker-secret","defaultScope":"openid email profile"}'- Execute a pilot migration with a test group of 20-50 users representing different roles and application usage patterns.
# Create pilot group /opt/keycloak/bin/kcadm.sh create groups -r production -s name="migration-pilot"
# Add pilot users (select from different departments) for username in "jsmith" "agarcia" "mwang" "okonkwo" "dubois"; do USER_ID=$(/opt/keycloak/bin/kcadm.sh get users -r production \ -q username="${username}" --fields id | jq -r '.[0].id') GROUP_ID=$(/opt/keycloak/bin/kcadm.sh get groups -r production \ -q search="migration-pilot" --fields id | jq -r '.[0].id') /opt/keycloak/bin/kcadm.sh update users/${USER_ID}/groups/${GROUP_ID} -r production -n donePilot users test:
- Password reset flow
- MFA enrollment
- Access to each migrated application
- Mobile device authentication
- VPN authentication (if applicable)
Execute the production cutover. Migrate applications in dependency order, starting with those that have no downstream dependencies.
Migration sequence for a typical organisation:
Order Application Dependencies Cutover window 1 Wiki/Intranet None 30 minutes 2 Email (if OIDC) None 60 minutes 3 HR Portal None 45 minutes 4 Finance System HR Portal (user sync) 60 minutes 5 Grants Management Finance System 45 minutes 6 Field Data Collection Grants Management 30 minutes 7 VPN All internal apps 120 minutes
# Cutover script for each application # Run during scheduled maintenance window
APP_NAME="grants-management" TIMESTAMP=$(date +%Y%m%d_%H%M%S)
echo "[${TIMESTAMP}] Starting cutover for ${APP_NAME}"
# 1. Notify active users /opt/scripts/notify_active_users.sh "${APP_NAME}" "Maintenance starting in 5 minutes" sleep 300
# 2. Put application in maintenance mode kubectl scale deployment ${APP_NAME} --replicas=0 -n production
# 3. Update application configuration kubectl create configmap ${APP_NAME}-oidc \ --from-file=oidc-config.yaml \ --dry-run=client -o yaml | kubectl apply -f -
# 4. Restart application with new configuration kubectl scale deployment ${APP_NAME} --replicas=3 -n production kubectl rollout status deployment ${APP_NAME} -n production
# 5. Verify authentication /opt/scripts/test_oidc_flow.sh "${APP_NAME}"
echo "[$(date +%Y%m%d_%H%M%S)] Cutover complete for ${APP_NAME}"- Disable source platform authentication for migrated applications while maintaining the platform for rollback capability.
# Microsoft Entra ID - disable enterprise application $app = Get-MgServicePrincipal -Filter "displayName eq 'Grants Management'" Update-MgServicePrincipal -ServicePrincipalId $app.Id -AccountEnabled:$falseMaintain the source platform in read-only mode for 30 days to support rollback if critical issues emerge.
Rollback
If critical issues require reverting to the source platform, execute the rollback within the 72-hour window before source platform accounts are disabled.
Identify the scope of rollback: single application, group of applications, or full migration.
For single application rollback:
# Revert application configuration to source IdP kubectl create configmap ${APP_NAME}-oidc \ --from-file=oidc-config-source.yaml \ --dry-run=client -o yaml | kubectl apply -f -
kubectl rollout restart deployment ${APP_NAME} -n production- For full migration rollback:
# Re-enable source platform applications Get-MgServicePrincipal -Filter "startswith(displayName, 'Migrated-')" | ForEach-Object { Update-MgServicePrincipal -ServicePrincipalId $_.Id -AccountEnabled:$true }Communicate rollback to users with instructions to use their original credentials.
Document rollback reasons and blockers for resolution before reattempting migration.
Verification
After cutover, verify each component of the migration:
User authentication verification:
# Test authentication flow for sample usersfor username in "jsmith" "agarcia" "mwang"; do # Request token using resource owner password grant (test only) RESPONSE=$(curl -s -X POST \ https://idp.example.org/realms/production/protocol/openid-connect/token \ -d "grant_type=password" \ -d "client_id=test-client" \ -d "client_secret=${TEST_CLIENT_SECRET}" \ -d "username=${username}" \ -d "password=${TEST_PASSWORD}" \ -d "scope=openid")
if echo "${RESPONSE}" | jq -e '.access_token' > /dev/null; then echo "✓ ${username}: authentication successful" else echo "✗ ${username}: authentication failed" echo "${RESPONSE}" | jq '.error_description' fidoneGroup membership verification:
# Verify group assignments match source/opt/keycloak/bin/kcadm.sh get groups -r production --fields name,id | \ jq -r '.[].id' | while read -r group_id; do GROUP_NAME=$(/opt/keycloak/bin/kcadm.sh get groups/${group_id} -r production --fields name | jq -r '.name') MEMBER_COUNT=$(/opt/keycloak/bin/kcadm.sh get groups/${group_id}/members -r production | jq 'length') echo "${GROUP_NAME}: ${MEMBER_COUNT} members" done
# Compare with source counts# Expected: counts match within 5% (accounting for disabled accounts)Application integration verification:
# Verify each application can obtain tokens and validate claimsfor app in "grants-management" "hr-portal" "field-collection"; do # Get application health endpoint that requires authentication HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ -H "Authorization: Bearer ${ADMIN_TOKEN}" \ "https://${app}.example.org/api/health/auth")
if [ "${HTTP_CODE}" = "200" ]; then echo "✓ ${app}: integration working" else echo "✗ ${app}: integration failed (HTTP ${HTTP_CODE})" fidoneMFA enrollment verification:
# Check MFA enrollment status/opt/keycloak/bin/kcadm.sh get users -r production --fields username,totp | \ jq -r '.[] | select(.totp==true) | .username' | wc -l# Expected: increases daily during migration period as users enroll
# Users without MFA (may need reminders)/opt/keycloak/bin/kcadm.sh get users -r production --fields username,totp,email | \ jq -r '.[] | select(.totp==false) | "\(.username),\(.email)"' > users_without_mfa.csvTroubleshooting
| Symptom | Cause | Resolution |
|---|---|---|
| User cannot authenticate: “Invalid credentials” | Password not migrated or user has not reset | Direct user to password reset flow; verify reset email delivery |
| User cannot authenticate: “Account disabled” | User disabled in source and status migrated | Re-enable account if user should have access: /opt/keycloak/bin/kcadm.sh update users/${USER_ID} -r production -s enabled=true |
| Application error: “Invalid redirect URI” | Redirect URI mismatch between app config and IdP client | Add missing redirect URI to client configuration; verify no trailing slashes |
| Application error: “Token validation failed” | Client secret mismatch or wrong issuer URL | Regenerate client secret and update application configuration |
| Missing claims in token | Protocol mapper not configured | Add required protocol mapper to client; verify claim name matches application expectation |
| SAML assertion signature invalid | Certificate mismatch or algorithm incompatibility | Re-download IdP metadata; verify signature algorithm matches application requirement (SHA256 vs SHA1) |
| User missing from target platform | Transformation script filtered user (disabled, no email) | Check transformation logs; manually import if legitimately needed |
| Group membership not applied | Group name mapping mismatch | Verify group exists in target with expected name; check case sensitivity |
| MFA prompt on every login | Session not persisting or SSO session timeout too short | Increase ssoSessionIdleTimeout; verify cookies not blocked |
| Federated login loop | Misconfigured identity brokering | Disable identity brokering temporarily; verify broker client credentials |
| Login page displays source IdP branding | Cached assets or incorrect theme binding | Clear browser cache; verify realm theme configuration |
| Mobile app authentication fails | Redirect URI scheme not registered | Add mobile app custom scheme to client redirect URIs: org.example.app:/callback |
| VPN authentication fails after migration | RADIUS/LDAP integration not updated | Reconfigure VPN to use new LDAP bind credentials or OIDC resource owner grant |
| Slow authentication (over 5 seconds) | Database performance or network latency to IdP | Check IdP database query performance; verify network path; consider regional deployment |
| Intermittent “Session expired” errors | Clock skew between IdP and application servers | Synchronise NTP across all servers; allow 60-second clock skew tolerance in token validation |
Post-migration tasks
After successful verification, complete these tasks to finalise the migration:
- Decommission source platform (after 30-day observation period):
# Microsoft Entra ID - delete migrated user accounts (irreversible) # Only after confirming all users successfully authenticate to target Import-Csv migrated_users.csv | ForEach-Object { Remove-MgUser -UserId $_.SourceId -Confirm:$false }Update documentation: Replace all references to source IdP URLs, procedures, and screenshots.
Train service desk: Provide updated procedures for password reset, account unlock, and MFA reset.
Configure monitoring: Establish alerting for authentication failures, token errors, and service availability.
Schedule access review: Plan first quarterly access certification in new platform within 90 days.