Identityserver Ui Flows

作者 DuendeSoftwarefb32edc51982無授權條款9 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫4 週前更新

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.

AI 產生的概覽

指導建置 Duende IdentityServer 的登入、登出、同意、錯誤及聯合閘道 UI 頁面。

功能
此技能為 Duende IdentityServer 面向使用者的頁面提供實作指引,涵蓋登入、登出、同意、錯誤與註冊頁面。它說明 UI 程式碼如何透過 IIdentityServerInteractionService 與協定引擎互動,包括取得授權內容、建立工作階段以及驗證返回 URL。內容也涉及外部身分提供者整合、用戶端登出通知,以及搭配 Home Realm Discovery 的聯合閘道模式。產出為參考指引與程式碼範例,而非產生的檔案。
適用情境
適用於自訂或建置 IdentityServer 的登入、登出、同意、錯誤或註冊頁面。也適合外部提供者聯合、Home Realm Discovery 或用戶端登出通知策略相關工作。
執行需求
未隨附指令碼,僅為說明性指引。內容以使用 Duende IdentityServer 的 ASP.NET Core 應用為對象,程式碼範例為 C#。

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

PagePurposeDefault URL
LoginEstablish authentication sessionInferred from cookie handler LoginPath
LogoutTerminate session, notify clientsSet via opt.UserInteraction.LogoutUrl
ConsentGrant/deny client access to resources/consent
ErrorDisplay protocol error information/home/error

Login Page

Configuring the Login URL

csharp
// Program.cs — explicit configurationbuilder.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 Identitybuilder.Services.AddIdentityServer()    .AddAspNetIdentity<ApplicationUser>();
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<IActionResult> 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 endpointreturn 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

ClaimPurposeDefault
subRequired. Unique user identifier. Must never change or be reassigned.None — you must provide it
nameDisplay name of the userNone
amrAuthentication method referencepwd
auth_timeTime user entered credentials (epoch)Current time
idpIdentity provider scheme namelocal
tenantTenant identifierNone

Protecting Against Open Redirects

Always validate the returnUrl before redirecting:

csharp
// Option 1: Use ASP.NET Core helperif (Url.IsLocalUrl(returnUrl)){    return Redirect(returnUrl);}
// Option 2: Use IdentityServer interaction serviceif (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.csbuilder.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 cookievar 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 uservar user = FindUserFromExternalProvider(scheme, userId);
// 3. Establish sessionawait HttpContext.SignInAsync(new IdentityServerUser(user.SubjectId){    DisplayName = user.DisplayName,    IdentityProvider = scheme});
// 4. Clean up external cookieawait HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// 5. Return to protocol processingreturn Redirect(returnUrl);

SignInScheme and SignOutScheme

ScenarioSignInSchemeSignOutScheme
Without ASP.NET IdentityIdentityServerConstants.ExternalCookieAuthenticationSchemeIdentityServerConstants.SignoutScheme
With ASP.NET IdentityIdentityServerConstants.ExternalCookieAuthenticationSchemeIdentityConstants.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 statebuilder.Services.AddOidcStateDataFormatterCache();
// Or specific schemes onlybuilder.Services.AddOidcStateDataFormatterCache("aad", "demoidsrv");

Logout Page

Configuring the Logout URL

csharp
// Program.csbuilder.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

MechanismHow It WorksClient Setting
Front-channelRender <iframe> on logged-out page pointing to client's logout URIFrontChannelLogoutUri
Back-channelServer-to-server HTTP call with a logout JWT (typ: logout+jwt)BackChannelLogoutUri
JS-basedClient monitors check_session_iframeBuilt into spec-compliant JS clients

Recommendation: Use back-channel notifications for cross-site architectures. Front-channel and JS-based notifications rely on cookies in iframes, which may not work reliably across different sites.

Getting Logout Context

csharp
var context = await _interaction.GetLogoutContextAsync(logoutId);
// context.SignOutIFrameUrl — render in <iframe> for front-channel logout// context.PostLogoutRedirectUri — where to send the user after logout

Back-Channel Logout

Back-channel logout happens automatically when you call HttpContext.SignOutAsync() — IdentityServer uses IBackChannelLogoutService to notify all clients that have BackChannelLogoutUri configured.

