Asp.net core 学习笔记之 authen + autho + oidc + oauth + spa 第八篇 OAuth & OpenID Connect (OIDC)

摘要:
如果asp。netcore想做oauth,有三种选择。1.身份服务器4原本是首选,但在2020年他们开了一家公司,身份服务器5将收费https://leastprivilege.com/2020/10/01/the-future-of-identityserver/2.AzureActiveDirectoryB2CMicrosoft云sasshttps://devblogs.

asp.net core 要做 oauth 的话有 3 个选择

1. identity server 4 

这个本来是首选的, 但 2020 他们开了公司, identity server 5 是收费的了

https://leastprivilege.com/2020/10/01/the-future-of-identityserver/

2. Azure Active Directory B2C

微软云的 sass

https://devblogs.microsoft.com/aspnet/asp-net-core-6-and-authentication-servers/

asp.net core team 明确表示他们不会投入任何资源去搞类似 identity server 的东西, 虽然 asp.net core 5.0 开始, SPA 的 template 是依赖 identity server 4 的

6.0 也会依赖, 7.0 会有替代, 但是绝对没有计划要做类似的或者辅助类似的 library. 原因就是微软要卖 azure 产品. 

3. openiddict core 

https://github.com/openiddict/openiddict-core

这个是目前跟 identity server 4 最靠近的替代库了. 这篇主要聊这个

关于这 3 个对比可以参考 : 

https://www.thinktecture.com/en/identity/three-alternatives-to-identityserver/

https://kevinchalet.com/2021/05/24/asp-net-core-6-and-authentication-servers-the-real-bait-and-switch-is-not-the-one-you-think/ (openiddict 作者写的)

好进入正题

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

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

一些重要的 refer: 

https://dev.to/robinvanderknaap/setting-up-an-authorization-server-with-openiddict-part-i-introduction-4jid (step by step tutorial, postman 作为 client)

https://damienbod.com/2017/04/11/implementing-openid-implicit-flow-using-openiddict-and-angular/ (step by step tutorial 结合 angular)

https://github.com/openiddict/openiddict-core (项目源码)

https://documentation.openiddict.com/guide/getting-started.html (官网文档)

https://github.com/openiddict/openiddict-samples (各种 sample, clone 下来看源码)

https://documentation.openiddict.com/configuration/index.html (config 的一些细节)

我这里只是记入一下 openiddict 的大概 setup 对应的关系解释, 并不会 step by step 的教, 要整个搞起来建议跟着上面的做

startup.cs 说起

openiddict 是使用 ef core 来搞的 (好像是可以换 storage, 但我就是用 ef core 的所以也没有去研究啦)

使用 extension UseOpenIddict 来进行 setup 

Asp.net core 学习笔记之 authen + autho + oidc + oauth + spa 第八篇 OAuth & OpenID Connect (OIDC)第1张

这里和 identity server 4 的 setup 方式不太一样哦. library 如果要结合 ef core 的话, 我目前觉得 identity server 4 的方式会比较正确. 就是使用分开的 migrations 

Asp.net core 学习笔记之 authen + autho + oidc + oauth + spa 第八篇 OAuth & OpenID Connect (OIDC)第2张

 openiddict 用的方式是 ReplaceService

Asp.net core 学习笔记之 authen + autho + oidc + oauth + spa 第八篇 OAuth & OpenID Connect (OIDC)第3张

通过 ReplaceService, openiddict 就可以拦截到 entity model builder

Asp.net core 学习笔记之 authen + autho + oidc + oauth + spa 第八篇 OAuth & OpenID Connect (OIDC)第4张

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

接下来就是 AddOpenIddict() 了, 

Asp.net core 学习笔记之 authen + autho + oidc + oauth + spa 第八篇 OAuth & OpenID Connect (OIDC)第5张

AddQuartz 是因为 openiddict 需要定时去 database clear 掉过期的 access token, 所以需要依赖一个 server task library, 而它选了 Quartz ... 竟然不选 hangfire /.

然后是 .AddCore 它负责 setup ef core 和 quartz, 没什么特别的

Asp.net core 学习笔记之 authen + autho + oidc + oauth + spa 第八篇 OAuth & OpenID Connect (OIDC)第6张

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

Asp.net core 学习笔记之 authen + autho + oidc + oauth + spa 第八篇 OAuth & OpenID Connect (OIDC)第7张

里面有: 

token 的寿命 

支持的 flow 

各种 endpoint url 

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

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

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

最后的 UseDataProtection 是指 access token, refresh token 的加密, identity token 好像是只能用 jwt. (我看官网好像是推荐使用 data protection, 我觉得也挺好的, 毕竟 identity cookie 也是用这个嘛)

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

