toolup-forgetoolup-forge

Migration — 0.4.0 OidcAppConfig (unified one-declaration config)

Migration — 0.4.0 OidcAppConfig (unified one-declaration config)

Status. Breaking — ships on the 0.4.0 coordinated minor bump alongside the re-applied Phase 69a Remoting sweep. Consumers must update their preset call sites + (eventually) their ClientConfig/ServerConfig wiring. The 0.3.x advisory at 0.3.x-oidc-presets.md introduced the preset shape additively; 0.4.0 collapses the preset return type onto the new unified record.

What changed

A new package ToolUp.AuthProviders.Oidc.Shared (NuGet ToolUp.AuthProviders.Oidc.Shared) ships at 0.4.0, owning the cross-side OidcAppConfig record and the PresetKind provenance DU. Existing companion packages (ToolUp.AuthProviders.Oidc.Client, ToolUp.AuthProviders.Oidc server) gain a transitive dependency on the new package.

Breaking surface changes

0.3.x 0.4.0
OidcPresets.entraWorkforce tenantId clientId redirectUri : OidcUIConfig * PresetMetadata OidcPresets.entraWorkforce tenantId clientId redirectUri : OidcAppConfig
OidcPresets.entraExternalId tenantSubdomain clientId redirectUri : OidcUIConfig * PresetMetadata OidcPresets.entraExternalId tenantSubdomain clientId redirectUri : OidcAppConfig
OidcPresets.entraExternalIdWithDomain ... : OidcUIConfig * PresetMetadata OidcPresets.entraExternalIdWithDomain ... : OidcAppConfig
OidcPresets.auth0 domain clientId redirectUri : OidcUIConfig * PresetMetadata OidcPresets.auth0 ... : OidcAppConfig
OidcPresets.generic issuer clientId redirectUri : OidcUIConfig * PresetMetadata OidcPresets.generic ... : OidcAppConfig
OidcCoherenceValidator(cfg: OidcUIConfig, presetMeta: PresetMetadata option, ?timeout) OidcCoherenceValidator(cfg: OidcAppConfig, ?timeout)
OidcCoherenceValidator.evaluate cfg presetMeta OidcCoherenceValidator.evaluate cfg
PresetMetadata record (Name / IssuerForm / AutoAddedScopes / ExpectsDecodableAccessToken / Notes) Deleted. Same data derived from PresetKind via OidcAppConfig.PresetKind.label / .issuerForm / .autoAddedScopes / .expectsDecodableAccessToken / .notes.

Non-breaking

  • OidcUIConfig (in ToolUp.Platform) is preserved unchanged. It remains the client-tier shape that OidcAuthUI.OidcShell and OidcClient.handleCallback consume internally. Consumers writing OidcAppConfig never see it — the SDK projects via OidcAppConfig.toClientConfig.
  • Existing handler signatures (OidcAuthUI.OidcShell cfg, OidcClient.beginSignIn cfg, etc.) still take OidcUIConfig. A subsequent change folds them onto OidcAppConfig directly; until then the projection bridges.

The OidcAppConfig record

type OidcAppConfig = {
    Issuer: string                 // base for OIDC discovery
    Audience: string               // access-token `aud` the server binds against
    ClientId: string               // app-registration client id
    Scopes: string list            // includes preset-auto-added entries
    RedirectUri: string            // registered callback URL
    PostLogoutRedirectUri: string option
    ValidateIdToken: bool option   // None = off; Some true = full pipeline
    Preset: PresetKind option      // provenance for the validator + tracer
}

Audience is a new field at 0.4.0. Preset constructors default it to ClientId (the workforce-Entra / External-ID case). Auth0 deployments using a configured API audience override it after construction:

let cfg =
    { OidcPresets.auth0 "my-tenant.auth0.com" clientId redirectUri with
        Audience = "https://api.yourapp.com" }   // matches Auth0 dashboard "Audience" field

Worked migration — workforce Entra consumer

Pre-0.4.0 (with 0.3.x-oidc-presets.md advisory adoption):

