使用Identity Server 4创建Authorization Server (4)

预备知识: http://www.cnblogs.com/cgzl/p/7746496.htmlhtml

第一部分: http://www.cnblogs.com/cgzl/p/7780559.htmlweb

第二部分: http://www.cnblogs.com/cgzl/p/7788636.html数据库

第三部分: http://www.cnblogs.com/cgzl/p/7793241.htmljson

上一篇讲了使用OpenId Connect进行Authentication.api

下面讲浏览器

Hybrid Flow和Offline Access

目前咱们解决方案里面有三个项目 Authorization Server, Web api和Mvc Client. 在现实世界中, 他们可能都在不一样的地方.cookie

如今让咱们从MvcClient使用从Authorization Server获取的token来访问web api. 而且确保这个token不过时.网络

如今咱们的mvcClient使用的是implicit flow, 也就是说, token 被发送到client. 这种状况下 token的生命可能很短, 可是咱们能够重定向到authorization server 从新获取新的token.mvc

例如, 在SPA(Single Page Application)中, implicit flow基本上就是除了resource owner password flow 之外惟一合适的flow, 可是咱们的网站可能会在client(SPA client/或者指用户)没使用网站的时候访问api, 为了这样作, 不但要保证token不过时, 咱们还须要使用别的flow. 咱们要介绍一下authorization code flow. 它和implicit flow 很像, 不一样的是, 在重定向回到网站的时候获取的不是access token, 而是从authorization server获取了一个code, 使用它网站能够交换一个secret, 使用这个secret能够获取access token和refresh tokens.async

Hybrid Flow, 是两种的混合, 首先identity token经过浏览器传过来了, 而后客户端能够在进行任何工做以前对其验证, 若是验证成功, 客户端就会再打开一个通道向Authorization Server请求获取access token.

首先在Authorization server的InMemoryConfiguration添加一个Client:

new Client
                {
                    ClientId = "mvc_code",
                    ClientName = "MVC Code Client",
                    AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
                    ClientSecrets =
                    {
                        new Secret("secret".Sha256()) },
                    RedirectUris = { "http://localhost:5002/signin-oidc" },
                    PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
                    AllowedScopes = new List<string>
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        IdentityServerConstants.StandardScopes.Email,
                        "socialnetwork"
                    },
                    AllowOfflineAccess = true,
                    AllowAccessTokensViaBrowser = true
                }

 

首先确定要修改一下ClientId.

GrantType要改为Hybrid或者HybrdAndClientCredentials, 若是只使用Code Flow的话不行, 由于咱们的网站使用Authorization Server来进行Authentication, 咱们想获取Access token以便被受权来访问api. 因此这里用HybridFlow.

还须要添加一个新的Email scope, 由于我想改变api来容许我基于email来建立用户的数据, 由于authorization server 和 web api是分开的, 因此用户的数据库也是分开的. Api使用用户名(email)来查询数据库中的数据.

AllowOfflineAccess. 咱们还须要获取Refresh Token, 这就要求咱们的网站必须能够"离线"工做, 这里离线是指用户和网站之间断开了, 并非指网站离线了.

这就是说网站可使用token来和api进行交互, 而不须要用户登录到网站上. 

修改MvcClient的Startup的ConfigureServices:

public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
            services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            })
            .AddCookie("Cookies")
            .AddOpenIdConnect("oidc", options =>
            {
                options.SignInScheme = "Cookies";
                options.Authority = "http://localhost:5000";
                options.RequireHttpsMetadata = false;
                options.ClientId = "mvc_code";
                options.ClientSecret = "secret";
                options.ResponseType = "id_token code";
                options.Scope.Add("socialnetwork");
                options.Scope.Add("offline_access");
                options.SaveTokens = true;
                options.GetClaimsFromUserInfoEndpoint = true;
            });
        }

首先改ClientId和Authorization server一致. 这样用户访问的时候和implicit差很少, 只不太重定向回来的时候, 获取了一个code, 使用这个code能够换取secret而后获取access token.

因此须要在网站(MvcClient)上指定Client Secret. 这个不要泄露出去.

还须要改变reponse type, 不须要再获取access token了, 而是code, 这意味着使用的是Authorization Code flow.

还须要指定请求访问的scopes: 包括 socialnetwork api和离线访问

最后还能够告诉它从UserInfo节点获取用户的Claims.

运行

点击About, 重定向到Authorization Server:

同时在Authorization Server的控制台能够看见以下信息:

这里能够看到请求访问的scope, response_type. 还告诉咱们respose mode是from_post, 这就是说, 在这登录后重定向回到网站是使用的form post方式.

而后登录:

这里能够看到请求访问的范围, 包括我的信息和Application Access.

点击Yes, Allow:

重定向回到了网站. 这里看起来好像和之前同样. 可是若是看一下Authorization Server的控制台:

就会看到一个request. 中间件发起了一个请求使用Authorization Code和ClientId和secret来换取了Access token.

