天天看點

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

預備知識:  http://www.cnblogs.com/cgzl/p/7746496.html 第一部分:  http://www.cnblogs.com/cgzl/p/7780559.html 第二部分:  http://www.cnblogs.com/cgzl/p/7788636.html 第三部分:  http://www.cnblogs.com/cgzl/p/7793241.html

上一篇講了使用OpenId Connect進行Authentication.

下面講

Hybrid Flow和Offline Access

目前我們解決方案裡面有三個項目 Authorization Server, Web api和Mvc Client. 在現實世界中, 他們可能都在不同的地方.

現在讓我們從MvcClient使用從Authorization Server擷取的token來通路web api. 并且確定這個token不過期.

現在我們的mvcClient使用的是implicit flow, 也就是說, token 被發送到client. 這種情況下 token的生命可能很短, 但是我們可以重定向到authorization server 重新擷取新的token.

例如, 在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.

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:

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

同時在Authorization Server的控制台可以看見如下資訊:

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

這裡可以看到請求通路的scope, response_type. 還告訴我們respose mode是from_post, 這就是說, 在這登陸後重定向回到網站是使用的form post方式.

然後登陸:

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

這裡可以看到請求通路的範圍, 包括個人資訊和Application Access.

點選Yes, Allow:

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

重定向回到了網站. 這裡看起來好像和以前一樣. 但是如果看一下Authorization Server的控制台:

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

就會看到一個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>      

重新整理頁面:

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

看到了refresh token.

這些token包含了什麼時候過期的資訊.

如果access token過期了, 就無法通路api了. 是以需要確定access token不過期. 這就需要使用refresh token了.

複制一下refresh token, 然後使用postman:

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

使用這個refresh token可以擷取到新的access token和refresh_token, 當這個access_token過期的時候, 可以使用refresh_token再擷取一個access_token和refresh_token......

而如果使用同一個refresh token兩次, 就會得到下面的結果:

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

看看Authorization Server的控制台, 顯示是一個invalid refresh token:

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

是以說, refresh token是一次性的.

擷取自定義Claims

web api 要求request請求提供access token, 以證明請求的使用者是已經授權的. 現在我們準備從Access token裡面提取一些自定義的Claims, 例如Email.

看看Authorization Server的Client配置:

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

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 = "[email protected]",
                    Password = "password",
                    Claims = new [] { new Claim("email", "[email protected]") }
                }
            };
        }      

然後需要對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.

運作:

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

這時在同意(consent)頁面就會出現email address一欄.

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

同意之後, 可以看到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看看斷點調試情況:

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

由于我們已經授權了, 是以可以看到User的一些claims, 而其中沒有email這個claim. 再運作就報錯了.

這是怎麼回事? 我們回到About頁面, 複制一下access_token, 去jwt.io分析一下:

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

确實沒有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:

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

有email了. 

然後執行GetIdentity(), 在web api斷點調試, 可以看到UserClaims已經包含了email:

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

上面這些如果您不會的話, 需要整理總結一下.

使用者使用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的用戶端庫:

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token
[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之後, 調用這個方法, 如果再失敗, 再傳回錯誤.

運作一下:

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

發現擷取的access token是空的, 一定是哪出現了問題, 看一下 authorization server的控制台:

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

說refresh token不正确(應該是記憶體資料和cookie資料不比對). 那就重新登陸.

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

看斷點, 有token了:

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

并且和About頁面顯示的不一樣, 說明重新整理token了.

也可以看一下authorization server的控制台:

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token

說明成功請求了token.

今天先到這裡.

下面是我的關于ASP.NET Core Web API相關技術的公衆号--草根專欄:

使用Identity Server 4建立Authorization Server (4)Hybrid Flow和Offline Access首先在Authorization server的InMemoryConfiguration添加一個Client:修改MvcClient的Startup的ConfigureServices:運作列印Refresh Token擷取自定義Claims使用Access Token調用Web Api重新整理Access Token