IdentityServer4系列 | 简化模式

一、前言

从上一篇关于资源密码凭证模式中,通过使用client_id和client_secret以及用户名密码通过应用Client(客户端)直接获取,从而请求获取受保护的资源,但是这种方式存在client可能存了用户密码这不安全性问题,所以需要做到client是高可信的应用。因此,我们可以考虑通过其他方式来解决这个问题。

我们通过Oauth2.0的「简化授权」模式了解到,可以使用这种方式来解决这个问题,让用户自己在IdentityServer服务器进行登录验证,客户端不需要知道用户的密码,从而实现用户密码的安全性。.

所以在这一篇中,我们将通过多种授权模式中的「简化授权」模式进行说明,主要针对介绍「IdentityServer」保护API的资源,「简化授权」访问API资源。

二、初识

有些 Web 应用是纯前端应用,没有后端,必须将令牌储存在前端。RFC 6749 就规定了这种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)"简化"(implicit)。

「简化模式」(implicit grant type)「不通过第三方应用程序的服务器」,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤(授权码模式后续会说明)。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。

这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。

2.1 适用范围

这种模式的使用场景是基于浏览器的应用

这种模式基于安全性考虑,建议把token时效设置短一些, 不支持refresh token

2.2  授权流程:

 +----------+
     | Resource |
     |  Owner   |
     |          |
     +----------+
          ^
          |
         (B)
     +----|-----+          Client Identifier     +---------------+
     |         -+----(A)-- & Redirection URI --->|               |
     |  User-   |                                | Authorization |
     |  Agent  -|----(B)-- User authenticates -->|     Server    |
     |          |                                |               |
     |          |<---(C)--- Redirection URI ----<|               |
     |          |          with Access Token     +---------------+
     |          |            in Fragment
     |          |                                +---------------+
     |          |----(D)--- Redirection URI ---->|   Web-Hosted  |
     |          |          without Fragment      |     Client    |
     |          |                                |    Resource   |
     |     (F)  |<---(E)------- Script ---------<|               |
     |          |                                +---------------+
     +-|--------+
       |    |
      (A)  (G) Access Token
       |    |
       ^    v
     +---------+
     |         |
     |  Client |
     |         |
     +---------+
     

「简化授权流程描述」

(A)客户端携带客户端标识以及重定向URI到授权服务器;

(B)用户确认是否要授权给客户端;

(C)授权服务器得到许可后,跳转到指定的重定向地址,并将令牌也包含在了里面;

(D)客户端不携带上次获取到的包含令牌的片段,去请求资源服务器;

(E)资源服务器会向浏览器返回一个脚本;

(F)浏览器会根据上一步返回的脚本,去提取在C步骤中获取到的令牌;

(G)浏览器将令牌推送给客户端。

2.2.1 过程详解


访问令牌请求
参数 是否必须 含义
response_type 必需 表示授权类型,此处的值固定为"token"
client_id 必需 客户端ID
redirect_uri 可选 表示重定向的URI
scope 可选 表示授权范围。
state 可选 表示随机字符串