当Authorization验证上述信息后, 它就会建立一个token.

打印Refresh Token

修改MvcClient的About.cshtml:

@using Microsoft.AspNetCore.Authentication
<div>
    <strong>id_token</strong>
    <span>@await ViewContext.HttpContext.GetTokenAsync("id_token")</span>
</div>
<div>
    <strong>access_token</strong>
    <span>@await ViewContext.HttpContext.GetTokenAsync("access_token")</span>
</div>
<div>
    <strong>refresh_token</strong>
    <span>@await ViewContext.HttpContext.GetTokenAsync("refresh_token")</span>
</div>
<dl>
    @foreach (var claim in User.Claims)
    {
        <dt>@claim.Type</dt>
        <dd>@claim.Value</dd>
    }
</dl>

刷新页面:

看到了refresh token.

这些token包含了何时过时的信息.

若是access token过时了, 就没法访问api了. 因此须要确保access token不过时. 这就须要使用refresh token了.

复制一下refresh token, 而后使用postman:

使用这个refresh token能够获取到新的access token和refresh_token, 当这个access_token过时的时候, 可使用refresh_token再获取一个access_token和refresh_token......

而若是使用同一个refresh token两次, 就会获得下面的结果:

看看Authorization Server的控制台, 显示是一个invalid refresh token:

因此说, refresh token是一次性的.

获取自定义Claims

web api 要求request请求提供access token, 以证实请求的用户是已经受权的. 如今咱们准备从Access token里面提取一些自定义的Claims, 例如Email.

看看Authorization Server的Client配置:

Client的AllowedScopes已经包括了Email. 可是尚未配置Authorization Server容许这个Scope. 因此须要修改GetIdentityResources()(我本身的代码可能更名成IdentityResources()了):

public static IEnumerable<IdentityResource> IdentityResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
                new IdentityResources.Email()
            };
        }

而后须要为TestUser添加一个自定义的Claims;

public static IEnumerable<TestUser> Users()
        {
            return new[]
            {
                new TestUser
                {
                    SubjectId = "1",
                    Username = "mail@qq.com",
                    Password = "password",
                    Claims = new [] { new Claim("email", "mail@qq.com") }
                }
            };
        }

而后须要对MvcClient进行设置, Startup的ConfigureServices:

public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
            services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            })
            .AddCookie("Cookies")
            .AddOpenIdConnect("oidc", options =>
            {
                options.SignInScheme = "Cookies";
                options.Authority = "http://localhost:5000";
                options.RequireHttpsMetadata = false;
                options.ClientId = "mvc_code";
                options.ClientSecret = "secret";
                options.ResponseType = "id_token code";
                options.Scope.Add("socialnetwork");
                options.Scope.Add("offline_access");
                options.Scope.Add("email");
                options.SaveTokens = true;
                options.GetClaimsFromUserInfoEndpoint = true;
            });
        }

添加email scope. 因此MvcClient就会也请求这个scope.

运行:

这时在赞成(consent)页面就会出现email address一栏.

赞成以后, 能够看到email已经获取到了.

使用Access Token调用Web Api

首先在web api项目创建一个IdentityController:

namespace WebApi.Controllers
{
    [Route("api/[controller]")]
    public class IdentityController: Controller
    {
        [Authorize]
        [HttpGet]
        public IActionResult Get()
        {
            var username = User.Claims.First(x => x.Type == "email").Value;
            return Ok(username);
            //return new JsonResult(from c in User.Claims select new { c.Type, c.Value});
        }

    }
}

咱们想要经过自定义的claim: email的值.

而后回到mvcClient的HomeController, 添加一个方法:

        [Authorize]
        public async Task<IActionResult> GetIdentity()
        {
            var token = await HttpContext.GetTokenAsync("access_token");
            using (var client = new HttpClient())
            {
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
                var content = await client.GetStringAsync("http://localhost:5001/api/identity");
                // var json = JArray.Parse(content).ToString();
                return Ok(new { value = content });
            }
        }

这里首先经过HttpContext得到access token, 而后在请求的Authorization Header加上Bearer Token.

让咱们运行一下, 并在MvcClient和Web Api里面都设好断点,

登陆后在浏览器输入 http://localhost:5002/Home/GetIdentity 以执行GetIdenttiy方法, 而后进入Web Api看看断点调试状况:

因为咱们已经受权了, 因此能够看到User的一些claims, 而其中没有email这个claim. 再运行就报错了.

这是怎么回事? 咱们回到About页面, 复制一下access_token, 去jwt.io分析一下:

确实没有email的值, 因此提取不出来.

因此咱们须要把email添加到access token的数据里面, 这就须要告诉Authorization Server的Api Resource里面要包括User的Scope, 由于这是Identity Scope, 咱们想要把它添加到access token里:

修改Authorization Server的InMemoryConfiguration的ApiResources():

