Skip to main content

Federation with OpenID Connect

The partner route opens on AccessOps · Partner sign-in as the walkthrough draws it: "Sign in to AccessOps", "Partner route · documents, approvals and records for your tenant", the blue Sign in with your work account button (C badge, arrow), "You sign in at your organisation. AccessOps never sees the password.", the divider SERVICE ACCOUNTS ONLY with the Module 4 password row underneath (account name, password, Sign in) and the bottom strip STATUS Ready … partner route · https. The button takes the tab to Contoso Identity · test provider (purple bar, "AccessOps is requesting" openid profile email, e-mail and password, Continue, then "Approve the sign-in on your phone" with Approve / Deny). Back in AccessOps the page load maps issuer + subject to a local account, creates one grant, rotates the session ID and shows AccessOps · Records: Dana Ellis, "Reader · Tenant A · signed in via Contoso Identity", the grid Record · Tenant · Class (Record 17 · Supplier contract, Record 22 · Rate card 2026, Record 31 · Onboarding checklist), Export record, Sign out, status "Signed in via Contoso Identity" and the note grant G-xxxx… · tenant from AccessOps. Every refused sign-in shows the orange "Sign-in did not complete. Try again or contact your administrator."

Solution AccessOps.slnx = AccessOps/ (net10.0, Wisej-4 4.1.7, Microsoft.Data.Sqlite 10.0.14, Microsoft.AspNetCore.Authentication.OpenIdConnect 10.0.14) + AccessOps.TestIdentityProvider/ (Contoso Identity, a test double - never deploy) + AccessOps.Tests/ (xUnit) + PolicyHarness/. Started from a copy of Module 4. Data in AccessOpsData/ (git-ignored). Module 6 starts from here.

Run it​

Two throwaway secrets, one value: the test provider's client secret and AccessOps' Oidc:ClientSecret. They live in user secrets (Development) or the environment - never in a tracked file.

cd "Security and Auth Patterns Course\Module 5"
$secret = "lab-only-" + [guid]::NewGuid().ToString("N")
dotnet user-secrets set "Clients:accessops:Secret" $secret --project AccessOps.TestIdentityProvider
dotnet user-secrets set "Oidc:ClientSecret" $secret --project AccessOps
dotnet build AccessOps.slnx

dotnet run --project AccessOps.TestIdentityProvider --urls http://localhost:6415 # terminal 1
dotnet run --project AccessOps --urls https://localhost:6405 # terminal 2
dotnet test AccessOps.slnx
dotnet run --project PolicyHarness

