Upgrading to Multi-Provider IDP¶
Version: v1.2 Last Updated: 2026-05-28 (PLAN-1619) Audience: Operators upgrading an existing single-provider Cesivi deployment
See also:
- MULTI-PROVIDER-IDP.md — Full operator guide for multi-provider config
- PEOPLE-PICKER-IDP.md — People-picker directory browse
Overview¶
Cesivi's multi-provider IDP support (shipped in PLAN-1613..1618) is fully backwards compatible. An existing single-provider deployment requires no configuration changes to keep working. This guide is for operators who:
- Want to add a second provider to an existing deployment, OR
- Want to migrate existing users to the federated identity model, OR
- Need to recover from an auto-link mistake.
Part 1: Config Migration¶
Is migration required?¶
No. The original single-object config continues to work unchanged. Skip to Part 2 if you only want to add a second provider without touching the existing one.
What changed under the hood¶
When Cesivi sees the old single-object form:
"Oidc": {
"Authority": "https://sso.example.com/realms/corp",
"ClientId": "cesivi-webui",
...
}
It internally wraps it as a 1-element list with Name="oidc". The user's login names in
storage are encoded as i:0e.t|oidc|<subject>. These continue to work.
Migrating to the named list form (optional)¶
If you want an explicit name (e.g. keycloak-corp instead of the synthetic oidc),
you must update the config AND rename the login-name encoding in storage.
Step 1 — Update config:
"Oidc": [
{
"Name": "keycloak-corp",
"Authority": "https://sso.example.com/realms/corp",
"ClientId": "cesivi-webui",
"ClientSecret": "...",
"Audience": "cesivi"
}
]
Step 2 — Run the login-name migration script (FileSystem storage only):
# Scripts/Migrate-OidcProviderName.ps1
# Renames "oidc" → "keycloak-corp" in all ExternalLogin rows and CesiviUser.LoginName columns.
# Run once after updating the config; safe to run with server stopped or running (read-modify-write is atomic per user).
param(
[string]$DataRootPath = "R:\MockData",
[string]$OldProviderName = "oidc",
[string]$NewProviderName = "keycloak-corp"
)
$pattern = "i:0e.t|$OldProviderName|"
$replacement = "i:0e.t|$NewProviderName|"
Get-ChildItem -Recurse -Filter "*.json" $DataRootPath | ForEach-Object {
$content = Get-Content $_.FullName -Raw
if ($content -like "*$pattern*") {
$content = $content.Replace($pattern, $replacement)
Set-Content $_.FullName $content
Write-Host "Updated: $($_.FullName)"
}
}
Write-Host "Done. Start Cesivi and verify login."
Step 3 — Restart Cesivi after the config change.
SQL / LiteDb storage: the migration is a simple
UPDATEon theExternalLoginstable'sProviderNamecolumn and theCesiviUserstable'sLoginNamecolumn. Adapt the script above to your SQL provider.
Adding a second provider¶
- Change the
Oidcvalue to an array (if not already). - Add the new provider entry. See Section 2 and 3 in
MULTI-PROVIDER-IDP.mdfor Keycloak and Entra examples. - Restart Cesivi.
- The login screen now shows a button (chip) per enabled provider.
- New users logging in via the new provider receive their own
CesiviUserand anExternalLoginrow tied to the new provider.
Part 2: Migrating Existing Users to ExternalLogin Rows¶
Why you might need this¶
Before PLAN-1616, Cesivi stored OIDC user identity directly in the CesiviUser record
(in the LoginName field, e.g. i:0e.t|oidc|<subject>). After PLAN-1616, each
authentication event also creates or updates an ExternalLogin row.
For users who logged in after PLAN-1616 deployed, the row is created automatically on first login. For users who existed before the upgrade, the row is missing until they log in once. This is usually fine — their next login creates it.
If you need all users to have explicit ExternalLogin rows immediately (e.g. for reporting
or before enabling the federated auto-link policy), run the backfill:
One-shot ExternalLogin backfill¶
# POST /_api/admin/federatedidentity/backfill
# Body: { "ProviderName": "keycloak-corp", "DryRun": true }
#
# DryRun=true shows what would be created without modifying storage.
# DryRun=false applies the changes.
$headers = @{ Authorization = "Bearer $adminToken"; "Content-Type" = "application/json" }
$body = @{ ProviderName = "keycloak-corp"; DryRun = $false } | ConvertTo-Json
$result = Invoke-RestMethod `
-Method POST `
-Uri "https://cesivi.example.com/_api/admin/federatedidentity/backfill" `
-Headers $headers `
-Body $body
Write-Host "Backfill result: $($result | ConvertTo-Json -Depth 3)"
# Output: { "processedUsers": 142, "rowsCreated": 139, "alreadyLinked": 3, "skipped": 0 }
The backfill endpoint:
- Scans all CesiviUser records whose LoginName matches i:0e.t|<ProviderName>|*
- Creates an ExternalLogin row for each one (skipping those that already exist)
- Writes an audit-log entry for each row created
- Is idempotent: running it twice produces no duplicates
Requires: the caller must have the FarmAdmin role claim.
Part 3: Recovering from an Auto-Link Mistake¶
What can go wrong¶
When AutoLinkPolicy: SameVerifiedEmail is enabled, a first login from a new provider
links the new ExternalLogin to an existing CesiviUser if the emails match. If your
IDP configuration was wrong (e.g. email verification was off, or you pointed at the wrong
realm), you may end up with an incorrect link.
How to detect bad links¶
# List all ExternalLogins for a user (by display name or email)
curl -H "Authorization: Bearer $adminToken" \
"https://cesivi.example.com/_api/web/siteusers/getbyemail('alice@example.com')/externallogins"
# Response:
# { "value": [
# { "ProviderName": "keycloak-corp", "ProviderSubject": "...", "AutoLinked": true, "LinkedAt": "..." },
# { "ProviderName": "bad-provider", "ProviderSubject": "...", "AutoLinked": true, "LinkedAt": "..." }
# ] }
How to unlink a specific provider¶
# DELETE the bad ExternalLogin
curl -X DELETE \
-H "Authorization: Bearer $adminToken" \
"https://cesivi.example.com/_api/web/siteusers/getbyemail('alice@example.com')/externallogins('bad-provider')"
This removes the link but does not delete the CesiviUser. The user can still log in via
their remaining ExternalLogin entries.
Preventing future mistakes¶
- Turn
AutoLinkPolicyback toOffuntil the IDP config is correct. - Set
TrustedEmailProvidersto an explicit allowlist of providers whose email claims you trust. - Ensure every provider in the allowlist has
email_verifiedenforced server-side.
Bulk unlink / audit¶
To audit all auto-linked rows across the deployment:
curl -H "Authorization: Bearer $adminToken" \
"https://cesivi.example.com/_api/admin/federatedidentity/links?autoLinked=true"
Response is a paged list of all ExternalLogin rows where AutoLinked=true. Pipe through
jq to filter by provider name or link date.
Quick Upgrade Checklist¶
- [ ] Review current
Oidcconfig: single-object or already array? - [ ] Decide: keep synthetic
Name="oidc"or rename to something explicit? - [ ] If renaming: run the login-name migration script (Step 2 in Part 1)
- [ ] Add the new provider entry to the list
- [ ] Set callback URI in the new provider's IDP admin console
- [ ] Restart Cesivi; verify both provider buttons appear on the login page
- [ ] Test login via each provider; confirm user + ExternalLogin row created
- [ ] If enabling auto-link: set
TrustedEmailProvidersfirst; then setAutoLinkPolicy - [ ] Run backfill if needed (Part 2)
This upgrade guide covers the backwards-compatible config migration from single-slot to multi-provider. All changes are non-destructive. PLAN-1619.