「(1)资源服务器生成授权URL并将用户重定向到授权服务器」

  (用户的操作:用户访问https://resourcesServer/index.html跳转到登录地址,选择授权服务器方式登录)

在授权开始之前,它首先生成state参数(随机字符串)。client端将需要存储这个(cookie,会话或其他方式),以便在下一步中使用。

第一步,A 网站提供一个链接,要求用户跳转到 B 网站,授权用户数据给 A 网站使用。

https://oauth2Server/oauth2/default/v1/authorize?
response_type=token
&client_id=${clientId}
&redirect_uri=https://resourcesServer/implicit.html
&scope=授权范围
&state=随机字符串

生成的授权URL如上所述(如上),请求这个地址后重定向访问授权服务器,其中 response_type参数为token,表示直接返回令牌。

「(2)验证授权服务器登陆状态」

(用户的操作:如果未登陆用账号 User,密码12345登陆https://oauth2Server/login,如果已登陆授权服务器不需要此步骤)

如果未登陆账号,自动跳转到授权服务器登陆地址,登陆授权服务器以后用户被重定向client端

https://resourcesServer/implicit.html  

如已提前登陆授权服务器或授权服务器登陆会话还存在自动重定向到client端

https://resourcesServer/implicit.html

「(3)验证状态参数」

(用户的操作:无需操作)

用户被重定向回客户机,URL中现在有一个片段包含访问令牌以及一些其他信息。

用户跳转到 B 网站,登录后同意给予 A 网站授权。这时,B 网站就会跳回redirect_uri参数指定的跳转网址,并且把令牌作为 URL 参数,传给 A 网站。

https://resourcesServer/authorization-code.html

\#access_token=&token_type=Bearer&expires_in=3600&scope=photo&state=随机字符串

其中,token参数就是令牌,A网站因此直接在前端拿到令牌。

注意,令牌的位置是 URL 锚点(fragment),而不是查询字符串(querystring),这是因为 OAuth 2.0 允许跳转网址是 HTTP 协议,因此存在"中间人攻击"的风险,而浏览器跳转时,锚点不会发到服务器,就减少了泄漏令牌的风险。

用户使用这个令牌访问资源服务器,当令牌失效时使用刷新令牌去换取新的令牌

三、实践

在示例实践中,我们将创建一个授权访问服务,定义一个MVC客户端,MVC客户端通过「IdentityServer」上请求访问令牌,并使用它来访问API。

3.1 搭建 Authorization Server 服务

搭建认证授权服务

3.1.1 安装Nuget包

IdentityServer4 程序包

3.1.2 配置内容

建立配置内容文件Config.cs

    public static class Config
    {
        public static IEnumerable<IdentityResource> IdentityResources =>
            new IdentityResource[]
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
            };


        public static IEnumerable<ApiScope> ApiScopes =>
            new ApiScope[]
            {
                new ApiScope("Implicit_scope1")
            };

        public static IEnumerable<ApiResource> ApiResources =>
            new ApiResource[]
            {

                new ApiResource("api1","api1")
                {
                    Scopes={ "Implicit_scope1" },
                    ApiSecrets={new Secret("apipwd".Sha256())}  //api密钥
                }
            };

        public static IEnumerable<Client> Clients =>
            new Client[]
            {
                 new Client
                {
                    ClientId = "Implicit_client",
                    ClientName = "Implicit Auth",
                    AllowedGrantTypes = GrantTypes.Implicit,
                    RedirectUris ={
                    "http://localhost:5002/signin-oidc",  //跳转登录到的客户端的地址
                    },
                    PostLogoutRedirectUris ={
                        "http://localhost:5002/signout-callback-oidc",//跳转登出到的客户端的地址
                    },      
                    AllowedScopes = {
                           IdentityServerConstants.StandardScopes.OpenId,
                            IdentityServerConstants.StandardScopes.Profile,
                         "Implicit_scope1"
                     },
                      // 是否需要同意授权 (默认是false)
                      RequireConsent=true
                 }, 
            };
    }

RedirectUris : 登录成功回调处理的客户端地址,处理回调返回的数据,可以有多个。

PostLogoutRedirectUris :跳转登出到的客户端的地址。

这两个都是配置的客户端的地址,且是identityserver4组件里面封装好的地址,作用分别是登录,注销的回调

因为是「简化」授权的方式,所以我们通过代码的方式来创建几个测试用户。

新建测试用户文件TestUsers.cs

    public class TestUsers
    {
        public static List<TestUser> Users
        {
            get
            {
                var address = new
                {
                    street_address = "One Hacker Way",
                    locality = "Heidelberg",
                    postal_code = 69118,
                    country = "Germany"
                };

                return new List<TestUser>
                {
                    new TestUser
                    {
                        SubjectId = "1",
                        Username = "i3yuan",
                        Password = "123456",
                        Claims =
                        {
                            new Claim(JwtClaimTypes.Name, "i3yuan Smith"),
                            new Claim(JwtClaimTypes.GivenName, "i3yuan"),
                            new Claim(JwtClaimTypes.FamilyName, "Smith"),
                            new Claim(JwtClaimTypes.Email, "i3yuan@email.com"),
                            new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
                            new Claim(JwtClaimTypes.WebSite, "http://i3yuan.top"),
                            new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json)
                        }
                    }
                };
            }
        }
    }

返回一个TestUser的集合。

通过以上添加好配置和测试用户后,我们需要将用户注册到IdentityServer4服务中,接下来继续介绍。

3.1.3 注册服务

在startup.cs中ConfigureServices方法添加如下代码:

        public void ConfigureServices(IServiceCollection services)
        {
            var builder = services.AddIdentityServer()
               .AddTestUsers(TestUsers.Users); //添加测试用户

            // in-memory, code config
            builder.AddInMemoryIdentityResources(Config.IdentityResources);
            builder.AddInMemoryApiScopes(Config.ApiScopes);
            builder.AddInMemoryApiResources(Config.ApiResources);
            builder.AddInMemoryClients(Config.Clients);

            // not recommended for production - you need to store your key material somewhere secure
            builder.AddDeveloperSigningCredential();
        }

3.1.4 配置管道

在startup.cs中Configure方法添加如下代码:

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseRouting();
            app.UseIdentityServer();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Hello World!");
                });
            });
        }

以上内容是快速搭建简易IdentityServer项目服务的方式。

「这搭建 Authorization Server 服务跟上一篇资源密码凭证模式有何不同之处呢?」

  1. 在Config中配置客户端(client)中定义了一个AllowedGrantTypes的属性,这个属性决定了Client可以被哪种模式被访问,「GrantTypes.Implicit」「简化授权」。所以在本文中我们需要添加一个Client用于支持简化授权(「implicit」)。
  2. 「简化授权不通过第三方应用程序的服务器」,直接在浏览器中向认证服务器申请令牌,所有步骤在浏览器中完成,所以需要配置对应的回调地址和登出地址。这也是不同于之前的「资源所有者凭证模式」

3.2 搭建MVC 客户端

