Skip to content

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:

  1. Want to add a second provider to an existing deployment, OR
  2. Want to migrate existing users to the federated identity model, OR
  3. 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 UPDATE on the ExternalLogins table's ProviderName column and the CesiviUsers table's LoginName column. Adapt the script above to your SQL provider.

Adding a second provider

  1. Change the Oidc value to an array (if not already).
  2. Add the new provider entry. See Section 2 and 3 in MULTI-PROVIDER-IDP.md for Keycloak and Entra examples.
  3. Restart Cesivi.
  4. The login screen now shows a button (chip) per enabled provider.
  5. New users logging in via the new provider receive their own CesiviUser and an ExternalLogin row 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.


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.

# 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": "..." }
# ] }
# 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

  1. Turn AutoLinkPolicy back to Off until the IDP config is correct.
  2. Set TrustedEmailProviders to an explicit allowlist of providers whose email claims you trust.
  3. Ensure every provider in the allowlist has email_verified enforced server-side.

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 Oidc config: 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 TrustedEmailProviders first; then set AutoLinkPolicy
  • [ ] 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.