3 选 1

IdentityServer 4

本来 IdentityServer 4 一直都是首选的, 但在 2020 年他们决定成立公司, IdentityServer 5 就开始收费了.

The Future of IdentityServer

 

Azure Active Directory B2C

Azure 的 SASS

ASP.NET Core 6 and Authentication Servers

ASP.NET Core Team 明确表示他们不会投入任何资源去研发类似 IdentityServer 的东西, 虽然 ASP.NET Core 5.0 开始, SPA 的 template 是依赖 IdentityServer 4 的,

并且 6.0 也会依赖, 7.0 会有替代, 但绝对没有计划要做类似的或者辅助类似的 Library, 原因是 M$ 要卖 Azure 产品.

 

OpenIddict Core 

Github 地址

这个是目前跟 IdentityServer 4 最靠近的替代库了. 这篇主要介绍这个

关于这 3 者的对比可以参考 : 

Token-Based Security: 3 Possible Alternatives To IdentityServer

ASP.NET Core 6 and authentication servers: the real bait and switch is not the one you think (OpenIddict Core 作者写的)

 

前言

OpenIddict 是很小的项目, 算是 1 个人维护的吧 (不过作者也挺厉害的, 一直用心维护着) 

它没有 IdentityServer 那么好的封装, 文档, 教程. 但是对于开发人员来说, 要搞起来还是挺容易的. 

这里只是大概记入一些 OpenIddict 对应的关系和解释, 并不会 step by step 的教, 要整个搞起来建议跟着 refer 做.

 

主要参考:

Step by step tutorial (client = Postman) 

Step by step tutorial (client = Angular)

项目源码 

官网文档

各种 Samples 

Config 的一些细节

 

Startup.cs 说起

OpenIddict 推荐使用 EF Core 做 (好像是可以换 Storage, 但我就是用 EF Core 的所以也没有去研究其它的)

使用 UseOpenIddict extension 来进行 setup 

OIDC – OpenIddict Core

这里和 IdentityServer 4 的 setup 方式不太一样哦. Library 如果要结合 EF Core 的话, 我目前觉得 IdentityServer 4 的方式会比较正确. 就是使用分开的 Context Migrations 

OIDC – OpenIddict Core

OpenIddict 用的方式是 ReplaceService

OIDC – OpenIddict Core

通过 ReplaceService, OpenIddict 就可以拦截到 EntityModelBuilder

OIDC – OpenIddict Core

不过有一点要注意哦, ReplaceService 只能调用一次而已. 如果项目本身有用到或者其它 Library 也有用到, 是会坏掉的. 所以我还是觉得 Library 应该要向 IdentityServer 4 那样,使用不同的 DbContext 来管理. 

接下来就是 AddOpenIddict()

OIDC – OpenIddict Core

AddQuartz 是因为 OpenIddict 需要定时去 Database 清楚过期的 Access Token, 所以需要依赖一个 Server Task Library, 而它选了 Quartz ... 竟然不选 Hangfire...

然后是 .AddCore 负责 setup EF Core 和 Quartz

OIDC – OpenIddict Core

接着是 .AddServer 就是 autho server 的主要 config 了

OIDC – OpenIddict Core

里面有: 

Token 的寿命 

支持的 Flow 

各种 Endpoint URL

签名用到的钥匙 (非对称加密 X.509).

如果想从 Azure Key Vault 拿 certificate 不能使用 Async 哦.

OpenIddict 作者的回复: Make the ConfigureServices method async in Startup.cs

Token 加密用到的钥匙 (我目前 autho 和 resource server 是同一台, 所以这里我选择用对称加密, 一把钥匙就好了, 如果是分开的情况那么 autho server 就要有 resource server 的公钥, 之前的文章有讲过)

它的钥匙也是支持 key rotation 的, 和 IdentityServer 类似. 你放多多把钥匙进去, 它选来用

最后的 UseDataProtection 是指 access token, refresh token 的加密, identity token 只能用 JWT 哦. (我看官网好像是推荐使用 Data Protection, 我觉得也挺好的, 毕竟 Identity Cookie 也是用 Data Protection)

接下来是 resource server 的 config (我的 autho 和 resource 是同一台, 所以是在一起 setup)

OIDC – OpenIddict Core

UseLocalServer 是因为我的 autho 和 resource 是同一台, 如果是分开的话, resource server 需要验证签名, 所以还需要 link 去 autho discover 发现 server 的公钥. 

同时解密 Token 也是需要 config 钥匙.

然后是 add test data, 这里的 test data 是指 client infomation

OIDC – OpenIddict Core

production 的情况下, client 数据 should be 已经在数据库里面了.

在来看看 client 是怎样输入的

OIDC – OpenIddict Core

 更新 2021-12-15: 补上很重要的 Logout

OIDC – OpenIddict Core

