--- name: identityserver-ui-flows description: "Guide for building login, logout, consent, error, and federation gateway UI pages in Duende IdentityServer, including IIdentityServerInteractionService usage, external provider integration, and Home Realm Discovery strategies." invocable: false --- # IdentityServer UI Flows: Login, Logout, Consent, and Federation ## When to Use This Skill - Building or customizing the login page (local credentials, MFA, passwordless) - Integrating external identity providers (Google, Azure AD, SAML, etc.) - Implementing the consent page for third-party client authorization - Building the logout flow with session cleanup and client notifications - Implementing a federation gateway with Home Realm Discovery (HRD) - Handling and displaying error pages for protocol errors - Using `IIdentityServerInteractionService` to interact with the protocol engine - Redirecting users back to clients after login/logout Docs: https://docs.duendesoftware.com/identityserver/ui ## Architecture Overview IdentityServer separates the protocol engine from the user interface. The engine handles OAuth/OIDC endpoints and redirects to your UI pages as needed. Your UI code handles all user interaction and then communicates results back to the engine. ``` Browser → IdentityServer Middleware → UI Pages (Login, Consent, Logout, Error) ↕ IIdentityServerInteractionService ↕ IdentityServer Protocol Engine ``` ### Required Pages | Page | Purpose | Default URL | | ------- | ------------------------------------- | ----------------------------------------- | | Login | Establish authentication session | Inferred from cookie handler `LoginPath` | | Logout | Terminate session, notify clients | Set via `opt.UserInteraction.LogoutUrl` | | Consent | Grant/deny client access to resources | `/consent` | | Error | Display protocol error information | `/home/error` | ## Login Page ### Configuring the Login URL ```csharp // Program.cs — explicit configuration builder.Services.AddIdentityServer(opt => { opt.UserInteraction.LoginUrl = "/path/to/login"; }); ``` If not set, IdentityServer infers the URL from the cookie handler's `LoginPath`: ```csharp // Program.cs — with ASP.NET Identity builder.Services.AddIdentityServer() .AddAspNetIdentity(); builder.Services.ConfigureApplicationCookie(options => { options.LoginPath = "/path/to/login/for/aspnet_identity"; }); ``` ### Authorization Context When IdentityServer redirects to the login page, it passes a `returnUrl` query parameter. Use `IIdentityServerInteractionService.GetAuthorizationContextAsync` to extract the original authorization request parameters: ```csharp public class LoginModel : PageModel { private readonly IIdentityServerInteractionService _interaction; public LoginModel(IIdentityServerInteractionService interaction) { _interaction = interaction; } public async Task OnGet(string returnUrl) { var context = await _interaction.GetAuthorizationContextAsync(returnUrl); // context contains: // - Client (the requesting client) // - IdP (requested identity provider hint) // - AcrValues (requested authentication context) // - Tenant (requested tenant) // - LoginHint (suggested username) // - Parameters (raw protocol parameters) // Use context for branding, HRD, MFA decisions, etc. } } ``` **Important**: Do not parse the `returnUrl` yourself. Always use the interaction service. ### Establishing the Authentication Session After validating credentials, create the authentication session: ```csharp var user = new IdentityServerUser("unique_id_for_your_user") { DisplayName = "Bob Smith" }; await HttpContext.SignInAsync(user); // Redirect back to the authorization endpoint return Redirect(returnUrl); ``` Or with explicit claims: ```csharp var claims = new Claim[] { new Claim("sub", "unique_id_for_your_user"), new Claim("name", "Bob Smith"), new Claim("amr", "pwd"), new Claim("idp", "local") }; var identity = new ClaimsIdentity(claims, "pwd"); var principal = new ClaimsPrincipal(identity); await HttpContext.SignInAsync(principal); ``` ### Well-Known Session Claims | Claim | Purpose | Default | | ----------- | ------------------------------------------------------------------------- | -------------------------- | | `sub` | **Required.** Unique user identifier. Must never change or be reassigned. | None — you must provide it | | `name` | Display name of the user | None | | `amr` | Authentication method reference | `pwd` | | `auth_time` | Time user entered credentials (epoch) | Current time | | `idp` | Identity provider scheme name | `local` | | `tenant` | Tenant identifier | None | ### Protecting Against Open Redirects Always validate the `returnUrl` before redirecting: ```csharp // Option 1: Use ASP.NET Core helper if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } // Option 2: Use IdentityServer interaction service if (await _interaction.IsValidReturnUrl(returnUrl)) { return Redirect(returnUrl); } ``` ### Completing Login with CompleteLoginAsync After establishing the authentication session, redirect the user back to the `returnUrl`. This causes the browser to re-issue the original authorize request, allowing IdentityServer to complete the protocol workflow. ## External Login (Federation) ### Registering External Providers ```csharp // Program.cs builder.Services.AddIdentityServer(); builder.Services.AddAuthentication() .AddOpenIdConnect("AAD", "Employee Login", options => { options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; // configure authority, client ID, etc. }); ``` ### Triggering External Authentication ```csharp var callbackUrl = Url.Action("MyCallback"); var props = new AuthenticationProperties { RedirectUri = callbackUrl, Items = { { "scheme", "AAD" }, { "returnUrl", returnUrl } } }; return Challenge("AAD", props); ``` ### Handling the Callback ```csharp // 1. Read external identity from temporary cookie var result = await HttpContext.AuthenticateAsync( IdentityServerConstants.ExternalCookieAuthenticationScheme); if (result?.Succeeded != true) throw new Exception("External authentication error"); var externalUser = result.Principal; var userId = externalUser.FindFirst("sub").Value; var scheme = result.Properties.Items["scheme"]; var returnUrl = result.Properties.Items["returnUrl"] ?? "~/"; // 2. Find or provision local user var user = FindUserFromExternalProvider(scheme, userId); // 3. Establish session await HttpContext.SignInAsync(new IdentityServerUser(user.SubjectId) { DisplayName = user.DisplayName, IdentityProvider = scheme }); // 4. Clean up external cookie await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme); // 5. Return to protocol processing return Redirect(returnUrl); ``` ### SignInScheme and SignOutScheme | Scenario | SignInScheme | SignOutScheme | | ------------------------ | ------------------------------------------------------------ | --------------------------------------- | | Without ASP.NET Identity | `IdentityServerConstants.ExternalCookieAuthenticationScheme` | `IdentityServerConstants.SignoutScheme` | | With ASP.NET Identity | `IdentityServerConstants.ExternalCookieAuthenticationScheme` | `IdentityConstants.ApplicationScheme` | ### State and URL Length If external provider state makes the URL too long (>2000 chars), use the IdentityServer-provided `IDistributedCache`-backed data format: ```csharp // Program.cs — all OIDC handlers use server-side state builder.Services.AddOidcStateDataFormatterCache(); // Or specific schemes only builder.Services.AddOidcStateDataFormatterCache("aad", "demoidsrv"); ``` ## Logout Page ### Configuring the Logout URL ```csharp // Program.cs builder.Services.AddIdentityServer(opt => { opt.UserInteraction.LogoutUrl = "/path/to/logout"; }); ``` ### Logout Steps 1. **End the IdentityServer session** — remove the authentication cookie 2. **Sign out of external provider** — if an external login was used 3. **Notify client applications** — via front-channel, back-channel, or JS-based notifications 4. **Redirect back to client** — if the logout is client-initiated ### Client Notification Mechanisms | Mechanism | How It Works | Client Setting | | ------------- | -------------------------------------------------------------------- | ------------------------------------ | | Front-channel | Render `