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

预备知识

可能须要看一点点预备知识html

OAuth 2.0 不彻底简介: http://www.javashuo.com/article/p-gtjytgin-ch.html前端

OpenID Connect 不彻底简介: http://www.javashuo.com/article/p-dgnpoqsb-dc.htmlgit

 

回顾一下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的github

 

OpenID Connect 三种流程

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

 

三种Flow的Response Type的值

 

Hybrid Flow

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

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

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

 

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部分.

相关文章
相关标签/搜索