For .NET clients: use the BFF framework which has built-in back-channel logout support, or see the IdentityServer samples.

Consent Page

When Consent Is Required

Consent applies only to user-based (interactive) authorization requests. Client-credentials (M2M) flows never prompt for consent — there Client.AllowedScopes alone governs access.

Consent is controlled per client via RequireConsent (default: false). Set RequireConsent = false for first-party clients to suppress the scope prompt; set true for third-party clients. When enabled, IdentityServer redirects to the consent page before completing authorization.

The offline_access scope always triggers consent when the client has consent enabled.

Required vs. Optional Scopes

IdentityResource and ApiScope expose a Required bool:

  • If the consent response omits a Required scope, IdentityServer returns access_denied and the request fails.
  • Optional scopes can be declined and the flow still succeeds — the issued tokens/userinfo simply omit that data.
csharp
new IdentityResource("profile", /* ... */) { Required = true }; // cannot be declinednew ApiScope("api.read") { Required = false };                  // may be declined

Remembered Consent

Enable persistence with Client.AllowRememberConsent (bool) and Client.ConsentLifetime (expiry). Granted scopes are stored in the operational (persisted grant) store; set RememberConsent = true on the ConsentResponse to persist a grant.

IdentityServer re-prompts for consent when:

  • there is no remembered consent, or it has expired,
  • a new, not-previously-granted scope is requested,
  • the request includes offline_access,
  • the request contains a parameterized scope value, or
  • AllowRememberConsent = false.

Device flow: in IdentityServer v8, device-flow (Device Authorization Grant) consent is never remembered — the user consents on every device authorization.

Revoking Consent

Use IIdentityServerInteractionService.RevokeUserConsentAsync(clientId) for the current user. This removes all persisted grants for that user/client — remembered consent, reference tokens, and refresh tokens.

csharp
await _interaction.RevokeUserConsentAsync("web.app");

Consent Page Flow

csharp
// 1. Get authorization contextvar context = await _interaction.GetAuthorizationContextAsync(returnUrl);
// 2. Show user: client info, requested scopes/resources// context.Client — the requesting client// Use IClientStore and IResourceStore for additional details
// 3. User grants or denies consentawait _interaction.GrantConsentAsync(context, new ConsentResponse{    ScopesValuesConsented = new[] { "openid", "profile", "api1" },    RememberConsent = true  // persist for future requests});
// 4. Redirect backreturn Redirect(returnUrl);

Denying Consent

csharp
await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied);

Validating returnUrl

csharp
// Use interaction serviceif (await _interaction.IsValidReturnUrl(returnUrl)){    return Redirect(returnUrl);}// Or check if GetAuthorizationContextAsync returns non-null

User Registration (prompt=create)

The prompt=create OIDC parameter sends the user straight to a registration page instead of login.

Host Configuration

Set CreateAccountUrl in AddIdentityServer. This makes IdentityServer advertise create in prompt_values_supported in discovery. If unset, prompt=create is ignored and not advertised.

csharp
// Program.csbuilder.Services.AddIdentityServer(options =>{    options.UserInteraction.CreateAccountUrl = "/Account/Register";});

prompt=create must be the only prompt value — it cannot be combined with login, consent, select_account, or none.

Triggering Registration from an ASP.NET Core Client

csharp
return Results.Challenge(    new OpenIdConnectChallengeProperties { Prompt = "create", RedirectUri = "/" },    ["oidc"]);

Registration Page Handler (Host)

csharp
public async Task<IActionResult> OnPost(string returnUrl, CancellationToken ct){    // Returns null for an invalid returnUrl → prevents open redirect    var context = await _interaction.GetAuthorizationContextAsync(returnUrl, ct);    if (context is null) return Redirect("~/");
    // Create + persist the local user    var user = await _users.CreateAsync(/* ... */);
    // Sign in ONLY after email confirmation / approval / MFA — not on submit    await HttpContext.SignInAsync(new IdentityServerUser(user.SubjectId));
    return Redirect(returnUrl);}

Important: GetAuthorizationContextAsync returning null signals an invalid returnUrl — redirect away instead of trusting it. Establish the session only after any required confirmation/approval/MFA step.