let oidcCfg, meta =
    OidcPresets.entraWorkforce entraTenantId entraClientId redirectUri

ClientConfig.compose
    {| ... AuthUI = OidcAuthUI oidcCfg ... |}

0.4.0:

let oidcCfg : OidcAppConfig =
    OidcPresets.entraWorkforce entraTenantId entraClientId redirectUri
// metadata is on cfg.Preset; OidcAppConfig.PresetKind.notes EntraWorkforce gives the hints

ClientConfig.compose
    {| ... AuthUI = OidcAuthUI (OidcAppConfig.toClientConfig oidcCfg) ... |}
//                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ projection until OidcAuthUI takes OidcAppConfig directly

The single-line change is dropping the _ from the tuple destructurelet oidcCfg, _ = ... becomes let oidcCfg = .... The projection at the OidcAuthUI call site is a one-cycle bridge: a follow-up flips OidcAuthUI's parameter type to OidcAppConfig and the projection disappears.

Worked migration — OidcCoherenceValidator consumer

// 0.3.x
let v = OidcCoherenceValidator(oidcUIConfig, Some presetMeta)
ServerApp.withConfigValidator v ...

// 0.4.0
let v = OidcCoherenceValidator(oidcAppConfig)   // preset on the config itself
ServerApp.withConfigValidator v ...

The validator's rule set is unchanged (same 11 rules — emptiness, OIDC-spec, HTTPS, preset-vs-issuer paste-mistakes, preset-AutoAddedScopes dropped, preset provenance). The data shape is denser.

Worked migration — PresetMetadata introspection

Consumers reading metadata fields directly (rare):

// 0.3.x
let _, meta = OidcPresets.entraWorkforce tid cid uri
printfn "applied: %s — %s" meta.Name meta.IssuerForm
for note in meta.Notes do printfn "  - %s" note

// 0.4.0
let cfg = OidcPresets.entraWorkforce tid cid uri
match cfg.Preset with
| Some kind ->
    printfn "applied: %s — %s" (PresetKind.label kind) (PresetKind.issuerForm kind)
    for note in PresetKind.notes kind do printfn "  - %s" note
| None -> ()

Verification

  1. dotnet buildOidcAppConfig is a new type the compiler will flag at every mismatched call site. The 0.4.0 break is intentionally loud.
  2. Test packOidcPresetsTests (34 cases) + OidcCoherenceValidatorTests (30 cases) are reshaped to construct OidcAppConfig values. If the consumer's own tests construct OidcUIConfig * PresetMetadata tuples, they break the same way.
  3. Smoke-test sign-in — confirm [auth] <corr> begin-sign-in appears in the browser console with TOOLUP_AUTH_TRACE enabled, followed by token-exchange-ok and transition:established. The transitions all wire through the same handler surface as 0.3.x.

Rollback

Pin <ToolUpSdkVersion> back to 0.3.8 in Directory.Packages.props. The new ToolUp.AuthProviders.Oidc.Shared package is opt-in via transitive reference — pinning the SDK below 0.4.0 keeps consumers off it.

Coordinated change at 0.4.0

0.4.0 ships:

  • Phase 69a re-application — forge SDK depends on ToolUp.Remoting 0.1.15+ (transitively). External consumers reach for ToolUp.Remoting the same way they reach for any other public ToolUp.* package family member.
  • OidcAppConfig unification (this migration).
  • EntraExternalId companion deprecation (separate migration doc, in progress).
  • Continued deprecation warnings (FS0044) for FableJsonConverter — replaced by ToolUp.Remoting.Json.SystemTextJson.FableConverters.create() per Phase 69 design. Non-blocking; consumer migration to the new converter is tracked separately.

Companion-side notes

EntraExternalIdClient (ToolUp.AuthProviders.EntraExternalId.Client) is marked deprecated at 0.4.0 in favour of OidcPresets.entraExternalId + OidcPresets.entraExternalIdWithDomain. The companion code stays compiling for one minor cycle (consumer migration window); removal lands at 0.Y.0. See the separate 0.4.0-entra-external-id-deprecation.md migration doc.