06 Authorization Code Flow 实例


原文:https://www.yuque.com/yuejiangliu/dotnet/ac0xok


06 Authorization Code Flow 实例.mp4 (137.6 MB)

Adding User Authentication with OpenID Connect 中的解释

最后在 HomeController 上标注 [Authorize] 以保护 HomeController。

2、添加 MVC Client

打开 Idp 项目,添加 MVC Client:

// MVC client, authorization code
new Client
{
    ClientId = "mvc client",
    ClientName = "ASP.NET Core MVC Client",

    AllowedGrantTypes = GrantTypes.CodeAndClientCredentials,
    ClientSecrets = { new Secret("mvc secret".Sha256()) },

    RedirectUris = { "http://localhost:5002/signin-oidc" },

    FrontChannelLogoutUri = "http://localhost:5002/signout-oidc",
    PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
    
    // 总是在 Id Token 里面包含所有 User Claims 信息
    AlwaysIncludeUserClaimsInIdToken = true,

    // 设为 True 即支持 Refresh Token
    AllowOfflineAccess = true, // offline_access
    
    AllowedScopes =
    {
        "api1",
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Email,
        IdentityServerConstants.StandardScopes.Address,
        IdentityServerConstants.StandardScopes.Phone,
        IdentityServerConstants.StandardScopes.Profile
    }
},

3、运行项目

依次启动 Idp、Api1 和 MVC Client 三个项目。

由于 MVC Client 的 HomeController 被保护了,所以启动项目后会自动跳转到授权服务器进行身份认证。

image.png

身份认证完毕后跳转到 grant 界面:

image.png

同意后回到 MVC Client 的 Home 界面:

image.png

4、在 MVC Client 中获取各个 Token

修改 HomeController 的 Privacy 方法,获取各个 Token:

public async Task<IActionResult> Privacy()
{
    var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
    var idToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken);

    var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
  
    return View();
}

注:修改 MVC Client 后每次重新调试记得手动清除浏览器 Cookie。

通过 Preserve log 查看 Authorization Token:

image.png

5、通过 Fiddler 监视到两个往返

image.png

点下 Yes, Allow 的瞬间,可以通过 Fiddler 监视到两个往返。

image.png

1 - 身份认证请求(浏览器与授权服务器间通讯):

HTTP/1.1 302 Found
Location: https://server.example.com/authorize?
    response _type=code
    &scope=openid%20profile&20email
    &client_id=s6BhdRkqt3
    &state=af0ifjsldkj
    &redirect_uri=https$3A%2F%2Fclient.example.org%2Fcb

1 - 身份认证请求的响应:

HTTP/1.1 302 Found
Location: https://client.example.org/cb?
    code=Splx10BeZQQYbYS6WxSbIA
    &state=af0ifjsldkj

2 - Token 请求(客户端服务器与授权服务器间通讯):

POST /token HTTP/1.1
Host: server.example.com
Content-Iype: application/x-www-form-urlencoded
Authorization: Basic czzCaGRSa3FOMzpniDFmQmFOM2JW

grant_type=authorization_code
    &code=Splx10BeZQQYbYS6WxSbIA
    &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb

2 - Token 请求的响应:

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
  "access_token": "S1AV32hkKG",
  "token_type": "Bearer",
  "refresh_token": "8xLOxBtZp8",
  "expires_in": 3600,
  "id_token": "eyJhbGci0iJSUzI1NiIsImtpZCI6IjF10WdkazcifQ.ewogImlzc
    yI6ICJodHRwOi8vc2VyimVyLmV4YW1wbGUuY29tIiwKICJzdWIi0iAiM)Q4Mjg5
    NzYXMDAXIiwKICJh/Qi0iAiczZCaGRSa3FOMyIsCiAibm9uY2Ui0iAibi0wUz2
    fV3pBMK1qIiwKICJLeHAi01AXMzExM)gxOTcwLAogImlhdCI6IDEzMTEyODA5Nz
    AKEQ.ggwehZ1EuVLuxNuuIJKX V8a OMXzROEHR9R6jgdqrOOF4daGU96Sr P6g
    Jp6IcmD3HP990bi1PR3-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CU
    NgeGpe-gccMg4vfKjkMBFcGvnzZUN4 _KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpG
    QyHE51cMiKPXEEIQILVqOpC E2DzL7emopioaoZTF mO NOYzFC6g6EJbOEoRoS
    K5hoDalrcvRYLSrQAZZKf1yuVCyixEoV9GfNQC3_o3jzM2PAithfubEEBLuVWk4
    XUVrWOLrL10nx7RkKUSNXNHg-rvMzgg"
}

6、展示 Token

修改 HomeController 的 Privacy 方法,展示 Token:

public async Task<IActionResult> Privacy()
{
    var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
    var idToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken);

    var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);

    ViewData["accessToken"] = accessToken;
    ViewData["idToken"] = idToken;
    ViewData["refreshToken"] = refreshToken;

    return View();
}

Privacy 的 View:

@{
    ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]h1>

<h2>Access Token:h2>
<p>@ViewData["accessToken"]p>

<h2>Id Token:h2>
<p>@ViewData["idToken"]p>

<h2>Refresh Token:h2>
<p>@ViewData["refreshToken"]p>

<dl>
    @foreach (var claim in User.Claims)
    {
        <dt>@claim.Typedt>
        <dd>@claim.Valuedd>
    }
dl>

image.png

7、使用 Access Token 访问 Api1 的资源

先修改 MVC Client HomeController 的 Index Action:

public async Task<IActionResult> Index()
{
    var client = new HttpClient();
    var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000/");
    if (disco.IsError) throw new Exception(disco.Error);

    var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);

    client.SetBearerToken(accessToken);

    var response = await client.GetAsync("http://localhost:5001/identity");
    if (!response.IsSuccessStatusCode) throw new Exception(response.ReasonPhrase);

    var content = await response.Content.ReadAsStringAsync();
    return View("Index", content);
}

再修改 Index View:

@model string

@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Api1 Resource Respose:h1>
    <p>@Modelp>
div>

效果:

image.png

8、单点登录

用户登录后,再打开授权服务器可以看到 alice 处于登录状态(即会话被 Idp 项目保持了)。

此时如果有其他 Web 应用请求授权,跳转到授权服务器后会发现 alice 已经登录了,它就无需登录直接跳转回去。整个过程就相当于是一个单点登录。

image.png

9、登出

登出时既要登出 MVC 客户端,又要登出 IdentityServer4 用户会话。

MVC Client _Layout 中添加 Logout 链接:

<ul class="navbar-nav flex-grow-1">
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Homea>
    li>
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacya>
    li>
    @if (User.Identity.IsAuthenticated)
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Logout">Logouta>
        li>
    }
ul>

Logout Action:

public async Task Logout()
{
    // 登出当前网站的 Cookie
    await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    // 登出 IdentityServer4 的 Cookie
    await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
}

通过设置 Idp 项目 Quickstart\Account\AccountOptions.cs 的 AutomaticRedirectAfterSignOut = true; 可以在登出后自动跳转回客户端。

相关