Identity Server 4 - Hybrid Flow - MVC客户端身份验证

预备知识

可能需要看一点点预备知识

OAuth 2.0 不完全简介: https://www.cnblogs.com/cgzl/p/9221488.html

OpenID Connect 不完全简介: https://www.cnblogs.com/cgzl/p/9231219.html

回顾一下OAuth 2.0 和 OpenID Connect

OAuth 2.0 vs OpenID Connect 角色对应

客户端/依赖方的类型

OAuth 2.0 vs OpenID Connect 端点定义

OAuth 2.0 vs OpenID Connect 主要授权方式/流程对比

实际上OpenID Connect 是完全兼容OAuth 2.0的

OpenID Connect 三种流程

本系列文章主要关注OpenID Connect的三个流程

三种Flow的Response Type的值

Hybrid Flow

本文只介绍Hybrid Flow. 而根据其response_type的不同, 它又分为三种情况:

  • response_type=code id_token
  • response_type=code token
  • response_type=code id_token token

注意:为了表明是OpenID Connect协议的请求, scope参数里必须包含openid.

1. response_type=code id_token:

当reponse_type为这种类型的时候, 授权码和ID Token从授权端点发行返回, 然后Access Token 和 ID Token会从Token端点发行返回:

2. response_type=code token:

当reponse_type为这种类型的时候, 授权码和Access Token从授权端点发行返回, 然后Access Token 和 ID Token会从Token端点发行返回:

3. response_type=code id_token token:

当reponse_type为这种类型的时候, 授权码和Access Token和ID Token从授权端点发行返回, 然后Access Token 和 ID Token会从Token端点发行返回:

搭建Identity Server 4项目

Identity Server 4 是OpenID Connect和OAuth 2.0的框架, 它主要是为ASP.NET Core准备的. 它得到了OpenID基金会的官方认证. 它也是开源的, https://github.com/IdentityServer/IdentityServer4.

首先需要一个现成的API项目, 其实本文根本没用到: https://github.com/solenovex/Identity-Server-4-Tutorial-Code, 在该连接的00目录里. 

在此之上, 我再继续搭建Identity Server 4.

在该解决方案里建立一个ASP.NET Core Web Application:

由于Identity Provider 通常不是为某一个客户端项目或API资源所准备的, 所以该项目的名称通常独立于其它项目的名称. 在这里我教它Dave.IdentityProvider.

然后选择Empty模板, 并使用ASP.NET Core 2.1:

点击OK, 项目建立好之后, 为该项目安装Identity Server 4, 我通过Nuget:

随后是配置Identity Server 4.

打开Dave.IdentityProvider的Startup.cs, 在ConfigureServices里面调用 services.AddIdentityServer()来把Identity Server注册到ASP.NET Core的容器里面; 随后我调用了services.AddDeveloperSigningCredentials()方法, 它会创建一个用于对token签名的临时密钥材料(但是在生产环境中应该使用可持久的密钥材料):

然后需要添加资源和客户端, 按照官方文档的做法, 我添加一个Config类:

这里我首先添加了一个GetUsers()方法, 里面有两个最终用户.

注意TestUser的SubjectId属性的值, 在这个Identity Provider里面必须是唯一的.

每个用户下面还有个Claims属性, claims里面都是代表用户的一些信息.

但是如何让这些claims通过Identity Token返回来呢?

Claims 与 Scope 是紧密相连的, 是多对一的. 下面我建立一个方法来返回Scope:

在这里IdentityResource映射于那些关于用户信息的scope, 后边还要介绍ApiResource, 它映射于API资源的scopes. IdentityResource就是一些关于用户身份的数据, 例如user ID, name, email等等. 每个Identity Resource都有一个唯一的名称, 你可以为它赋一些claims, 然后这些claims就会包含在该用户的Identity Token里面(这只是一种情况), 客户端需要使用scope参数来请求访问某个identity resource.

OpenID Connect协议里的scopes可以理解为一组预定义的claims的简称. 

OpenID Connect预定义了几组标准的scopes 或者叫 identity resources:

  • openid, 这个是必须的. 它会为用户提供一个唯一的ID, 也叫做subject id. 它的出现也就是告诉授权服务器客户端发出的是OpenID Connect 请求. 它同时也要求返回ID Token. 如果 response_type 含有 "token" (指的是Access Token), 那么ID Token在授权的响应里和Access Token一同返回; 如果response_type 包含 "code" (指授权码), 那么ID Token会作为Token端点响应的一部分返回.
  • profile, 这个是可选的. 这个scope请求访问的是最终用户的个人资料, 但是不包括email, address和phone, 它包括的claims有: name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, 和 updated_at.
  • email, 这个是可选的. 它包括 emailemail_verified 两个claims.
  • address, 这个是可选的. 它只有address 一个claim.
  • phone, 这个是可选的. 它包括 phone_numberphone_number_verified 两个claims.