public static IEnumerable<ApiResource> ApiResources()
        {
            return new[]
            {
                new ApiResource("socialnetwork", "社交网络")
                { UserClaims = new [] { "email" } }
            };
        }

这对这个Api Resouce设置它的属性UserClaims, 里面写上email.

而后再运行一下程序, 这里须要从新登录, 首先分析一下token:

有email了. 

而后执行GetIdentity(), 在web api断点调试, 能够看到UserClaims已经包含了email:

上面这些若是您不会的话, 须要整理总结一下.

用户使用Authorization Server去登陆网站(MvcClient), 也就是说用户从网站跳转到第三方的系统完成了身份的验证, 而后被受权能够访问web api了(这里讲的是用户经过mvcClient访问api). 当访问web api的时候, 首先和authorization server沟通确认access token的正确性, 而后就能够成功的访问api了.

刷新Access Token

根据配置不一样, token的有效期可能差异很大, 若是token过时了, 那么发送请求以后就会返回401 UnAuthorized.

固然若是token过时了, 你可让用户重定向到Authorization Server从新登录,再回来操做, 不过这样太不友好, 太繁琐了.

既然咱们有refresh token了, 那不如向authorization server请求一个新的access token和refresh token. 而后再把这些更新到cookie里面. 因此下次再调用api的时候使用的是新的token.

在MvcClient的HomeController添加RefreshTokens()方法:

首先须要安装IdentityModel, 它是OpenIdConnect, OAuth2.0的客户端库:

        [Authorize]
        public async Task RefreshTokensAsync()
        {
            var authorizationServerInfo = await DiscoveryClient.GetAsync("http://localhost:5000/");
            var client = new TokenClient(authorizationServerInfo.TokenEndpoint, "mvc_code", "secret");
            var refreshToken = await HttpContext.GetTokenAsync("refresh_token");
            var response = await client.RequestRefreshTokenAsync(refreshToken);
            var identityToken = await HttpContext.GetTokenAsync("identity_token");
            var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(response.ExpiresIn);
            var tokens = new[]
            {
                new AuthenticationToken
                {
                    Name = OpenIdConnectParameterNames.IdToken,
                    Value = identityToken
                },
                new AuthenticationToken
                {
                    Name = OpenIdConnectParameterNames.AccessToken,
                    Value = response.AccessToken
                },
                new AuthenticationToken
                {
                    Name = OpenIdConnectParameterNames.RefreshToken,
                    Value = response.RefreshToken
                },
                new AuthenticationToken
                {
                    Name = "expires_at",
                    Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
                }
            };
            var authenticationInfo = await HttpContext.AuthenticateAsync("Cookies");
            authenticationInfo.Properties.StoreTokens(tokens);
            await HttpContext.SignInAsync("Cookies", authenticationInfo.Principal, authenticationInfo.Properties);
        }

首先使用一个叫作discovery client的东西来获取Authorization Server的信息. Authorization Server里面有一个discovery节点(endpoint), 能够经过这个地址查看: /.well-known/openid-configuration. 从这里能够得到不少信息, 例如: authorization节点, token节点, 发布者, key, scopes等等.

而后使用TokenClient, 参数有token节点, clientId和secret. 而后可使用这个client和refreshtoken来请求新的access token等. 

找到refresh token后, 使用client获取新的tokens, 返回结果是tokenresponse. 你能够设断点查看一下token reponse里面都有什么东西, 这里就不弄了, 里面包括identitytoken, accesstoken, refreshtoken等等.

而后须要找到原来的identity token, 由于它至关因而cookie中存储的主键...

而后设置一下过时时间.

而后将老的identity token和新获取到的其它tokens以及过时时间, 组成一个集合.

而后使用这些tokens来从新登录用户. 不过首先要获取当前用户的authentication信息, 使用HttpContext.AuthenticateAsync("Cookies"), 参数是AuthenticationScheme. 而后修改属性, 存储新的tokens.

最后就是重登陆, 把当前用户信息的Principal和Properties传进去. 这就会更新客户端的Cookies, 用户也就保持登录而且刷新了tokens.

先简单调用一下这个方法:

[Authorize]
        public async Task<IActionResult> GetIdentity()
        {
            await RefreshTokensAsync();
            var token = await HttpContext.GetTokenAsync("access_token");
            using (var client = new HttpClient())
            {
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
                var content = await client.GetStringAsync("http://localhost:5001/api/identity");
                //var json = JArray.Parse(content).ToString();
                return Ok(new { value = content });
            }
        }

正式生产环境中可不要这么作, 正式环境中应该在401以后, 调用这个方法, 若是再失败, 再返回错误.

运行一下:

发现获取的access token是空的, 必定是哪出现了问题, 看一下 authorization server的控制台:

说refresh token不正确(应该是内存数据和cookie数据不匹配). 那就从新登录.

看断点, 有token了:

而且和About页面显示的不同, 说明刷新token了.

也能够看一下authorization server的控制台:

说明成功请求了token.

今天先到这里.

相关文章
相关标签/搜索