Authentication is one of the areas where a migration cannot rely on visual parity. A login page that looks the same may still produce different claims, cookies, role checks, and session behaviour.
Separate external identity from local application identity
Azure AD or another OIDC provider proves who the user is externally. The application still needs a local user record, local roles, local employee or organisation associations, and an application cookie.
That distinction is important. External login is a step in the flow, not a replacement for application authorisation.
The callback is the boundary
The external flow is easiest to reason about as a sequence:
challenge provider
-> provider callback
-> validate state and claims
-> find or create local user
-> resolve roles and application data
-> issue local application cookie
-> create or refresh active session
Cookie SameSite and Secure settings, session state, callback paths, and middleware order all participate in this flow. A failure that says “invalid state” may be a cookie or session problem rather than an Identity problem.
Development login is a guarded substitute
Local development needs a way to work without production identity infrastructure. A development login can create or find a local user and assign baseline roles, but it must be protected by both environment and configuration checks.
It should still use the normal sign-in path where possible. That keeps the development cookie and claims closer to the deployed behaviour and makes tests more representative.
Sessions are a second state system
The authentication cookie answers “who is this user?” Session state answers “what short-lived server-side state belongs to this browser?” Active-user tracking adds operational information about recent use. These are related but not interchangeable.
The migration became safer when those responsibilities were explicit and their expiry, cleanup, and diagnostics were documented together.
The lesson
Treat authentication as a protocol and state-machine migration, not a form rewrite. Preserve the security invariants, make the callback sequence visible, and test both successful and incomplete flows.