其中通过profile, email, address, phone这四个scope请求的claims, 如果请求的response_type的值包含"token"(指的是access token), 那么这些claims是从用户信息端点(UserInfo Endpoint)返回的. 而如果response_type不包含Access Token, 那么这些claims是在ID Token里面返回.

Identity Server 4的IdentityResources类里面包含着上述这5个预定义的scopes.

所以上面方法里TestUser的given_name和family_name将会在ID_Token里面返回.

最后, 还需要定义客户端:

暂时它还只是返回一个空的集合.

这个Config类先到这, 现在还需要再修改一下Startup里的ConfigureServices方法, 把上面Config里面的配置都加进去:

然后修改Startup里的Configure方法, 把IdentityServer添加到ASP.NET Core的管道里:

启用TLS(SSL)

我直接修改的launchSettings.json文件, 只保留了这一部分.

然后运行程序, 访问该网址: https://localhost:5001/.well-known/openid-configuration, 会得到以下画面就说明Identity Server 4配置成功了:

为Identity Server 4 添加UI

Identity Server 4 的UI可以在这里找到: https://github.com/IdentityServer/IdentityServer4.Quickstart.UI

根据文档描述, 在Dave.IdentityProvider项目目录下打开Powershell执行这句话即可安装UI:

iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/IdentityServer/IdentityServer4.Quickstart.UI/release/get.ps1'))

安装好之后可以看到项目文件的变化:

但是由于这套UI使用了ASP.NET Core MVC, 所以我还需要再配置一些东西.

在Startup的ConfigureServices里, 注册MVC:

在Startup的Configure里, 在管道里使用静态文件和MVC:

再次运行程序, 首页如下:

点击discovery document, 它就是我之前打开的那个页面.

ASP.NET Core MVC 作为客户端

首先考虑ASP.NET Core MVC 作为客户端应用的情况.

ASP.NET Core MVC是机密客户端(Confidential Client), 它是传统的服务器端Web应用.

它需要长时间访问(long-lived access), 所以需要refresh token. 那么它可以使用Authorization Code FlowHybrid Flow.

在这里Hybrid Flow是相对高级一些的, 它可以让客户端首先从授权端点获得一个ID Token并通过浏览器(front-channel)传递过来, 这样我们就可以验证这个ID Token. 如果验证成功然后, 客户端再打开一个后端通道(back-channel), 从Token端点获取Access Token.

下面是OpenID Connect官方文档给出的一个身份认证请求的例子.

第一行的URI: "/authorize" 就是授权端点(Authorization Endpoint), 它位于身份提供商(Identity provider, IDP)那里. 这个URI可以从前面介绍的discovery document里面找到.

第二行 response_type=code id_token, 它决定了采取了哪一种Hybrid流程(参考上面那三个图).

第三行 client_id=xxxx, 这是客户端的身份标识.

第四行 redirect_uri=https...., 这是客户端那里的重定向端点(Redirection Endpoint).

第五行 scope=openid profile email, 这就是客户端所请求的scopes.

再看一遍这张图:

为什么要返回两次ID Token呢? 这是因为第(4)步里面请求Token的时候要求客户端身份认证, 这时请求Token的时候需要提供Authorization Code, Client ID和 Client Secret, 这些secret并不暴露给外界, 这些东西是由客户端服务器通过后端通道传递给Token端点的. 而第一次获得的ID Token是从前端通道(浏览器)返回的.  当这个ID Token被验证通过之后, 也就证明了当前用户到底是谁.

下面简单对比一下前端和后端通道:

创建ASP.NET Core MVC 客户端

创建好后回到IdentityProvider项目, 添加一个Client:

这里ClientName是客户端名称, 它会出现在用户同意授权的页面. 流程选择的是Hybrid. 这里暂时只请求OpenId这一个Scope, 以便只返回ID Token, 在GetIdentityResources()方法里我知道支持这个scope. 这个流程的授权码和tokens是通过跳转来传递到浏览器的URI上面的, 所以我需要一个URI来接收这些东西, 而RedirectUris里面的URI就是允许做这个工作的URI.

下面继续配置MVC客户端 (官方文档: https://identityserver4.readthedocs.io/en/release/quickstarts/3_interactive_login.html#creating-an-mvc-client).

在MVC客户端的Startup的ConfigureServices里:

下面的文字都是翻译的官方文档.

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); 这句话是指, 我们关闭了JWT的Claim 类型映射, 以便允许well-known claims.

