Openiddict开源的身份认证和授权库,可用它见OAuth 2.0/OpenID Connect功能集成到应用程序中。

另一个流行的库是IdentityServer4,但其已另起新项目变为收费的了,旧IdentityServer4项目不再维护,Openiddict是一个很好的替代库。

本次学习只是简单的了解下如何将Openiddict应用到在asp.net core应用程序中,目标是搭建一个独立的身份认证服务器,为各种不同类型的客户端提供身份认证服务。本文主要记录次此学习实践的一些细节总结。

本次实践完整的源码地址:( https://github.com/izanhzh/amos-learn/tree/main/OpeniddictTest
注:源码仓库未公开

服务端

  1. 创建一个asp.net core web应用项目: OpeniddictTest.Server,将启动地址设置为:https://localhost:5001

  2. 主要引入的一些nuget

    包名主要作用
    OpenIddict.AspNetCore注册OpenIddict server中间件,将应用程序变成一个身份认证服务器
    OpenIddict.EntityFrameworkCore将OpenIddict相关的一些实体注册添加到数据库
    OpenIddict.Quartz参照官网例子引入的,本次实践没有深入理解,大概知道是用于自动清除过期token等
    Microsoft.AspNetCore.Identity.UI、Microsoft.AspNetCore.Identity.EntityFrameworkCore方便快速搭建用户账户登录等功能
  3. 配置 Startup.cs

    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using OpenIddict.Validation.AspNetCore;
    using OpeniddictTest.Server.Data;
    using Quartz;
    using static OpenIddict.Abstractions.OpenIddictConstants;
    
    namespace OpeniddictTest.Server
    {
        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }
    
            public IConfiguration Configuration { get; }
    
            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddControllersWithViews();
    
                services.AddDbContext<ApplicationDbContext>(options =>
                {
                    // Configure Entity Framework Core to use Microsoft SQL Server.
                    options.UseSqlServer(Configuration.GetConnectionString("Default"));
    
                    // Register the entity sets needed by OpenIddict.
                    // Note: use the generic overload if you need to replace the default OpenIddict entities.
                    options.UseOpenIddict();
                });
    
                // Register the Identity services.
                // AddIdentity时,包含了AddAuthentication(xxx).AddCookie(),定义了使用IdentityConstants.ApplicationScheme验证方案 https://github.com/dotnet/aspnetcore/blob/main/src/Identity/Core/src/IdentityServiceCollectionExtensions.cs
                services.AddIdentity<ApplicationUser, IdentityRole<long>>()
                    .AddEntityFrameworkStores<ApplicationDbContext>()
                    .AddDefaultTokenProviders()
                    .AddDefaultUI();//DefaultUI要配合_LoginPartial.cshtml及endpoints.MapRazorPages()使用
    
                services.AddQuartz(options =>
                {
                    options.UseMicrosoftDependencyInjectionJobFactory();
                    options.UseSimpleTypeLoader();
                    options.UseInMemoryStore();
                });
                services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
    
                services.AddOpenIddict()
                  // Register the OpenIddict core components.
                  .AddCore(options =>
                  {
                      // Configure OpenIddict to use the Entity Framework Core stores and models.
                      // Note: call ReplaceDefaultEntities() to replace the default entities.
                      options.UseEntityFrameworkCore().UseDbContext<ApplicationDbContext>();
    
                      // Enable Quartz.NET integration.
                      options.UseQuartz();
                  })
                  // Register the OpenIddict server components.
                  // AddServer会对认证方案OpenIddictServerAspNetCoreDefaults.AuthenticationScheme进行Handle
                  .AddServer(options =>
                  {
                      // Enable the token endpoint.
                      options.SetAuthorizationEndpointUris("/connect/authorize");
                      options.SetLogoutEndpointUris("connect/logout");
                      options.SetTokenEndpointUris("connect/token");
                      options.SetUserinfoEndpointUris("connect/userinfo");
    
                      // Enable the client credentials flow.
                      options.AllowClientCredentialsFlow();
                      options.AllowAuthorizationCodeFlow();
    
                      options.RegisterScopes(Scopes.Email);
                      options.RegisterScopes(Scopes.Profile);
                      options.RegisterScopes(Scopes.Roles);
    
                      // Register the signing and encryption credentials.
                      options.AddDevelopmentEncryptionCertificate()
                             .AddDevelopmentSigningCertificate();
    
                      // Register the ASP.NET Core host and configure the ASP.NET Core options.
                      options.UseAspNetCore()
                             .EnableAuthorizationEndpointPassthrough()
                             .EnableTokenEndpointPassthrough()
                             .EnableStatusCodePagesIntegration()
                             .EnableLogoutEndpointPassthrough()
                             .EnableUserinfoEndpointPassthrough();//启动这个才会进入到自定义的connect/userinfo中
                  })
                  // Register the OpenIddict validation components.
                  // AddValidation会对认证方案OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme进行Handle
                  .AddValidation(options =>
                  {
                      // Import the configuration from the local OpenIddict server instance.
                      options.UseLocalServer();
    
                      // Register the ASP.NET Core host.
                      options.UseAspNetCore();
                  });
    
    
                // Register the worker responsible of seeding the database with the sample clients.
                // Note: in a real world application, this step should be part of a setup script.
                services.AddHostedService<Worker>();
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                else
                {
                    app.UseExceptionHandler("/Home/Error");
                    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                    app.UseHsts();
                }
                app.UseHttpsRedirection();
                app.UseStaticFiles();
    
                app.UseRouting();
    
                app.UseAuthentication();
                app.Use(async (ctx, next) =>
                {
                    if (ctx.User.Identity?.IsAuthenticated != true)
                    {
                        var result = await ctx.AuthenticateAsync(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
                        if (result.Succeeded && result.Principal != null)
                        {
                            ctx.User = result.Principal;
                        }
                    }
                    await next();
                });
                app.UseAuthorization();
    
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapControllers();
                    endpoints.MapControllerRoute(
                        name: "default",
                        pattern: "{controller=Home}/{action=Index}/{id?}");
                    endpoints.MapRazorPages();
                });
            }
        }
    }

    ApplicationDbContext需要继承IdentityDbContext,并重写 OnModelCreating ,添加 modelBuilder.UseOpenIddict() 进行注册OpenIddict需要的实体

    public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityRole<long>, long>
    {
        public ApplicationDbContext(DbContextOptions options) : base(options)
        {
        }
    
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            base.OnConfiguring(optionsBuilder);
        }
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
    
            modelBuilder.UseOpenIddict();
        }
    }

    services.AddOpenIddict().AddServer(options =>xxx) 中,指定了几个终结点,这些终结点是需要在项目中进行实现的(详见后文描述),当 OpenIddict 进行身份认证的时候会去访问这些终结点

    options.SetAuthorizationEndpointUris("/connect/authorize");
    options.SetLogoutEndpointUris("connect/logout");
    options.SetTokenEndpointUris("connect/token");
    options.SetUserinfoEndpointUris("connect/userinfo");

    Worker 用于初始化数据,参照官网资料创建一个 OpenIddictApplication,也就是配置需要接入的客户端,实际项目中可以用其他方式进行创建配置可以端,增加配置管理页面等

    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using OpenIddict.Abstractions;
    using OpeniddictTest.Server.Data;
    using System;
    using System.Threading;
    using System.Threading.Tasks;
    using static OpenIddict.Abstractions.OpenIddictConstants;
    
    namespace OpeniddictTest.Server
    {
        public class Worker : IHostedService
        {
            private readonly IServiceProvider _serviceProvider;
    
            public Worker(IServiceProvider serviceProvider)
            {
                _serviceProvider = serviceProvider;
            }
    
            public async Task StartAsync(CancellationToken cancellationToken)
            {
                using var scope = _serviceProvider.CreateScope();
    
                var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
                await context.Database.EnsureCreatedAsync();
    
                var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
    
                var webClient1AppDescriptor = new OpenIddictApplicationDescriptor
                {
                    ClientId = "WebMvc1",
                    ClientSecret = "3C68DE8C-7195-4E1B-835E-6DDE77319419",
                    ConsentType = ConsentTypes.Systematic,
                    DisplayName = "Web测试客户端1",
                    //只有配置了的Uri才允许回调,否则认证回调时会提示redirect_uri无效
                    RedirectUris =
                    {
                        new Uri("https://localhost:5003/callback/login"),//客户端的回调地址
                        new Uri("https://oauth.pstmn.io/v1/callback")//支持postman进行测试回调获取token
                    },
                    PostLogoutRedirectUris =
                    {
                        new Uri("https://localhost:5003/callback/logout")
                    },
                    Permissions =
                    {
                        //只有配置了的Endpoints,客户端才允许访问
                        Permissions.Endpoints.Authorization,
                        Permissions.Endpoints.Token,
                        Permissions.Endpoints.Logout,
                        Permissions.Scopes.Email,
                        Permissions.Scopes.Profile,
                        Permissions.Scopes.Roles,
                        Permissions.GrantTypes.ClientCredentials,
                        Permissions.GrantTypes.AuthorizationCode,
                        Permissions.ResponseTypes.Code,
                    },
                    Type = ClientTypes.Confidential
                };
    
                var webClient1App = await manager.FindByClientIdAsync(webClient1AppDescriptor.ClientId);
                if (webClient1App is null)
                {
                    await manager.CreateAsync(webClient1AppDescriptor);
                }
                else
                {
                    await manager.UpdateAsync(webClient1App, webClient1AppDescriptor);
                }
            }
    
            public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
        }
    }

    现实中一般会存在这样的一种情况,我们系统最初不需要与外部系统做数据对接,因此也就不需要对外部系统提供身份认证服务,使用 asp.net core identity 就可以为我们的系统功能进行身份认证。

    但是后来出现了与外部系统数据对接的需求,某个外部系统想要调用我们的系统获取一些数据,或者外部系统想要用我们系统的用户信息登录他们的系统(类似于现在很多网站支持用微信登录),且外部系统不是本公司的,那么一般会采用 OAuth2.0 方案,我们不需要提供系统的账户密码给外部系统就可以完成身份认证。

    此时可以引入 OpenIddict 来进行 OAuth2.0 实现,那么就会存在这么一个问题,系统同时存在两种身份认证方案,一个是默认的 identity 身份认证方案,一个是 OpenIddict 的身份认证方案。假设某个 Controller 我们的系统可以调用,我们用 AuthorizeAttribute 特性标记了该 ControllerAuthorizeAttribute 默认不设定身份认证方案 (AuthenticationSchemes) 参数时,默认是按identity 身份认证方案进行处理的,当外部系统调用时,因为是通过 OpenIddict 进行身份认证的,因此会用 OpenIddict 的身份认证方案去获取登录凭证,而不同认证方案获取到的凭证是不能共用的,也就说此时外部系统获取到的登录凭证,还不能直接调用 Controller, 除非我们对 Controller 再指定 AuthenticationSchemes 参数。

    针对这种情况,我们可以在 Configure 中,添加下面这段代码,系统首先会判断当前请求有没有 identity 登录凭证,如果没有,则会再判断有没有 OpenIddict 的登录凭证,如果有 OpenIddict 的登录凭证,则将 OpenIddict 登录凭证的用户信息赋给 identity 登录凭证,这样我们的 Controller 就不需要做任何处理,既可以让本系统的功能用 identity 进行身份认证调用,也可以让外部系统用 OpenIddict 进行身份认证调用。

    app.Use(async (ctx, next) =>
    {
        if (ctx.User.Identity?.IsAuthenticated != true)
        {
            var result = await ctx.AuthenticateAsync(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
            if (result.Succeeded && result.Principal != null)
            {
                ctx.User = result.Principal;
            }
        }
        await next();
    });
  4. 添加一个 AuthorizationController 实现之前配置定义的登录、注销等终结点

    using Microsoft.AspNetCore;
    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Primitives;
    using Microsoft.IdentityModel.Tokens;
    using OpenIddict.Abstractions;
    using OpenIddict.Server.AspNetCore;
    using OpeniddictTest.Server.Data;
    using OpeniddictTest.Server.Helpers;
    using OpeniddictTest.Server.Models.Authorization;
    using System;
    using System.Collections.Generic;
    using System.Collections.Immutable;
    using System.Linq;
    using System.Security.Claims;
    using System.Threading.Tasks;
    using static OpenIddict.Abstractions.OpenIddictConstants;
    
    namespace OpeniddictTest.Server.Controllers
    {
    
        public class AuthorizationController : Controller
        {
            private readonly IOpenIddictApplicationManager _applicationManager;
            private readonly IOpenIddictAuthorizationManager _authorizationManager;
            private readonly IOpenIddictScopeManager _scopeManager;
            private readonly SignInManager<ApplicationUser> _signInManager;
            private readonly UserManager<ApplicationUser> _userManager;
    
            public AuthorizationController(
                IOpenIddictApplicationManager applicationManager,
                IOpenIddictAuthorizationManager authorizationManager,
                IOpenIddictScopeManager scopeManager,
                SignInManager<ApplicationUser> signInManager,
                UserManager<ApplicationUser> userManager)
            {
                _applicationManager = applicationManager;
                _authorizationManager = authorizationManager;
                _scopeManager = scopeManager;
                _signInManager = signInManager;
                _userManager = userManager;
            }
    
            [HttpGet("~/connect/authorize")]
            [HttpPost("~/connect/authorize")]
            [IgnoreAntiforgeryToken]
            public async Task<IActionResult> Authorize()
            {
                var request = HttpContext.GetOpenIddictServerRequest() ??
                    throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
    
                // Try to retrieve the user principal stored in the authentication cookie and redirect
                // the user agent to the login page (or to an external provider) in the following cases:
                //
                //  - If the user principal can't be extracted or the cookie is too old.
                //  - If prompt=login was specified by the client application.
                //  - If a max_age parameter was provided and the authentication cookie is not considered "fresh" enough.
                //
                // For scenarios where the default authentication handler configured in the ASP.NET Core
                // authentication options shouldn't be used, a specific scheme can be specified here.
                var result = await HttpContext.AuthenticateAsync();
                if (result == null || !result.Succeeded || request.HasPrompt(Prompts.Login) ||
                   (request.MaxAge != null && result.Properties?.IssuedUtc != null &&
                    DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value)))
                {
                    // If the client application requested promptless authentication,
                    // return an error indicating that the user is not logged in.
                    if (request.HasPrompt(Prompts.None))
                    {
                        return Forbid(
                            authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                            properties: new AuthenticationProperties(new Dictionary<string, string>
                            {
                                [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
                                [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
                            }));
                    }
    
                    // To avoid endless login -> authorization redirects, the prompt=login flag
                    // is removed from the authorization request payload before redirecting the user.
                    var prompt = string.Join(" ", request.GetPrompts().Remove(Prompts.Login));
    
                    var parameters = Request.HasFormContentType ?
                        Request.Form.Where(parameter => parameter.Key != Parameters.Prompt).ToList() :
                        Request.Query.Where(parameter => parameter.Key != Parameters.Prompt).ToList();
    
                    parameters.Add(KeyValuePair.Create(Parameters.Prompt, new StringValues(prompt)));
    
                    // For scenarios where the default challenge handler configured in the ASP.NET Core
                    // authentication options shouldn't be used, a specific scheme can be specified here.
                    return Challenge(new AuthenticationProperties
                    {
                        RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters)
                    });
                }
    
                // Retrieve the profile of the logged in user.
                var user = await _userManager.GetUserAsync(result.Principal) ??
                    throw new InvalidOperationException("The user details cannot be retrieved.");
    
                // Retrieve the application details from the database.
                var application = await _applicationManager.FindByClientIdAsync(request.ClientId) ??
                    throw new InvalidOperationException("Details concerning the calling client application cannot be found.");
    
                // Retrieve the permanent authorizations associated with the user and the calling client application.
                var authorizations = await _authorizationManager.FindAsync(
                    subject: await _userManager.GetUserIdAsync(user),
                    client: await _applicationManager.GetIdAsync(application),
                    status: Statuses.Valid,
                    type: AuthorizationTypes.Permanent,//此参数用于控制登录时永久的还是临时的
                    scopes: request.GetScopes()).ToListAsync();
    
                switch (await _applicationManager.GetConsentTypeAsync(application))
                {
                    // If the consent is external (e.g when authorizations are granted by a sysadmin),
                    // immediately return an error if no authorization can be found in the database.
                    case ConsentTypes.External when !authorizations.Any():
                        return Forbid(
                            authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                            properties: new AuthenticationProperties(new Dictionary<string, string>
                            {
                                [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
                                [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
                                    "The logged in user is not allowed to access this client application."
                            }));
    
                    // If the consent is implicit or if an authorization was found,
                    // return an authorization response without displaying the consent form.
                    case ConsentTypes.Implicit:
                    case ConsentTypes.External when authorizations.Any():
                    case ConsentTypes.Explicit when authorizations.Any() && !request.HasPrompt(Prompts.Consent):
                        // Create the claims-based identity that will be used by OpenIddict to generate tokens.
                        var identity = new ClaimsIdentity(
                            authenticationType: TokenValidationParameters.DefaultAuthenticationType,
                            nameType: Claims.Name,
                            roleType: Claims.Role);
    
                        // Add the claims that will be persisted in the tokens.
                        identity.SetClaim(Claims.Subject, await _userManager.GetUserIdAsync(user))
                                .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user))
                                .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user))
                                .SetClaims(Claims.Role, (await _userManager.GetRolesAsync(user)).ToImmutableArray());
    
                        // Note: in this sample, the granted scopes match the requested scope
                        // but you may want to allow the user to uncheck specific scopes.
                        // For that, simply restrict the list of scopes before calling SetScopes.
                        identity.SetScopes(request.GetScopes());
                        identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
    
                        // Automatically create a permanent authorization to avoid requiring explicit consent
                        // for future authorization or token requests containing the same scopes.
                        var authorization = authorizations.LastOrDefault();
                        authorization ??= await _authorizationManager.CreateAsync(
                            identity: identity,
                            subject: await _userManager.GetUserIdAsync(user),
                            client: await _applicationManager.GetIdAsync(application),
                            type: AuthorizationTypes.Permanent,
                            scopes: identity.GetScopes());
    
                        identity.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
                        identity.SetDestinations(GetDestinations);
    
                        return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
    
                    // At this point, no authorization was found in the database and an error must be returned
                    // if the client application specified prompt=none in the authorization request.
                    case ConsentTypes.Explicit when request.HasPrompt(Prompts.None):
                    case ConsentTypes.Systematic when request.HasPrompt(Prompts.None):
                        return Forbid(
                            authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                            properties: new AuthenticationProperties(new Dictionary<string, string>
                            {
                                [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
                                [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
                                    "Interactive user consent is required."
                            }));
    
                    // In every other case, render the consent form.
                    default:
                        return View(new AuthorizeViewModel
                        {
                            ApplicationName = await _applicationManager.GetLocalizedDisplayNameAsync(application),
                            Scope = request.Scope
                        });
                }
            }
    
            [Authorize, FormValueRequired("submit.Accept")]
            [HttpPost("~/connect/authorize"), ValidateAntiForgeryToken]
            public async Task<IActionResult> Accept()
            {
                var request = HttpContext.GetOpenIddictServerRequest() ??
                    throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
    
                // Retrieve the profile of the logged in user.
                var user = await _userManager.GetUserAsync(User) ??
                    throw new InvalidOperationException("The user details cannot be retrieved.");
    
                // Retrieve the application details from the database.
                var application = await _applicationManager.FindByClientIdAsync(request.ClientId) ??
                    throw new InvalidOperationException("Details concerning the calling client application cannot be found.");
    
                // Retrieve the permanent authorizations associated with the user and the calling client application.
                var authorizations = await _authorizationManager.FindAsync(
                    subject: await _userManager.GetUserIdAsync(user),
                    client: await _applicationManager.GetIdAsync(application),
                    status: Statuses.Valid,
                    type: AuthorizationTypes.Permanent,
                    scopes: request.GetScopes()).ToListAsync();
    
                // Note: the same check is already made in the other action but is repeated
                // here to ensure a malicious user can't abuse this POST-only endpoint and
                // force it to return a valid response without the external authorization.
                if (!authorizations.Any() && await _applicationManager.HasConsentTypeAsync(application, ConsentTypes.External))
                {
                    return Forbid(
                        authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                        properties: new AuthenticationProperties(new Dictionary<string, string>
                        {
                            [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
                            [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
                                "The logged in user is not allowed to access this client application."
                        }));
                }
    
                // Create the claims-based identity that will be used by OpenIddict to generate tokens.
                var identity = new ClaimsIdentity(
                    authenticationType: TokenValidationParameters.DefaultAuthenticationType,
                    nameType: Claims.Name,
                    roleType: Claims.Role);
    
                // Add the claims that will be persisted in the tokens.
                identity.SetClaim(Claims.Subject, await _userManager.GetUserIdAsync(user))
                        .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user))
                        .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user))
                        .SetClaims(Claims.Role, (await _userManager.GetRolesAsync(user)).ToImmutableArray());
    
                // Note: in this sample, the granted scopes match the requested scope
                // but you may want to allow the user to uncheck specific scopes.
                // For that, simply restrict the list of scopes before calling SetScopes.
                identity.SetScopes(request.GetScopes());
                identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
    
                // Automatically create a permanent authorization to avoid requiring explicit consent
                // for future authorization or token requests containing the same scopes.
                var authorization = authorizations.LastOrDefault();
                authorization ??= await _authorizationManager.CreateAsync(
                    identity: identity,
                    subject: await _userManager.GetUserIdAsync(user),
                    client: await _applicationManager.GetIdAsync(application),
                    type: AuthorizationTypes.Permanent,
                    scopes: identity.GetScopes());
    
                identity.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
                identity.SetDestinations(GetDestinations);
    
                // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
                return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            }
    
            [Authorize, FormValueRequired("submit.Deny")]
            [HttpPost("~/connect/authorize"), ValidateAntiForgeryToken]
            // Notify OpenIddict that the authorization grant has been denied by the resource owner
            // to redirect the user agent to the client application using the appropriate response_mode.
            public IActionResult Deny() => Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
    
            [HttpGet("~/connect/logout")]
            public IActionResult Logout() => View();
    
            [ActionName(nameof(Logout)), HttpPost("~/connect/logout"), ValidateAntiForgeryToken]
            public async Task<IActionResult> LogoutPost()
            {
                // Ask ASP.NET Core Identity to delete the local and external cookies created
                // when the user agent is redirected from the external identity provider
                // after a successful authentication flow (e.g Google or Facebook).
                await _signInManager.SignOutAsync();
    
                // Returning a SignOutResult will ask OpenIddict to redirect the user agent
                // to the post_logout_redirect_uri specified by the client application or to
                // the RedirectUri specified in the authentication properties if none was set.
                return SignOut(
                    authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                    properties: new AuthenticationProperties
                    {
                        RedirectUri = "/"
                    });
            }
    
            [HttpPost("~/connect/token"), IgnoreAntiforgeryToken, Produces("application/json")]
            public async Task<IActionResult> Exchange()
            {
                var request = HttpContext.GetOpenIddictServerRequest() ??
                    throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
    
                if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
                {
                    // Retrieve the claims principal stored in the authorization code/refresh token.
                    var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
    
                    // Retrieve the user profile corresponding to the authorization code/refresh token.
                    var user = await _userManager.FindByIdAsync(result.Principal.GetClaim(Claims.Subject));
                    if (user is null)
                    {
                        return Forbid(
                            authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                            properties: new AuthenticationProperties(new Dictionary<string, string>
                            {
                                [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
                                [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
                            }));
                    }
    
                    // Ensure the user is still allowed to sign in.
                    if (!await _signInManager.CanSignInAsync(user))
                    {
                        return Forbid(
                            authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                            properties: new AuthenticationProperties(new Dictionary<string, string>
                            {
                                [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
                                [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
                            }));
                    }
    
                    var identity = new ClaimsIdentity(result.Principal.Claims,
                        authenticationType: TokenValidationParameters.DefaultAuthenticationType,
                        nameType: Claims.Name,
                        roleType: Claims.Role);
    
                    // Override the user claims present in the principal in case they
                    // changed since the authorization code/refresh token was issued.
                    identity.SetClaim(Claims.Subject, await _userManager.GetUserIdAsync(user))
                            .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user))
                            .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user))
                            .SetClaims(Claims.Role, (await _userManager.GetRolesAsync(user)).ToImmutableArray());
    
                    identity.SetDestinations(GetDestinations);
    
                    // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
                    return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
                }
                else if (request.IsClientCredentialsGrantType())
                {
                    var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
                    if (application == null)
                    {
                        throw new InvalidOperationException("The application details cannot be found in the database.");
                    }
    
                    // Create the claims-based identity that will be used by OpenIddict to generate tokens.
                    var identity = new ClaimsIdentity(
                        authenticationType: TokenValidationParameters.DefaultAuthenticationType,
                        nameType: Claims.Name,
                        roleType: Claims.Role);
    
                    // Add the claims that will be persisted in the tokens (use the client_id as the subject identifier).
                    identity.SetClaim(Claims.Subject, await _applicationManager.GetClientIdAsync(application));
                    identity.SetClaim(Claims.Name, await _applicationManager.GetDisplayNameAsync(application));
    
                    // Note: In the original OAuth 2.0 specification, the client credentials grant
                    // doesn't return an identity token, which is an OpenID Connect concept.
                    //
                    // As a non-standardized extension, OpenIddict allows returning an id_token
                    // to convey information about the client application when the "openid" scope
                    // is granted (i.e specified when calling principal.SetScopes()). When the "openid"
                    // scope is not explicitly set, no identity token is returned to the client application.
    
                    // Set the list of scopes granted to the client application in access_token.
                    identity.SetScopes(request.GetScopes());
                    identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
                    identity.SetDestinations(GetDestinations);
    
                    return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
                }
    
                throw new InvalidOperationException("The specified grant type is not supported.");
            }
    
            private static IEnumerable<string> GetDestinations(Claim claim)
            {
                // Note: by default, claims are NOT automatically included in the access and identity tokens.
                // To allow OpenIddict to serialize them, you must attach them a destination, that specifies
                // whether they should be included in access tokens, in identity tokens or in both.
    
                switch (claim.Type)
                {
                    case Claims.Name:
                        yield return Destinations.AccessToken;
    
                        if (claim.Subject.HasScope(Scopes.Profile))
                            yield return Destinations.IdentityToken;
    
                        yield break;
    
                    case Claims.Email:
                        yield return Destinations.AccessToken;
    
                        if (claim.Subject.HasScope(Scopes.Email))
                            yield return Destinations.IdentityToken;
    
                        yield break;
    
                    case Claims.Role:
                        yield return Destinations.AccessToken;
    
                        if (claim.Subject.HasScope(Scopes.Roles))
                            yield return Destinations.IdentityToken;
    
                        yield break;
    
                    // Never include the security stamp in the access and identity tokens, as it's a secret value.
                    case "AspNet.Identity.SecurityStamp": yield break;
    
                    default:
                        yield return Destinations.AccessToken;
                        yield break;
                }
            }
        }
    }

    Authorize 方法中,先判断了请求是否是 OpenIddict 身份认证的请求,如果是,则会用默认的身份认证方案(Identity)校验是否有登录,如果没有,会返回一个 ChallengeResult 将请求重定向用默认的身份认证方案(Identity)进行登录,登录完成后回调回来,重新执行一次 Authorize 方法,此时就有了默认的身份认证方案(Identity)的登录凭证了,根据这个登录凭证信息生成 ClaimsPrincipal,对 OpenIddict 身份认证方案进行SignIn操作,记录OpenIddict 身份认证方案的登录凭证。

  5. 增加授权确认页面,路径:Views/Authorization/Authorize.cshtml,用于让用户点击确认要授权

    @using Microsoft.Extensions.Primitives
    @using OpeniddictTest.Server.Models.Authorization
    
    @model AuthorizeViewModel
    
    <div class="jumbotron">
        <h1>Authorization</h1>
    
        <p class="lead text-left">Do you want to grant <strong>@Model.ApplicationName</strong> access to your data? (scopes requested: @Model.Scope)</p>
    
        <form asp-controller="Authorization" asp-action="Authorize" method="post">
            @* Flow the request parameters so they can be received by the Accept/Reject actions: *@
            @foreach (var parameter in Context.Request.HasFormContentType ? (IEnumerable<KeyValuePair<string, StringValues>>)Context.Request.Form : Context.Request.Query)
            {
                <input type="hidden" name="@parameter.Key" value="@parameter.Value" />
            }
    
            <input class="btn btn-lg btn-success" name="submit.Accept" type="submit" value="Yes" />
            <input class="btn btn-lg btn-danger" name="submit.Deny" type="submit" value="No" />
        </form>
    </div>
  6. 增加注销确认页面,路径:Views/Authorization/Logout.cshtml,用于让用户点击确认要注销

    @using Microsoft.Extensions.Primitives
    
    <div class="jumbotron">
        <h1>Log out</h1>
        <p class="lead text-left">Are you sure you want to sign out?</p>
    
        <form asp-controller="Authorization" asp-action="Logout" method="post">
            @* Flow the request parameters so they can be received by the LogoutPost action: *@
            @foreach (var parameter in Context.Request.HasFormContentType ? (IEnumerable<KeyValuePair<string, StringValues>>)Context.Request.Form : Context.Request.Query)
            {
                <input type="hidden" name="@parameter.Key" value="@parameter.Value" />
            }
    
            <input class="btn btn-lg btn-success" name="Confirm" type="submit" value="Yes" />
        </form>
    </div>

