在 ASP.NET Core 中配置 OpenID Connect Web (UI) 身份验证

作者:Damien Bowden

查看或下载示例代码

本文涵盖以下几个方面:

  • 什么是 OpenID Connect 机密交互式客户端
  • 在 ASP.NET Core 中创建 OpenID Connect 客户端
  • 包含代码片段的 OpenID Connect 客户端示例
  • 使用第三方 OpenID Connect 服务提供商客户端
  • 用于前端的后端 (BFF) 安全体系结构
  • 高级功能、标准、扩展 OpenID Connect 客户端

有关使用适用于 .NET的 Microsoft 身份验证库、Microsoft Web以及 Microsoft Entra ID的替代体验,请参阅 快速入门:登录用户并从 ASP.NET Core Web 应用(Azure 文档)调用 Microsoft 图形 API。

有关使用 Microsoft Entra 外部 ID OIDC 服务器的示例,请参阅以下内容:如何在外部租户中让用户登录到示例 ASP.NET Core Web 应用,以及使用 Microsoft Identity Web 对用户进行身份验证的 ASP.NET Core Web 应用

什么是 OpenID Connect 机密交互式客户端

OpenID Connect 可用于在 ASP.NET Core 应用程序中实现身份验证。 建议的方法是使用代码流使用 OpenID Connect 机密客户端。 对于此实现,建议使用 OAuth 公共客户端(PKCE)的代码交换证明密钥。 应用程序客户端和应用程序的用户都在机密流中进行身份验证。 应用程序客户端使用客户端密码或客户端断言进行身份验证。

不再建议对 Web 应用程序使用公共 OpenID Connect/OAuth 客户端。

默认流的工作方式如下图中所示:

使用 PKCE 的 OIDC 代码流机密客户端

OpenID Connect 具有许多变体,所有服务器实现的参数和要求略有不同。 某些服务器不支持用户信息终结点,有些服务器仍不支持 PKCE,而另一些服务器在令牌请求中需要特殊参数。 可以使用客户端断言,而不是客户端密钥。 此外,还存在新的标准,在 OpenID Connect Core 的基础上添加额外的安全性,例如 FAPI、CIBA 或 DPoP,用于下游 API。

注意

从 .NET 9 开始, 如果 OpenID Connect 服务器支持,则默认使用 OAuth 2.0 推送授权请求 (PAR) RFC 9126 。 这是三个步骤流,而不是上面所示的两个步骤流。 (用户信息请求是可选步骤。

使用 Razor Pages 创建 Open ID Connect 代码流客户端

以下部分演示如何在空的 ASP.NET Core Razor 页面项目中实现 OpenID Connect 客户端。 同一逻辑可以应用于任何 ASP.NET Core Web 项目,只有 UI 集成不同。

添加 OpenID Connect 支持

Microsoft.AspNetCore.Authentication.OpenIdConnect Nuget 包添加到 ASP.NET Core 项目。

设置 OpenID Connect 客户端

使用 Program.cs 文件中的 builder.Services 将身份验证添加到 Web 应用程序。 配置依赖于 OpenID Connect 服务器。 每个 OpenID Connect 服务器在设置中都需要细微的差异。

OpenID Connect 处理程序用于质询和注销。 cookie 用于处理 Web 应用程序中的会话。 可以根据需要指定身份验证的默认方案。

有关详细信息,请参阅 ASP.NET Core authentication-handler 指南

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
    var oidcConfig = builder.Configuration.GetSection("OpenIDConnectSettings");

    options.Authority = oidcConfig["Authority"];
    options.ClientId = oidcConfig["ClientId"];
    options.ClientSecret = oidcConfig["ClientSecret"];

    options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.ResponseType = OpenIdConnectResponseType.Code;

    options.SaveTokens = true;
    options.GetClaimsFromUserInfoEndpoint = true;

    options.MapInboundClaims = false;
    options.TokenValidationParameters.NameClaimType = JwtRegisteredClaimNames.Name;
    options.TokenValidationParameters.RoleClaimType = "roles";
});

有关不同 OpenID Connect 选项的详细信息,请参阅 Blazor Web App保护 ASP.NET 核心

有关不同的声明映射可能性,请参阅 映射、自定义和转换 ASP.NET Core中的声明。

注意

需要以下命名空间:

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;

设置配置属性

将 OpenID Connect 客户端设置添加到应用程序配置属性。 这些设置必须与 OpenID Connect 服务器中的客户端配置匹配。 在可能意外签入机密的应用程序设置中,不应保留任何机密。 机密应存储在安全位置(如 Azure 密钥库)的生产环境中或开发环境中的用户机密中。 有关详细信息,请参阅在 ASP.NET Core 开发中安全存储应用机密