这样做, 就保证它不会修改任何从Authorization Server返回的Claims.

这里通过调用services.AddAuthentication()方法来添加和配置身份认证中间件.

这里我们使用Cookie作为验证用户的首选方式, 而DefaultScheme = "Cookies", 这个"Cookies"字符串是可以任意填写的, 只要与后边的一致即可. 但是如果同一个服务器上有很多应用的话, 这个Scheme的名字不能重复.

而把DefaultChanllangeScheme设为"oidc", 这个名字与后边配置OpenIdConnect的名字要一样. 当用户需要登陆的时候, 将使用的是OpenId Connect Scheme.

然后的AddCookie, 其参数是之前配置的DefaultScheme名称, 这配置了Cookie的处理者, 并让应用程序为我们的DefaultScheme启用了基于Cookie的身份认证. 一旦ID Token验证成功并且转化为Claims身份标识后, 这些信息就将会保存于被加密的Cookie里.

下面的AddOpenIdConnect()方法添加了对OpenID Connect流程的支持, 它让配置了用来执行OpenId Connect 协议的处理者.

这个处理者会负责创建身份认证请求, Token请求和其它请求, 并负责ID Token的验证工作.

它的身份认证scheme就是之前配置的"oidc", 它的意思就是如果该客户端的某部分要求身份认证的时候, OpenID Connect将会作为默认方案被触发(因为之前设置的DefaultChallengeScheme是"oidc", 和这里的名字一样).

SignInScheme和上面的DefaultScheme一致, 它保证身份认证成功的结果将会被保存在方案名为"Cookies"的Cookie里. 

Authority就是Identity Provider的地址.

ClientId和Secret要与IdentityProvider里面的值一样.

ResponseType就是前面介绍过的.

请求的Scope有openid和profile, 其实中间件默认也包括了这些scope, 但是写出来更明确一些.

SaveTokens=true, 表示允许存储从Identity Provider那里获得的tokens.

然后配置管道:

确保中间件在UseMvc()之前调用.

还要确保监听地址和IdentityProvider里面配置的Client一致:

然后我对HomeController要求身份认证:

随后修改一下About方法, 我仅仅是想展示token的数据:

这个token来自于cookie.

再修改About的页面:

下面测试一下MVC客户端的身份认证:

同时运行Identity Provider 和 Mvc 两个程序, 最好使用控制台, 这样如果有错误的话就可以方便的看到相关信息了.

在访问Mvc的首页时, 会自动跳转到Identity Provider上:

具体的请求可以通过Chrome的Developer Tools看到:

在Identity Provider的控制台上, 也可以看到相关信息:

登录用户之后, 就会看到征求用户同意授权的页面:

点击Yes即可.

然后浏览器会调转会MVC Client, 通过Chrome的工具查看:

可以看到跳转回来的时候是到了signin-oidc这个地址, 它就是我之前在Identity Provider里面Client的RedirectUri.

与此同时, 可以在Identity Provider的控制台看到, MVC客户端通过后端通道向Token端点发出了Token请求, 这个过程用户是不会发现的:

这个过程就和前面图示的一样, 最后从token端点请求到新的ID Token之后, 会再次进行验证, 然后会通过它创建Claims Identity, 也就是前面代码里的User.Claims.

这个身份验证的凭据都会保存在加密的Cookie里面:

来到About菜单:

最上面可以看到ID Token的值.

sid是sessionid.

sub是用户的subjectid

idp是本地的.

我们可以在jwt.io来解析一下这个ID Token

解码之后的ID Token:

这里的内容以后再讲.

登录好用之后, 就考虑一下登出.

再_Layout.cshtml里面添加登出按钮, 这部分官方文档都有:

然后建立Action方法:

首先要清除本地的Cookie, 这个Cookie的名字要与之前配置的默认方案里的名字一致, 这一步就相当于登出MVC客户端.

后一行代码的作用是跳转回到Identity Provider, 然后用户可以继续登出IDP, 也就是IDP会清除它的Cookie.

但是登出之后, 用户会留在Identity Provider那里:

查看IDP的控制台, 可以看到这个失败: Invalida post logout URI:

这是因为我们配置Client的时候没有指定在登出之后的跳转URI地址.

回到IDP的客户端配置那里:

添加PostLogoutRedirectUris属性, 里面这个值是就是默认的登出后跳转地址.

再次操作后, 效果如下:

点击here之后会回到MVC客户端, 然后由于权限问题会又立即跳转到IDP.

