上篇文章 辛苦安裝完 AD FS,接下來試試 AD FS 跨界整合的威力。

AD FS 支援多種登入協定,包含 WS-FED、SAML-P、OAuth 等,但 OAtuh 已成當今業界標準,聚焦 OAuth 就好。AD FS 認證身分取得授權的 OAuth 流程有以下幾種:

  1. Implicit Grant Flow
    適用純前端(如SPA),依循 OAuth 2.0 規格,不需要後端伺服器認證交換,讓前端維持登入身分並獲得授權存取 WebAPI
  2. Authorization Code Grant Flow
    適用大部分網站、App,分兩階段取得 authorization_code 及 access_token
  3. On-Behalf-Of Flow
    WebAPI 代表用者呼叫其他 WebAPI
  4. Client Credentials Grant Flow
    通常用於伺服器對伺服器間互動,背景程式執行期間沒有使用者參與,以應用程式的身分存取其他資源 (AD FS 也允許用憑證取代 Secret 提高安全性)
  5. Device Code Flow
    用於輸入受限裝置的登入(例如:Smart TV, IoT, 印表機),使用者在其他裝置開瀏覽器輸入裝置代碼及登入,讓裝置取得授權。

以下是一些經典的 AD FS 應用場景:

  • SPA (Single Page Application) 純 JavaScript 開發,使用 MSAL 登入取得權限以存取 MS Graph API、企業 API 等資源
  • 網頁應用程式登入
  • 原生 App 登入取得授權存取 WebAPI
  • 網頁應用程式取得授權存取 WebAPI
  • WebAPI 代表使用者呼叫其他 WebAPI (OBO, On Behalf Of)
  • 背景服務呼叫 Web API (使用 Client Credentials)
  • 網頁應用程式使用 Client Credentials 呼叫 WebAPI
  • 無瀏覽器 App 呼叫 Web API (使用 Device Code)

為了簡化開發,微軟提供通用的驗證程式庫 - MSAL - Microsoft Authentication Library,支援 .NET/Java/JavaScript/Python/Android/Go/iOS,除了方便串接 Microsft Identity Platform (v2.0) Endpoint,整合 Azure AD、AD FS,也能整合 MS Account、FB/Google/Linkedin 帳號進行身分認證。


圖片來源

要用 MSAL.NET 整合 AD FS 有兩種做法:透過 Azure AD (Azure AD 與 AD FS 同盟,取得地端帳號、群組資料),或直接與 AD FS 通訊(AD FS 2019+,要直接連 AD FS 2016 的話要用 ADAL.NET參考

研究了一下,要在雲端使用地端 AD,由 Azure AD 主導身分認證是未來的主流。以 MFA 為例,AD FS 也是靠 Azure AD 加入 MFA 多因子驗證 (或是依賴 MFA 協力廠商方案),前一段提到 MSAL.NET 可支援直接連 AD FS 2019,但查不到範例,幾乎都是 MSAL.NET 整合 Azure AD。

我決定這篇文章先用 Microsoft.AspNetCore.Authentication.OpenIdConnect 簡單實現直連 AD FS,之後再玩 Azure AD。

新建一個 MVC 專案,dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect 引用套件。動手改程式前,先連上 AD FS 新增 Application Gruop:

輸入名稱,範本選 Web browser accessing a web application:

系統會自動產生 Client Identifier,這個要記下來,稍後程式會用到。Redirect URI 輸入要整合 AD FS 做登入的網站網址,我先在 Visual Studio 測試,填入偵錯模式網址 URL。若已知日後要部署的 URL,也可一併輸入:

存取控制有多組預設原則,我們先選 Permit everyone,之後若要強制啟用 MFA,可在此設定:

完成這些設定後,網站呼叫 AD FS 完成登入後,由 User.Claims 只會取得使用者帳號,我們可以調整下圖中的 Web Application 設定,把群組資料也帶入:

頁籤 Issuance Transform Rules 可設定要從 AD 傳送哪些 Claims 資料出去,我們的目的是送出帳號姓名及所屬群組:

做法是按 Add Rule,選 Send LDAP Attributes as Claims:

填入 Claim rule name,Attribute store 選 Active Directory,加入 Display-Name 轉 Common Name、Token-Groups - Unqualified Names 轉 Role 兩條設定。

至此,AD FS 這邊就設定好了。

程式的話,先在 Program.cs 加入中文註解部分的程式碼啟用 OpenIdConnect 認證:

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

var builder = WebApplication.CreateBuilder(args);

// 加入 Cookie / OpenIdConnection 認證
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
    options.AccessDeniedPath = "/";
    options.LogoutPath = "/";
})
.AddOpenIdConnect(options =>
{
    // 由設定檔取得 Metadata URL https://adfs.<domain-name>/adfs/.well-known/openid-configuration
    options.MetadataAddress = builder.Configuration["Adfs:MetadataAddress"];
    // 由設定檔取得在 AD FS 建立 Appication Group 產生的 Client Id
    options.ClientId = builder.Configuration["Adfs:ClientId"];
    options.SignInScheme = "Cookies";
    options.ResponseType = OpenIdConnectResponseType.Code; 
    options.UsePkce = false;

    options.Scope.Clear();
    options.Scope.Add("openid"); // 只要取得 OpenId 身分資料就好

    options.SaveTokens = true;
});


// Add services to the container.
builder.Services.AddControllersWithViews();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

//  啟用身分認證
app.UseAuthorization();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

HomeController 的話,借用 Privacy Action,加上 [Authorize] 要求登入,

using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using adfs_sso.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

namespace adfs_sso.Controllers;

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    public HomeController(ILogger<HomeController> logger)
    {
        _logger = logger;
    }

    public IActionResult Index()
    {
        return View();
    }

    [Authorize] //要求登入
    public IActionResult Privacy()
    {
        return View();
    }

    // 實作登出
    public IActionResult Logout()
    {
        return SignOut(CookieAuthenticationDefaults.AuthenticationScheme, 
            OpenIdConnectDefaults.AuthenticationScheme);
    }

    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    public IActionResult Error()
    {
        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
    }
}

_Layout.cshtml 加上一個 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">Home</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
    </li>
    @if (Context.User.Identity.IsAuthenticated)
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Logout">Logout</a>
        </li>
    }
</ul>

至於 Privacy.cshtml,則換成以下內容:

@using System.Security.Claims
<div>
    <fieldset>
        <legend>User Info</legend>
        <div>User.Identity.Name = @User.Identity.Name</div>
        <div>
            Common Name = 
            @User.FindFirstValue("http://schemas.xmlsoap.org/claims/CommonName")</div>
        <div>Is DomainUsers = @User.IsInRole("Domain Users")</div>
        <div>Is DomainAdmins = @User.IsInRole("Domain Admins")</div>
    </fieldset>
    <fieldset>
        <legend>Claims</legend>
        @foreach (var claim in User.Claims)
        {
            <div>@claim.Type : @claim.Value</div>
        }
    </fieldset>
</div>

好戲上場。在以下展示中,開啟 Privacy 頁面會被導向 AD FS 登入畫面,輸入 AD 帳號密碼後,回到 Privacy 頁,由 User.Identity.Name、User.Claims 可取得傳回 Claims,而人員所屬群組 (Domain Users) 也被傳回,並可用 User.IsInRole("Domain Users") 成功簽測。

操作展示

下一篇,我們再來看如何整合 Azure AD。

Example of ASP.NET Core authentication with AD FS via OpenIdConnect.


Comments

Be the first to post a comment

Post a comment