Asp.net core 学习笔记之 authen + autho + oidc + oauth + spa 第八篇 OAuth & OpenID Connect (OIDC)第8张

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

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

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

Asp.net core 学习笔记之 authen + autho + oidc + oauth + spa 第八篇 OAuth & OpenID Connect (OIDC)第9张

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

来看看 client 是怎样输入的

Asp.net core 学习笔记之 authen + autho + oidc + oauth + spa 第八篇 OAuth & OpenID Connect (OIDC)第10张

我这里是用 insomnia (类似 postman) 来做测试 

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

insomnia.rest 是 client redirect url, 

scope api 就是 resource server

open id 是要求返回 identity token 

offline_access 就是返回 refresh token 

具体测试画面长这样

Asp.net core 学习笔记之 authen + autho + oidc + oauth + spa 第八篇 OAuth & OpenID Connect (OIDC)第11张

pkce 不需要 client secret 但需要 code_verifier 和 code_challenge 哦 (insomnia 内置了)

所有的 token endpoint 我们需要自己做 (identity server 是有封装的 /.)

Asp.net core 学习笔记之 authen + autho + oidc + oauth + spa 第八篇 OAuth & OpenID Connect (OIDC)第12张Asp.net core 学习笔记之 authen + autho + oidc + oauth + spa 第八篇 OAuth & OpenID Connect (OIDC)第13张
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
    {
        [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(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
        [HttpPost("~/connect/logout")]
        public ActionResult LogOut()
        {
            return SignOut(
                IdentityConstants.ApplicationScheme,
                OpenIddictServerAspNetCoreDefaults.AuthenticationScheme
            );
        }
    }

}
View Code

最后是 resource server 的 api, 大概长这样. 

Asp.net core 学习笔记之 authen + autho + oidc + oauth + spa 第八篇 OAuth &amp; OpenID Connect (OIDC)第14张

全部大概就是这样了. 可以看出来上面这些只是 simple play around 而已. 

要在项目上真的跑起来的话, 还需要搞定 : 

1. client 进数据库

2. 证书

3. 登入页面和同意 (consent) 页面, assign claims

4. web app 的部分 (之前是靠 identity server 4 封装 https://github.com/IdentityModel/oidc-client-js)

这次从 identity server 4 换取 openiddict 算是往下层走.. 对一些东西有了更多的基础认识..偶尔接触一下也是不错的啦. 

免责声明:文章转载自《Asp.net core 学习笔记之 authen + autho + oidc + oauth + spa 第八篇 OAuth &amp;amp; OpenID Connect (OIDC)》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Spring 中的观察者模式ajax jsonp函数调用下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

随便看看

Echarts实践-实现3D地球

我昨天被拉进了一个项目组,我将与Echarts相关的一些任务联系。老实说,在此之前,Echarts Js已经被使用过,但它很少见,也很肤浅。我会学习并做好准备。因为没有实际数据,我将使用所有模拟数据。首先,我创建了一个关于编码的新项目,然后初始化项目vueinitwebpack……这些跳过。该项目引入了cnpminstallcharts--save;cnpm...

zlog 使用手册

Zlog是一个纯C日志函数库,具有高可靠性、高性能、线程安全性、灵活性和清晰的概念。Syslog是一个系统级的轮子,但它的速度慢,功能单调。Zlog比log4c更高效、更实用、更安全,它是用c编写的。Zlog使用了C99兼容的vsnprintf。...

allure报告实现保存失败用例截图功能

allure中可以保存日志信息和截图日志allure能够自动识别。截图需要自己在添加allure方法。...

利用油猴插件实现全网VIP视频免费看

利用油猴插件实现全网VIP视频免费看第一步:首先打开谷歌应用商店搜索tampermonkey安装这个插件第二步:在百度搜索框搜索油猴可以看到以下页面,点击进入。下载谷歌上网助手解压后,将后缀为crx的文件拖入即可。之后注册一个谷歌上网助手账后登录即可进入谷歌应用商店油猴插件...

wxparse使用(富文本插件)

优点:唯一已知的可以将HTML转换为小程序识别的插件缺点:转换HTML标签可能需要大量的微信小程序标签和样式配置:步骤1,下载https://github.com/icindy/wxParse第二步:把它放到项目中。我选择页面目录。步骤3:配置wxml以添加:需要时使用:...

【转】 中兴OLT-C300常用命令

在当前的C220版本中,ONU类型名称在GPON和EPON中应该是唯一的。这里我们使用“ZTEG-F620”。ZXAN#ponZXAN#onu-typegponZTEG-F620描述4ETH,2POTSZXAN#onu-ifZTEG-F620eth_0/1-4ZXAN#onon-ifZTEG-F620pots_0/1-2ZXAN#on u type attr...