如果想让这个过程自动跳转, 可以修改IDP的Quickstart/Account/AccountOptions类里面的这个值改成true:

再次操作, 跳转就是自动完成的了.

用户信息节点

查看解码的ID Token, 可以看到里面包含了这些claims:

这里除了sub之外, 并没有关于用户的其他信息.

我们可以通过指定参数来要求在ID Token里面返回用户其他的claims, 但是由于id token是从URI进行传输的, 而浏览器会有URI的长度限制, 所以尽量让token小点, 以免超限.

为了获得用户其他的claims, 客户端应用可以使用用户信息端点, 这需要用access token和相关claims对应的scopes.

首先在MVC客户端配置, GetClaimsFromUserInfoEndpoit为true, 并请求profile scope:

随后在IDP那里为MVC Client添加上profile scope:

再次执行操作, 回到About页面:

可以看到profile scope里对应的这两个claims值已经出来了.

再把ID Token到jwt.io去解码一下:

可以看到这两个claims并不在ID Token里面, 这就说明它们来自用户信息端点.

在ID Token里面的东西(官方文档有介绍: http://openid.net/specs/openid-connect-core-1_0.html#IDToken):

sub是用户的subjectid, 也就是用户的身份标识.

iss是ID Token的发行者.

aud是这个token的目标观众, 这里就是MVC客户端的clientid.

nbf是指在这个时间之前, ID Token是不被接受的.

exp是ID Token的过期时间.

iat是这个JWT token发行的时间.

auth_time是原始身份认证的时间.

amr是指身份认证的方法. 这里用的是pwd, 密码.

nonce, 它是Number only to be used once的意思. 它是一个字符串, 使用ID Token和客户端Session关联, 来减少重复攻击.

最后是at_hash, 其实还有c_hash, 它们分别代表Access Token Hash和Code Hash. 就是通过某种方式对Access Token和Code的Base64编码. 它们可以用来把Access Token或Authorization Code链接到这个ID Token上.

今天先到这.

代码在: https://github.com/solenovex/Identity-Server-4-Tutorial-Code 的01部分.

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Debian社区

Debian 10 Buster 将确保自动安装安全升级

Debian Project的Cyril Brulebois本周早些时候很高兴地宣布,即将释出的Debian GNU/Linux 10 “Buster”操作系统...

1392
来自专栏LuckQI

Linux学习桌面系统生成快捷方式与普通用户权限

说了这么多其实还是建议如果有环境的话,程序员还是在Linux环境下开发的好。虽然刚开始有点难,但是后面会发现有很多好处。那么我们在Linux系统下开发首先会遇到...

920
来自专栏黑白安全

Nginx中如何限制某个IP同一时间段的访问次数

如何设置能限制某个IP某一时间段的访问次数是一个让人头疼的问题,特别面对恶意的ddos攻击的时候。其中CC攻击(Challenge Collapsar)是DDO...

1614
来自专栏高性能服务器开发

关于windows完成端口(IOCP)的一些理解(三)

系列目录 关于windows完成端口(IOCP)的一些理解(一) 关于windows完成端口(IOCP)的一些理解(二) 关于windows完成端口(IOCP)...

3406
来自专栏乐沙弥的世界

Vmware server 下为Oracle RAC 添加共享磁盘

    在VMware下的Oracle RAC 环境中,对于共享存储不够或者需要增加新的共享磁盘来配置ocr或votingdisk的多路镜像,我们可以通过vmw...

1761
来自专栏张善友的专栏

创建安全的ASP.NET虚拟主机

随着基于.net平台的应用程序的数量级增加,提供.net空间的虚拟主机商也越来越多,但是有不少的虚拟主机提供商对.net环境的权限分配并不是非常的熟悉,从而导致...

32810
来自专栏逸鹏说道

ASP.NET5 Beta8可用性

ASP.NET5 beta8现已上都的NuGet作为一个工具升级到Visual Studio2015!此版本极大地扩展.NET核心对OS X和Linux所支持的...

38416
来自专栏维恩的派VNPIE

如何手动搭建vnpy环境

请先搭建好运行环境。 编程环境其实就是选一个IDE,Visual Studio或者PyCharm都可以。

3782
来自专栏张善友的专栏

ASP.NET2.0应用中定制安全凭证

阅读提要 在缺省状况下,你只能使用Visual Studio 2005的一个本机实例来管理与ASP.NET 2.0一同发行的SQL Server数据库中的安全凭...

1939
来自专栏张善友的专栏

.NET Web 自动化测试工具

Inspired by Watir development of WatiN started in December 2005 to make a simila...

1999

扫码关注云+社区

领取腾讯云代金券