客户端(asp.net core mvc)

  1. 创建一个asp.net core web应用项目: OpeniddictTest.Web.Mvc ,将启动地址设置为:https://localhost:5003

  2. 主要引入的一些 nuget

    包名主要作用
    OpenIddict.AspNetCore注册OpenIddict client中间件,帮助请求OpenIddict身份认证服务完成身份认证
  3. 配置 Startup.cs

    using Microsoft.AspNetCore.Authentication.Cookies;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.HttpsPolicy;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using OpenIddict.Client;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using static OpenIddict.Abstractions.OpenIddictConstants;
    
    namespace OpeniddictTest.Web.Mvc
    {
        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }
    
            public IConfiguration Configuration { get; }
    
            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddControllersWithViews();
    
                services.AddAuthentication(options =>
                {
                    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                })
                .AddCookie(options =>
                 {
                     options.LoginPath = "/login";
                     options.LogoutPath = "/logout";
                     options.ExpireTimeSpan = TimeSpan.FromMinutes(50);
                     options.SlidingExpiration = false;
                 });
    
                services.AddOpenIddict()
                    // Register the OpenIddict client components.
                    .AddClient(options =>
                    {
                        // Note: this sample uses the code flow, but you can enable the other flows if necessary.
                        options.AllowAuthorizationCodeFlow();
    
                        // Register the signing and encryption credentials used to protect
                        // sensitive data like the state tokens produced by OpenIddict.
                        options.AddDevelopmentEncryptionCertificate()
                              .AddDevelopmentSigningCertificate();
    
                        // Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
                        options.UseAspNetCore()
                              .EnableStatusCodePagesIntegration()
                              .EnableRedirectionEndpointPassthrough()
                              .EnablePostLogoutRedirectionEndpointPassthrough();
    
                        // Register the System.Net.Http integration and use the identity of the current
                        // assembly as a more specific user agent, which can be useful when dealing with
                        // providers that use the user agent as a way to throttle requests (e.g Reddit).
                        options.UseSystemNetHttp()
                              .SetProductInformation(typeof(Startup).Assembly);
    
                        options.DisableTokenStorage();
    
                        // Add a client registration matching the client application definition in the server project.
                        options.AddRegistration(new OpenIddictClientRegistration
                        {
                            Issuer = new Uri("https://localhost:5001/", UriKind.Absolute),
    
                            ClientId = "WebMvc1",
                            ClientSecret = "3C68DE8C-7195-4E1B-835E-6DDE77319419",
                            Scopes = { Scopes.Email, Scopes.Roles, Scopes.Profile },
    
                            // Note: to mitigate mix-up attacks, it's recommended to use a unique redirection endpoint
                            // URI per provider, unless all the registered providers support returning a special "iss"
                            // parameter containing their URL as part of authorization responses. For more information,
                            // see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
                            RedirectUri = new Uri("callback/login", UriKind.Relative),
                            PostLogoutRedirectUri = new Uri("callback/logout", UriKind.Relative)
                        });
                    });
    
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                else
                {
                    app.UseExceptionHandler("/Home/Error");
                    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                    app.UseHsts();
                }
                app.UseHttpsRedirection();
                app.UseStaticFiles();
    
                app.UseRouting();
    
                app.UseAuthentication();
                app.UseAuthorization();
    
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapControllers();
                    endpoints.MapControllerRoute(
                        name: "default",
                        pattern: "{controller=Home}/{action=Index}/{id?}");
                });
            }
        }
    }

    先注册了 Cookie 身份认证中间件,从Cookie中获取用户登录信息,并指定登录和注销的终结点,这两个终结点是要进行实现的(详见后文描述

    options.LoginPath = "/login";
    options.LogoutPath = "/logout";

    然后注册了 OpenIddict 客户端生成认证中间件,指定了身份认证服务器登录后回调和注销后回调的终结点,这两个终结点是要进行实现的(详见后文描述

    options.AddRegistration(new OpenIddictClientRegistration
    {
        Issuer = new Uri("https://localhost:5001/", UriKind.Absolute),
    
        ClientId = "WebMvc1",
        ClientSecret = "3C68DE8C-7195-4E1B-835E-6DDE77319419",
        Scopes = { Scopes.Email, Scopes.Roles, Scopes.Profile },
    
        // Note: to mitigate mix-up attacks, it's recommended to use a unique redirection endpoint
        // URI per provider, unless all the registered providers support returning a special "iss"
        // parameter containing their URL as part of authorization responses. For more information,
        // see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
        RedirectUri = new Uri("callback/login", UriKind.Relative),
        PostLogoutRedirectUri = new Uri("callback/logout", UriKind.Relative)
    });
  4. 添加一个 AuthenticationController 实现之前配置定义的相关终结点

    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Authentication.Cookies;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.IdentityModel.Tokens;
    using OpenIddict.Abstractions;
    using OpenIddict.Client.AspNetCore;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Claims;
    using System.Threading.Tasks;
    using static OpenIddict.Abstractions.OpenIddictConstants;
    
    namespace OpeniddictTest.Web.Mvc.Controllers
    {
        public class AuthenticationController : Controller
        {
            [HttpGet("~/login")]
            public ActionResult LogIn(string returnUrl)
            {
                var properties = new AuthenticationProperties
                {
                    // Only allow local return URLs to prevent open redirect attacks.
                    RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/Home/Index"
                };
    
                // Ask the OpenIddict client middleware to redirect the user agent to the identity provider.
                return Challenge(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
            }
    
            [HttpPost("~/logout"), ValidateAntiForgeryToken]
            public async Task<ActionResult> LogOut(string returnUrl)
            {
                // Retrieve the identity stored in the local authentication cookie. If it's not available,
                // this indicate that the user is already logged out locally (or has not logged in yet).
                //
                // For scenarios where the default authentication handler configured in the ASP.NET Core
                // authentication options shouldn't be used, a specific scheme can be specified here.
                var result = await HttpContext.AuthenticateAsync();
                if (!result.Succeeded)
                {
                    // Only allow local return URLs to prevent open redirect attacks.
                    return Redirect(Url.IsLocalUrl(returnUrl) ? returnUrl : "/");
                }
    
                // Remove the local authentication cookie before triggering a redirection to the remote server.
                //
                // For scenarios where the default sign-out handler configured in the ASP.NET Core
                // authentication options shouldn't be used, a specific scheme can be specified here.
                await HttpContext.SignOutAsync();
    
                var properties = new AuthenticationProperties(new Dictionary<string, string>
                {
                    // While not required, the specification encourages sending an id_token_hint
                    // parameter containing an identity token returned by the server for this user.
                    [OpenIddictClientAspNetCoreConstants.Properties.IdentityTokenHint] =
                        result.Properties.GetTokenValue(OpenIddictClientAspNetCoreConstants.Tokens.BackchannelIdentityToken)
                })
                {
                    // Only allow local return URLs to prevent open redirect attacks.
                    RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/"
                };
    
                // Ask the OpenIddict client middleware to redirect the user agent to the identity provider.
                return SignOut(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
            }
    
            // Note: this controller uses the same callback action for all providers
            // but for users who prefer using a different action per provider,
            // the following action can be split into separate actions.
            [HttpGet("~/callback/login"), HttpPost("~/callback/login"), IgnoreAntiforgeryToken]
            public async Task<ActionResult> LogInCallback()
            {
                // Retrieve the authorization data validated by OpenIddict as part of the callback handling.
                var result = await HttpContext.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
    
                // Multiple strategies exist to handle OAuth 2.0/OpenID Connect callbacks, each with their pros and cons:
                //
                //   * Directly using the tokens to perform the necessary action(s) on behalf of the user, which is suitable
                //     for applications that don't need a long-term access to the user's resources or don't want to store
                //     access/refresh tokens in a database or in an authentication cookie (which has security implications).
                //     It is also suitable for applications that don't need to authenticate users but only need to perform
                //     action(s) on their behalf by making API calls using the access token returned by the remote server.
                //
                //   * Storing the external claims/tokens in a database (and optionally keeping the essential claims in an
                //     authentication cookie so that cookie size limits are not hit). For the applications that use ASP.NET
                //     Core Identity, the UserManager.SetAuthenticationTokenAsync() API can be used to store external tokens.
                //
                //     Note: in this case, it's recommended to use column encryption to protect the tokens in the database.
                //
                //   * Storing the external claims/tokens in an authentication cookie, which doesn't require having
                //     a user database but may be affected by the cookie size limits enforced by most browser vendors
                //     (e.g Safari for macOS and Safari for iOS/iPadOS enforce a per-domain 4KB limit for all cookies).
                //
                //     Note: this is the approach used here, but the external claims are first filtered to only persist
                //     a few claims like the user identifier. The same approach is used to store the access/refresh tokens.
    
                // Important: if the remote server doesn't support OpenID Connect and doesn't expose a userinfo endpoint,
                // result.Principal.Identity will represent an unauthenticated identity and won't contain any claim.
                //
                // Such identities cannot be used as-is to build an authentication cookie in ASP.NET Core (as the
                // antiforgery stack requires at least a name claim to bind CSRF cookies to the user's identity) but
                // the access/refresh tokens can be retrieved using result.Properties.GetTokens() to make API calls.
                if (!(result.Principal is ClaimsPrincipal) || !result.Principal.Identity.IsAuthenticated)
                {
                    throw new InvalidOperationException("The external authorization data cannot be used for authentication.");
                }
    
                // Build an identity based on the external claims and that will be used to create the authentication cookie.
                var identity = new ClaimsIdentity(
                    authenticationType: TokenValidationParameters.DefaultAuthenticationType,
                    nameType: Claims.Name,
                    roleType: Claims.Role);
    
                // By default, OpenIddict will automatically try to map the email/name and name identifier claims from
                // their standard OpenID Connect or provider-specific equivalent, if available. If needed, additional
                // claims can be resolved from the external identity and copied to the final authentication cookie.
                identity.SetClaim(Claims.Email, result.Principal.GetClaim(Claims.Email))
                        .SetClaim(Claims.Name, result.Principal.GetClaim(Claims.Name))
                        .SetClaim(Claims.Role, result.Principal.GetClaim(Claims.Role))
                        .SetClaim(ClaimTypes.NameIdentifier, result.Principal.GetClaim(ClaimTypes.NameIdentifier));
    
                // Preserve the registration identifier to be able to resolve it later.
                identity.SetClaim(Claims.Private.RegistrationId, result.Principal.GetClaim(Claims.Private.RegistrationId));
    
                // Build the authentication properties based on the properties that were added when the challenge was triggered.
                var properties = new AuthenticationProperties(result.Properties.Items)
                {
                    RedirectUri = result.Properties.RedirectUri ?? "/"
                };
    
                // If needed, the tokens returned by the authorization server can be stored in the authentication cookie.
                //
                // To make cookies less heavy, tokens that are not used are filtered out before creating the cookie.
                properties.StoreTokens(result.Properties.GetTokens().Where(token => token.Name switch
                {
                    // Preserve the access, identity and refresh tokens returned in the token response, if available.
                    OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken => true,
                    OpenIddictClientAspNetCoreConstants.Tokens.BackchannelIdentityToken => true,
                    OpenIddictClientAspNetCoreConstants.Tokens.RefreshToken => true,
                    // Ignore the other tokens.
                    _ => false
                }));
    
                // Ask the default sign-in handler to return a new cookie and redirect the
                // user agent to the return URL stored in the authentication properties.
                //
                // For scenarios where the default sign-in handler configured in the ASP.NET Core
                // authentication options shouldn't be used, a specific scheme can be specified here.
                //return SignIn(new ClaimsPrincipal(identity), properties, CookieAuthenticationDefaults.AuthenticationScheme);
    
                //return SignIn(new ClaimsPrincipal(identity), properties, CookieAuthenticationDefaults.AuthenticationScheme);
    
                await HttpContext.SignInAsync(new ClaimsPrincipal(identity), properties);
                return Redirect(result!.Properties!.RedirectUri);
            }
    
            // Note: this controller uses the same callback action for all providers
            // but for users who prefer using a different action per provider,
            // the following action can be split into separate actions.
            [HttpGet("~/callback/logout"), HttpPost("~/callback/logout"), IgnoreAntiforgeryToken]
            public async Task<ActionResult> LogOutCallback()
            {
                // Retrieve the data stored by OpenIddict in the state token created when the logout was triggered.
                var result = await HttpContext.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
    
                // In this sample, the local authentication cookie is always removed before the user agent is redirected
                // to the authorization server. Applications that prefer delaying the removal of the local cookie can
                // remove the corresponding code from the logout action and remove the authentication cookie in this action.
    
                return Redirect(result!.Properties!.RedirectUri);
            }
        }
    }

    LogIn 方法只做了一件事,返回一个 ChallengeResult, 将请求重定向到用 OpenIddictClientAspNetCoreDefaults.AuthenticationScheme 身份认证方案进行登录, 此时会通过 OpenIddict 客户端中间件,向配置的身份认证服务器请求登录。

    用户在身份认证服务器提供的登录页面操作完成登录后,回调回来,调用 LogInCallback 方法,此方法会根据回调回来的登录凭证信息,生成 ClaimsPrincipal ,对 Cookie 身份认证方案进行SignIn操作,记录Cookie 身份认证方案的登录凭证。

整体流程就是用户访问客户端需要授权的接口, 经过 Cookie 身份验证中间件,从 Cookie 中获取登录凭证,如果没有,重定向到 /login 登录终结点,登录终结点再重定向到 OpenIddict 身份认证服务器中进行登录,登录完成后,OpenIddict 身份认证服务器回调 /callback/login 终结点,在 /callback/login 中将登录凭证保存到 Cookie 中。

注销的流程类似。