实现对客户端认证授权访问资源

3.2.1 快速搭建一个MVC项目

3.2.2 安装Nuget包

IdentityServer4.AccessTokenValidation 包

3.2.3 注册服务

要将对 OpenID Connect 身份认证的支持添加到MVC应用程序中。

在startup.cs中ConfigureServices方法添加如下代码:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
        services.AddAuthorization();

        services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            })
               .AddCookie("Cookies")
              .AddOpenIdConnect("oidc", options =>
              {
                  options.Authority = "http://localhost:5001";
                  options.RequireHttpsMetadata = false;
                  options.ClientId = "Implicit_client";
                  options.SaveTokens = true;
                  options.GetClaimsFromUserInfoEndpoint = true;
              });
    }
  1. AddAuthentication注入添加认证授权,当需要用户登录时,使用 cookie 来本地登录用户(通过“Cookies”作为DefaultScheme),并将 DefaultChallengeScheme 设置为“oidc”,

  2. 使用 AddCookie 添加可以处理 cookie 的处理程序。

  3. 因为「简化模式」的实现是就是 OpenID Connect,所以在AddOpenIdConnect用于配置执行 OpenID Connect 协议的处理程序。Authority表明之前搭建的 IdentityServer 授权服务地址。然后我们通过ClientId。识别这个客户端。SaveTokens用于在 cookie 中保留来自IdentityServer 的令牌。

3.2.4 配置管道

然后要确保认证服务执行对每个请求的验证,加入UseAuthenticationUseAuthorizationConfigure中,在startup.cs中Configure方法添加如下代码:

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }    
            app.UseRouting();
            app.UseCookiePolicy();
            app.UseAuthentication();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
            });
        }

UseAuthentication将身份验证中间件添加到管道中;

UseAuthorization 将启动授权中间件添加到管道中,以便在每次调用主机时执行身份验证授权功能。

3.2.5 添加授权

在HomeController控制器并添加[Authorize]特性到其中一个方法。在进行请求的时候,需进行认证授权通过后,才能进行访问。

        [Authorize]
        public IActionResult Privacy()
        {
            ViewData["Message"] = "Secure page.";
            return View();
        }

还要修改主视图以显示用户的Claim以及cookie属性。

@using Microsoft.AspNetCore.Authentication

<h2>Claims</h2>

<dl>
    @foreach (var claim in User.Claims)
    {
        <dt>@claim.Type</dt>
        <dd>@claim.Value</dd>
    }
</dl>

<h2>Properties</h2>

<dl>
    @foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items)
    {
        <dt>@prop.Key</dt>
        <dd>@prop.Value</dd>
    }
</dl>

访问 Privacy 页面,跳转到认证服务地址,进行账号密码登录,Logout 用于用户的注销操作。

3.3 效果

3.3.1 项目测试

IdentityServer4系列 | 简化模式

四、问题

4.1 SameSite策略

IdentityServer4系列 | 简化模式

在Chrome浏览器中,进行认证授权的时候,用户登录之后,无法跳转到原网页,还是停留在登录页中,可以看控制台就发现上图的效果。

最后查找资料发现,是Google将于2020年2月份发布Chrome 80版本。本次发布将推进Google的“渐进改良Cookie”策略,打造一个更为安全和保障用户隐私的网络环境。所以本次更新可能导致浏览器无法向服务端发送Cookie。如果你有多个不同域名的应用,部分用户很有可能出现会话时常被打断的情况,还有部分用户可能无法正常登出系统。

所以我们需要解决这个问题:

方法一:将域名升级为 HTTPS

方法二:使用代码修改 SameSite 设置

新增 「SameSiteCookiesServiceCollectionExtensions」 类 (可以下载源码查看)

private const SameSiteMode Unspecified = (SameSiteMode)(-1);
 
改为
 
private const SameSiteMode Unspecified = SameSiteMode.Lax;

如果没有域名或内网环境,可以使用该方法,在 Startup 添加引用。

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    ...
    services.ConfigureNonBreakingSameSiteCookies();
    ...

参考资料 Chrome80调整SameSite策略对IdentityServer4的影响以及处理方案

五、总结

  1. 本篇主要阐述以「简化授权」,编写一个MVC客户端,并通过客户端以浏览器的形式请求「IdentityServer」上请求获取访问令牌,从而访问资源。
  2. 「简化模式」解决了客户端模式用户身份验证和授权的问题,也解决了上一篇中「资源所有者密码凭证授权」面临的用户密码暴露的问题,是基于浏览器的应用。但由于token携带在url中,安全性方面不能保证,建议把token时效设置短一些
  3. 在后续会对在安全性方面做得更好的模式进行说明,数据库持久化问题,以及如何应用在API资源服务器中和配置在客户端中,会进一步说明。
  4. 如果有不对的或不理解的地方,希望大家可以多多指正,提出问题,一起讨论,不断学习,共同进步。
  5. 项目地址
https://github.com/i3yuan/Yuan.IdentityServer4.Demo/tree/main/DiffAuthMode/ImplicitMVC