User secrets load only because AccessOps.csproj sets GenerateAssemblyInfo to true (the Wisej.NET template's false drops the UserSecretsId attribute). Without a secret AccessOps refuses to start ("Oidc:ClientSecret is not configured…"). Instead of user secrets you can set Oidc__ClientSecret and Clients__accessops__Secret in the environment.

Oidc in appsettings.json holds Authority (http://localhost:6415), ClientId (accessops), CallbackPath, Scopes and RequireHttpsMetadata: false - false only because the lab provider runs on plain HTTP; the handler's default (true) stays for any real provider. The provider registration (AccessOps.TestIdentityProvider/appsettings.json) has one exact callback, https://localhost:6405/signin-oidc. Behind a TLS-terminating proxy app.UseForwardedHeaders() (configured for Hosting:KnownProxies) keeps the https origin, or the handler would build an http:// redirect URI the provider refuses.

Browser that does not trust the dev certificate (as on the course machine): add the Development verification listener of Module 4 and register its callback at the provider for that run only:

$env:AccessOps__LocalHttpVerification = "true"; $env:AccessOps__AllowedOrigins__1 = "http://localhost:6445"
dotnet run --project AccessOps --urls "https://localhost:6405;http://localhost:6445"
# provider terminal:
$env:Clients__accessops__RedirectUris__1 = "http://localhost:6445/signin-oidc"
$env:Clients__accessops__PostLogoutRedirectUris__1 = "http://localhost:6445/signout-callback-oidc"
dotnet run --project AccessOps.TestIdentityProvider --urls http://localhost:6415

Demo accounts (invented, local only)​

WhereSign-inSecretMaps to
Contoso Identitydana.ellis@contoso.exampleDemo-IdP-2026Dana Ellis, tenant A, Reader (external_logins)
Contoso Identitykai.berg@northwind.exampleDemo-IdP-2026Kai Berg, tenant C - not approved (F08)
Contoso Identityjo.newman@contoso.exampleDemo-IdP-2026nobody (no external_logins row)
AccessOps, Service accounts onlysvc.reportsReports-demo-31Reports service, tenant A (Debug builds seed the hash)
AccessOps, Service accounts onlypartner.ana@fabrikam.testFabrikam-demo-17interactive account → generic failure

The provider's tenant contoso-lab is disposable: users come from Data/users.json, every session, code, token and signing key lives in memory, a restart wipes it.

What to try​

ActionExpected result
Sign in with your work account → Dana → Continue → ApproveAccessOps · Records, Dana Ellis, "Signed in via Contoso Identity"; log external sign-in completed … grants total=1 and RefreshSessionId() · rotated=True
Export recordrecord-A-17.txt, status "Record 17 exported", note "repository reads: 1"
F06: start an attempt in a second client, put its state into this tab's authorize URL at the provider, sign in and approve"Sign-in did not complete…", log reason=correlation, grants unchanged
F07: after a success, open http://localhost:6415/lab/replay-last-callback (test-provider helper: the same callback URL again)the console stays as it was; log reason=correlation; still one grant
F08: sign in as kai.berg@northwind.example, Approve"Sign-in did not complete…", log reason=unapproved_tenant, no grant
Provider denial: sign in, then Deny (or "Cancel and return to AccessOps")"Sign-in did not complete…", log reason=access_denied, no grant
Service accounts only: svc.reports / Reports-demo-31"Reports service · Reader · Tenant A · signed in with a service account"
Service accounts only: partner.ana@fabrikam.test / Fabrikam-demo-17"That user name and password combination is not valid." (audit NotAllowedHere)
Two tabs signed in as Dana; tab 1 Sign outtab 1 goes through the provider's end-session and returns: "You have signed out. Sign in to continue."; provider log endsession: IdP session … ended
Tab 2: Export record"Export denied · grant revoked", note "repository reads: 0"
Sign in againContoso Identity asks for the e-mail and password (the provider session is gone)

Where this sample differs from the lesson or video​

  • Lesson snippet vs. lab: the lesson's Startup.cs excerpt uses Scope.Add("openid"); Scope.Add("profile"); the lab asks for openid profile email, which the sample requests (Oidc:Scopes).
  • Response mode: the test provider answers with a query redirect, so ResponseMode is Query; the handler then sends no response_mode parameter at all.
  • Handler logging: Microsoft.AspNetCore.Authentication is logged at Error only: at Warning the handler writes the correlation cookie's name (it contains the correlation ID of the attempt). AccessOps writes its own allowlisted reason instead.
  • F07 helper: /lab/replay-last-callback is a test-double convenience on the provider, not part of OpenID Connect - it replays the browser's last callback URL so the lab can show the refusal.
  • Replayed callback in a signed-in tab: the console stays (nothing changes); in a signed-out tab the sign-in view shows the fixed sentence.
  • Status texts follow the run scene ("Signed in via Contoso Identity", "Export denied · grant revoked", "repository reads: 0"); "Redirecting to Contoso Identity" is shown only for the moment before the navigation.
  • Mock's note line: grant g-…7c1; the app shows the real shortened grant ID (G-xxxx…).
  • Window: the app fills the browser; the mock is a fixed-size window. Grid selection is grey (Wisej Bootstrap-4 theme).
  • The provider's page is the test double's own HTML (the "Contoso Identity" mock, with the Approve/Deny second factor and a "Cancel and return to AccessOps" link); the scopes, colours and texts follow the mock.