这里用 Insomnia (类似 Postman) 来做测试 

上半段注释掉的是 for client credentials flow 的, 下面是 authorization code flow + PKCE(前后端分离 web app 的 flow)

Insomnia.rest 是 client redirect URL, 

scope API 就是 resource server

openid 是要求返回 identity token 

offline_access 就是返回 refresh token 

具体测试画面长这样

OIDC – OpenIddict Core

setting 关掉 validate certificates (因为本地测试使用 self signed certificate)

OIDC – OpenIddict Core

不关掉会有 error

OIDC – OpenIddict Core

PKCE 不需要 client secret 但需要 code_verifier 和 code_challenge. (Insomnia 内置了)

所有的 token endpoint 我们需要自己做 (IdentityServer 4 是有封装的, OpenIddict 没有)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using OpenIddict.Validation.AspNetCore;

namespace AuthorizationServer.Controllers
{
    [ApiController]
    public class AuthorizationController : ControllerBase
    {
        private readonly SignInManager<User> _signInManager;
        public AuthorizationController(
            SignInManager<User> signInManager
        )
        {
            _signInManager = signInManager;
        }

        [HttpGet("~/connect/authorize")]
        [HttpPost("~/connect/authorize")]
        public async Task<IActionResult> Authorize()
        {
            var request = HttpContext.GetOpenIddictServerRequest() ??
                           throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

            // Retrieve the user principal stored in the authentication cookie.
            var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);

            // If the user principal can't be extracted, redirect the user to the login page.
            if (!result.Succeeded)
            {
                return Challenge(
                    authenticationSchemes: IdentityConstants.ApplicationScheme,
                    properties: new AuthenticationProperties
                    {
                        RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
                            Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
                    });
            }

            // Create a new claims principal
            var claims = new List<Claim>
            {
                // 'subject' claim which is required
                new Claim(OpenIddictConstants.Claims.Subject, result.Principal.Identity.Name),
                new Claim("some claim", "some value").SetDestinations(OpenIddictConstants.Destinations.AccessToken),
                new Claim(OpenIddictConstants.Claims.Email, "some@email").SetDestinations(OpenIddictConstants.Destinations.IdentityToken)
            };

            var claimsIdentity = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

            var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

            // Set requested scopes (this is not done automatically)
            claimsPrincipal.SetScopes(request.GetScopes());

            // Signing in with the OpenIddict authentiction scheme trigger OpenIddict to issue a code (which can be exchanged for an access token)
            return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }

        [HttpPost("~/connect/token")]
        public async Task<IActionResult> Exchange()
        {
            var request = HttpContext.GetOpenIddictServerRequest() ??
                          throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

            ClaimsPrincipal claimsPrincipal;

            if (request.IsClientCredentialsGrantType())
            {
                // Note: the client credentials are automatically validated by OpenIddict:
                // if client_id or client_secret are invalid, this action won't be invoked.

                var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

                // Subject (sub) is a required field, we use the client id as the subject identifier here.
                identity.AddClaim(OpenIddictConstants.Claims.Subject, request.ClientId ?? throw new InvalidOperationException());

                // Add some claim, don't forget to add destination otherwise it won't be added to the access token.
                identity.AddClaim("some-claim", "some-value", OpenIddictConstants.Destinations.AccessToken);

                claimsPrincipal = new ClaimsPrincipal(identity);

                claimsPrincipal.SetScopes(request.GetScopes());
            }

            else if (request.IsAuthorizationCodeGrantType())
            {
                // Retrieve the claims principal stored in the authorization code
                claimsPrincipal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
            }

            else if (request.IsRefreshTokenGrantType())
            {
                // Retrieve the claims principal stored in the refresh token.
                claimsPrincipal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
            }

            else
            {
                throw new InvalidOperationException("The specified grant type is not supported.");
            }

            // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
            return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }

        [Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
        [HttpGet("~/connect/userinfo")]
        public async Task<IActionResult> Userinfo()
        {
            var claimsPrincipal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;

            return Ok(new
            {
                Name = claimsPrincipal.GetClaim(OpenIddictConstants.Claims.Subject),
                Occupation = "Developer",
                Age = 43
            });
        }


        [Authorize]
        [HttpPost("~/connect/logout")]
        public ActionResult LogOut()
        {
            // 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).
            _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(
                OpenIddictServerAspNetCoreDefaults.AuthenticationScheme
            );
        }
    }
}
View Code

相关文章:

  • 2022-01-07
  • 2021-06-23
  • 2022-12-23
  • 2022-01-21
  • 2021-09-29
  • 2021-08-09
  • 2021-04-15
  • 2022-02-06
猜你喜欢
  • 2021-10-26
  • 2021-09-11
  • 2022-12-23
  • 2022-12-23
  • 2022-01-07
  • 2022-12-23
  • 2021-06-14
相关资源
相似解决方案