Error Page

Configuration

csharp
// Program.csbuilder.Services.AddIdentityServer(opt => {    opt.UserInteraction.ErrorUrl = "/path/to/error";    opt.UserInteraction.ErrorId = "ErrorQueryStringParamName"; // default: "errorId"});

Retrieving Error Details

csharp
var errorContext = await _interaction.GetErrorContextAsync(errorId);
// errorContext contains:// - Error (error code)// - ErrorDescription// - RequestId// - ClientId// - DisplayMode// - UiLocales

Errors are commonly due to misconfiguration. The error page should inform the user something went wrong without exposing sensitive details.

Federation Gateway and Home Realm Discovery

What Is a Federation Gateway?

A federation gateway architecture shields clients from authentication complexity. Clients trust only IdentityServer; the gateway coordinates with external providers, handling protocol bridging (OIDC, SAML, WS-Fed), claim transformation, and trust management.

Home Realm Discovery (HRD) Strategies

StrategyDescriptionBest For
Show all providersPresent a list of available authentication methodsSimple setups with few providers
Email/identifier-basedAsk for email, infer provider from domainSaaS with corporate federation
Client hint via acr_valuesClient passes idp:provider_nameKnown provider per client/URL
IdentityProviderRestrictionsRestrict available providers per clientMulti-tenant with per-client providers

Restricting Providers Per Client

csharp
var client = new Client{    ClientId = "tenant_a_app",    IdentityProviderRestrictions = { "AAD", "local" }    // Only Azure AD and local login are available};

HRD via acr_values

Clients can hint at the desired provider:

GET /connect/authorize?    client_id=app&    acr_values=idp:AAD&    ...

Your login page checks context.IdP from GetAuthorizationContextAsync and can skip the login UI entirely, redirecting straight to the external provider.

Common Anti-Patterns

  • ❌ Parsing returnUrl manually to extract authorization parameters

  • ✅ Use IIdentityServerInteractionService.GetAuthorizationContextAsync(returnUrl)

  • ❌ Redirecting to returnUrl without validation, enabling open redirect attacks

  • ✅ Validate with Url.IsLocalUrl() or _interaction.IsValidReturnUrl()

  • ❌ Forgetting to delete the external authentication cookie after callback processing

  • ✅ Always call HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme)

  • ❌ Using front-channel logout across different sites/domains (cookie/iframe issues)

  • ✅ Use back-channel logout for cross-site architectures

  • ❌ Issuing the authentication session without a sub claim

  • ✅ The sub claim is required — it uniquely identifies the user and must never change

  • ❌ Hardcoding external provider list without checking both static schemes and dynamic providers

  • ✅ Query IAuthenticationSchemeProvider for static schemes and IIdentityProviderStore for dynamic providers

Common Pitfalls

  1. Login page does not preserve returnUrl: The returnUrl must survive across all page transitions (post-backs, external redirects, MFA steps). Store it in hidden form fields, route data, or the AuthenticationProperties.Items dictionary.

  2. Cookie handler LoginPath mismatch: If no explicit LoginUrl is configured, IdentityServer infers it from the cookie handler's LoginPath. Make sure the cookie handler LoginPath matches your actual login page route. The LogoutUrl is not inferred from the cookie handler — it must always be set explicitly via opt.UserInteraction.LogoutUrl.

  3. SignOutScheme differs with ASP.NET Identity: When using ASP.NET Identity, the SignOutScheme for external providers should be IdentityConstants.ApplicationScheme, not IdentityServerConstants.SignoutScheme.

  4. Consent persistence is temporary by default: The consent result between the consent page and authorization endpoint is stored in a cookie. For custom persistence, implement IConsentMessageStore.

  5. Error messages are deliberately brief: For security, error messages returned to clients are minimal. Check the IdentityServer logs (at Debug level) for full error details.

  6. External provider sub is provider-specific: The sub claim from an external provider is that provider's unique ID. Map it to your local user database — do not use it directly as the IdentityServer sub.

來源與署名

來源:DuendeSoftware/duende-skills位於skills/identityserver-ui-flows提交fb32edc

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架