"OpenIDConnectSettings": {
  // OpenID Connect URL. (The base URL for the /.well-known/openid-configuration)
  "Authority": "<Authority>",
  // client ID from the OpenID Connect server
  "ClientId": "<Client ID>",
  //"ClientSecret": "--stored-in-user-secrets-or-key-vault--"
},

注销回调路径配置

SignedOutCallbackPath(配置键:“SignedOutCallbackPath”)是 OpenID Connect 处理程序拦截的应用基路径中的请求路径,用户代理在从标识提供者注销后首先返回该路径。 示例应用不会为路径设置值,因为使用了默认值“/signout-callback-oidc”。 截获请求后,OpenID Connect 处理程序会重定向到 SignedOutRedirectUriRedirectUri(如果指定)。

在应用的 OIDC 提供程序注册中配置用户登出回调路径。 在以下示例中,{PORT} 占位符是应用的端口:

https://localhost:{PORT}/signout-callback-oidc

注意

使用 Microsoft Entra ID 时,请在 Entra 或 Azure 门户的 Web 平台配置的重定向 URI 条目中设置路径。 使用 Entra 时,localhost 地址不需要端口。 大多数其他 OIDC 提供程序都需要正确的端口。 如果您不将“签出回调路径 URI”添加到 Entra 中应用的注册信息,Entra 将拒绝将用户重定向回应用程序,并只会要求他们关闭浏览器窗口。

更新程序类中的 ASP.NET Core 管道方法。

UseRouting 方法必须在 UseAuthorization 方法之前实现。

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();
app.UseAuthentication();
// Authorization is applied for middleware after the UseAuthorization method
app.UseAuthorization();
app.MapRazorPages();

强制授权

[Authorize] 属性 添加到受保护的 Razor 页:

[Authorize]

更好的方法是强制授权整个应用,并选择退出不安全的页面:

var requireAuthPolicy = new AuthorizationPolicyBuilder()
    .RequireAuthenticatedUser()
    .Build();

builder.Services.AddAuthorizationBuilder()
    .SetFallbackPolicy(requireAuthPolicy);

通过将 [AllowAnonymous] 属性应用于公共终结点,选择退出公共终结点的授权。 有关示例,请参阅“向项目添加新Logout.cshtml页面”和SignedOut.cshtmlRazor“实现Login”页部分。

向项目添加新 Logout.cshtmlSignedOut.cshtmlRazor 页

为了要注销 cookie 会话和 OpenID Connect 会话,必须进行登录注销。 整个应用需要重定向到 OpenID Connect 服务器才能注销。成功注销后,应用将打开 RedirectUri 路由。

实现默认注销页,并将 Logout razor 页面代码更改为以下内容:

[Authorize]
public class LogoutModel : PageModel
{
    public IActionResult OnGetAsync()
    {
        return SignOut(new AuthenticationProperties
        {
            RedirectUri = "/SignedOut"
        },
        // Clear auth cookie
        CookieAuthenticationDefaults.AuthenticationScheme,
        // Redirect to OIDC provider signout endpoint
        OpenIdConnectDefaults.AuthenticationScheme);
    }
}

SignedOut.cshtml 需要 [AllowAnonymous] 属性

[AllowAnonymous]
public class SignedOutModel : PageModel
{
    public void OnGet()
    {
    }
}

实现 Login

还可以实现 LoginRazor 页,以使用所需的 AuthProperties 直接调用 ChallengeAsync。 如果 Web 应用需要身份验证并使用默认质询,则不需要这样做。

Login.cshtml 页需要 [AllowAnonymous] 属性

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorPageOidc.Pages;

[AllowAnonymous]
public class LoginModel : PageModel
{
    [BindProperty(SupportsGet = true)]
    public string? ReturnUrl { get; set; }

    public async Task OnGetAsync()
    {
        var properties = GetAuthProperties(ReturnUrl);
        await HttpContext.ChallengeAsync(properties);
    }

    private static AuthenticationProperties GetAuthProperties(string? returnUrl)
    {
        const string pathBase = "/";

        // Prevent open redirects.
        if (string.IsNullOrEmpty(returnUrl))
        {
            returnUrl = pathBase;
        }
        else if (!Uri.IsWellFormedUriString(returnUrl, UriKind.Relative))
        {
            returnUrl = new Uri(returnUrl, UriKind.Absolute).PathAndQuery;
        }
        else if (returnUrl[0] != '/')
        {
            returnUrl = $"{pathBase}{returnUrl}";
        }

        return new AuthenticationProperties { RedirectUri = returnUrl };
    }
}

为用户添加登录和注销按钮

@if (Context.User.Identity!.IsAuthenticated)
{
	<li class="nav-item">
		<a class="nav-link text-dark" asp-area="" asp-page="/Logout">Logout</a>
	</li>

	<span class="nav-link text-dark">Hi @Context.User.Identity.Name</span>
}
else
{
	<li class="nav-item">
		<a class="nav-link text-dark" asp-area="" asp-page="/Index">Login</a>
	</li>
}

带有代码片段的示例

使用用户信息终结点的示例

OpenID Connect 选项可用于映射声明、实现处理程序,甚至将令牌保存在会话中供以后使用。

Scope 选项可用于请求不同的声明或刷新令牌,作为信息发送到 OpenID Connect 服务器。 请求 offline_access 请求服务器返回一个引用令牌,该令牌可用于刷新会话,而无需再次对应用程序的用户进行身份验证。

services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    var oidcConfig = builder.Configuration.GetSection("OpenIDConnectSettings");
    options.Authority = oidcConfig["IdentityProviderUrl"];
    options.ClientSecret = oidcConfig["ClientSecret"];
    options.ClientId = oidcConfig["Audience"];
    options.ResponseType = OpenIdConnectResponseType.Code;

    options.Scope.Clear();
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.Scope.Add("email");
    options.Scope.Add("offline_access");

    options.ClaimActions.Remove("amr");
    options.ClaimActions.MapUniqueJsonKey("website", "website");

    options.GetClaimsFromUserInfoEndpoint = true;
    options.SaveTokens = true;

    // .NET 9 feature
    options.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Require;

    options.TokenValidationParameters.NameClaimType = "name";
    options.TokenValidationParameters.RoleClaimType = "role";
});

实现 Microsoft 标识提供者

Microsoft 拥有多个标识提供者和 OpenID Connect 实现。 Microsoft具有不同的 OpenID Connect 服务器:

  • Microsoft Entra ID
  • Microsoft Entra 外部 ID
  • Azure AD B2C

如果使用 ASP.NET Core 中的某个Microsoft标识提供者进行身份验证,建议使用 Microsoft.Identity.Web Nuget 包。

Microsoft.Identity.Web Nuget 包是在 ASP.NET Core OpenID Connect 客户端的基础上构建的Microsoft特定客户端,对默认客户端进行了一些更改。

使用第三方 OpenID Connect 提供程序客户端

许多 OpenID Connect 服务器实现都会创建针对同一 OpenID Connect 实现进行优化的 Nuget 包。 这些包实现了 OpenID Connect 客户端的详细信息,以及特定 OpenID Connect 服务器所需的额外功能。 Microsoft.Identity.Web 就是其中一个示例。

如果在单个应用程序中实现来自不同 OpenID Connect 服务器的多个 OpenID Connect 客户端,则通常最好还原为默认的 ASP.NET Core 实现,因为不同的客户端会覆盖影响其他客户端的某些选项。

OpenIddict Web 提供程序 是支持许多不同的服务器实现的客户端实现。

IdentityModel 是一个 .NET 标准辅助库,用于声明式身份、OAuth 2.0 和 OpenID Connect。 这还可以用于协助客户端的实现。

用于前端的后端 (BFF) 安全体系结构

不再建议为任何 Web 应用实现 OpenID Connect 公共客户端。

有关详细信息,请参阅 OAuth 2.0 for Browser-Based Applications(草稿)

如果实现没有独立后端的 Web 应用程序,我们建议使用 Frontend (BFF) 模式 安全体系结构。 此模式可以通过不同的方式实现,但身份验证始终在后端实现,不会向 Web 客户端发送任何敏感数据,以便进一步授权或身份验证流。

高级功能、标准、扩展 OIDC 客户端

日志记录

调试 OpenID Connect 客户端可能很难。 默认情况下不会记录个人身份信息(PII)数据。 如果在开发模式下调试,可以使用 IdentityModelEventSource.ShowPII 记录敏感的个人数据。 不要将具有 IdentityModelEventSource.ShowPII 的应用部署到高效服务器。

//using ...

using Microsoft.IdentityModel.Logging;

var builder = WebApplication.CreateBuilder(args);

//... code 

var app = builder.Build();

IdentityModelEventSource.ShowPII = true;

//... code 

app.Run();

有关详细信息,请参阅日志记录

注意

可能需要降低配置的日志级别,以查看所有必需的日志。

OIDC 和 OAuth 参数自定义

OAuth 和 OIDC 身份验证处理程序(AdditionalAuthorizationParameters)选项允许自定义通常作为重定向查询字符串的一部分包含的授权消息参数。

映射 OpenID Connect 中的声明

有关详细信息,请参阅 在 ASP.NET Core 中映射、自定义和转换声明

Blazor OpenID Connect

有关详细信息,请参阅使用 OpenID Connect (OIDC) 保护 ASP.NET Core Blazor Web App

标准