[MDP.NetCore] 使用AzureAD+受控识别,快速建立两个服务之间的Service身分认证

MDP.AspNetCore.Authentication.AzureAD.Services for Managed Identity

MDP.AspNetCore.Authentication.AzureAD.Services扩充ASP.NET Core既有的身分验证,加入AzureAD提供的Service身分验证功能。开发人员可以透过Config设定,挂载在专案里使用的Service身分验证,用以验证Azure里的受控识别(Managed Identity)。

运作流程

MDP.AspNetCore.Authentication.AzureAD.Services使用AzureAD提供的OAuth服务,透过Client Credentials流程来进行Service身分验证,用以验证Azure里的受控识别(Managed Identity)。下列两个运作流程,说明AzureAD的凭证发放流程、服务验证流程。(内容为简化说明,完整细节可参考AzureAD文件)

凭证发放

Service身分验证-凭证发放.png

0.建立Azure资源,用来执行API Client应用程式时,Azure资源内会同时挂载IMDS服务(Azure Instance Metadata Service)。

1.开发人员至AzureAD,在执行API Client应用程式的Azure资源内,开启系统指派的受控识别(Managed Identity)。

2.AzureAD建立API Client的身分凭证,发送给IMDS服务储存。

3.开发人员至AzureAD,建立API Provider的应用程式注册。

4.开发人员从AzureAD,取得API Provider的身分凭证,内容包含:TenantId、ClientId。(没有ClientSecret)

5.开发人员将API Provider的身分凭证,设定在API Provider应用程式的Config参数。

服务验证

Service身分验证-服务验证.png

1.使用者开启API Client提供的URL。

2.API Client发送GetToken指令,给同Azure资源内的IMDS服务。

3.IMDS服务从储存空间,取得API Client的身分凭证发送给AzureAD,内容包含:TenantId、ClientId、ClientSecret。

4.AzureAD依照API Client的身分凭证,使用内建的公私钥加密机制,回传代表API Client的AccessToken。

5.IMDS服务,回传代表API Client的AccessToken。

6.API Client调用API,并且将AccessToken放置在API Request封包的Header。

7.API Provider从Config参数,取得API Provider的身分凭证发送给AzureAD,内容包含:TenantId、ClientId。(没有ClientSecret)

8.AzureAD依照API Provider的身分凭证,使用内建的公私钥管理机制,回传可以验证AccessToken的公钥(Public Key)。

9.API Provider使用Public Key验证AccessToken签章,确认合法就依照系统逻辑回传API Response。(不合法回传401 Unauthorized)

10.API Client依照API Response,回传Page给使用者。

模组使用-API服务端(API Provider)

申请服务

MDP.AspNetCore.Authentication.AzureAD.Services使用AzureAD提供的OAuth服务,透过Client Credentials流程来进行Service身分验证。依照下列操作步骤,即可申请AzureAD提供给API服务端(API Provider)的身分凭证。

1.注册并登入Microsoft Azure Portal。于首页左上角的选单里,点击应用程式注册后,进入应用程式注册页面。于应用程式注册页面,点击新增注册按钮,依照页面提示建立一个Application。

01.建立Application01.png

01.建立Application02.png

01.建立Application03.png

2.建立完毕后,于Application页面,点击新增应用程式识别码 URI按钮,进入公开API页面,然后点击新增,依照页面提示建立一个「应用程式识别码 URI」。建立完毕后,于Application页面取得:API服务端的「目录 (租用户) 识别码」、「应用程式 (用户端) 识别码」、「应用程式识别码 URI」。

03.取得参数01.png

03.取得参数02.png

03.取得参数03.png

03.取得参数04.png

3.于Application页面,点击左侧选单的应用程式角色,进入应用程式角色页面。然后点击建立应用程式角色,依照页面提示建立一个「应用程式角色」。建立完毕后,于应用程式角色页面取得:API服务端的「应用程式角色识别码」。

04.建立角色01.png

04.建立角色02.png

04.建立角色03.png

4.于Application页面,点击中页面内容的本机目录中受控的应用程式,进入企业应用程式页面。并于企业应用程式页面,取得:API服务端的「物件识别码」。

05.取得识别码01.png

05.取得识别码02.png

加入专案

申请服务完成之后,就可以开始建立专案并且加入模组。MDP.AspNetCore.Authentication.AzureAD.Services预设独立在MDP.Net专案范本外,依照下列操作步骤,即可建立加入MDP.AspNetCore.Authentication.AzureAD.Services的专案。

  • 在命令提示字元输入下列指令,使用MDP.Net专案范本建立专案。
dotnet new install MDP.WebApp
dotnet new MDP.WebApp -n ApiProvider
  • 使用Visual Studio开启专案。在专案里使用NuGet套件管理员,新增下列NuGet套件。
MDP.AspNetCore.Authentication.AzureAD.Services

设定参数

建立包含MDP.AspNetCore.Authentication.AzureAD.Services的专案之后,就可以透过Config设定,挂载在专案里使用的Service身分验证。

// Config设定
{
  "Authentication": {
    "AzureAD.Services": {
      "TenantId": "xxxxx",
      "ClientId": "xxxxx"
    }
  }
}

- 命名空间:Authentication
- 挂载的身分验证模组:AzureAD.Services
- API服务端的租户编号:TenantId="xxxxx"。(xxxxx填入API服务端的「目录 (租用户) 识别码」)
- API服务端的客户编号:ClientId="xxxxx"。(xxxxx填入API服务端的「应用程式 (用户端) 识别码」)

模组使用-API客户端(API Client)

申请服务(API Client)

MDP.AspNetCore.Authentication.AzureAD.Services使用AzureAD提供的OAuth服务,透过Client Credentials流程来进行Service身分验证。依照下列操作步骤,即可申请API客户端(API Client)的受控识别,用以提供身分凭证。

1.建立Azure资源(例如:Azure VM、App Service、Container Apps),用来执行API客户端程式。

11.建立受控识别01.png

2.执行API客户端程式的Azure资源,建立完毕之后。进入该Azure资源页面,点击左侧选单的身分识别,进入身分识别页面。然后点击开启,依照页面提示开启系统指派的受控识别,并取得API客户端的「物件 (主体) 识别码」。

11.建立受控识别02.png

11.建立受控识别03.png

3.回到Azure Portal。于右上角的选单里,点击Cloud Shell按钮后,开启Cloud Shell视窗。于Cloud Shell视窗,切换至Bash并执行下列指令,新增API服务端的「应用程式角色」给API客户端。

az rest \
-m POST \
--headers "Content-Type=application/json" \
-u "https://graph.microsoft.com/v1.0/servicePrincipals/xxxClient-PrincipalIdxxx/appRoleAssignments" \
-b "{
    \"principalId\": \"xxxClient-PrincipalIdxxx\", 
    \"resourceId\": \"xxxProvider-ResourceIdxxx\", 
    \"appRoleId\": \"xxxProvider-AppRoleIdxxx\"
}"

- API客户端的物件识别码:xxxClient-PrincipalIdxxx。(xxxClient-PrincipalIdxxx填入先前取得的API客户端「物件 (主体) 识别码」。注意!有两个地方要改。)
- API服务端的物件识别码:xxxProvider-ResourceIdxxx。(xxxProvider-ResourceIdxxx填入先前取得的API服务端「物件识别码」)
- API服务端的角色识别码:xxxProvider-AppRoleIdxxx。(xxxProvider-AppRoleIdxxx填入先前取得的API服务端「应用程式角色识别码」)

12.建立授权01.png

12.建立授权02.png

4.回到Azure Portal。于首页左上角的选单里,点击企业应用程式后,进入企业应用程式页面。于企业应用程式页面,可以找到API客户端(API Client)的受控识别。点击后,进入API客户端(API Client)的受控识别页面,选择左侧选单里的权限页签,可以看到授权给API客户端的权限。(这页也可以撤销权限)

13.检视授权01.png

13.检视授权02.png

13.检视授权03.png

加入专案

申请服务完成之后,就可以开始建立专案并且加入模组。Azure.Identity预设独立在MDP.Net专案范本外,依照下列操作步骤,即可建立加入Azure.Identity的专案。

  • 在命令提示字元输入下列指令,使用MDP.Net专案范本建立专案。
dotnet new install MDP.WebApp
dotnet new MDP.WebApp -n ApiClient
  • 使用Visual Studio开启专案。在专案里使用NuGet套件管理员,新增下列NuGet套件。
Azure.Identity

使用凭证

建立包含Azure.Identity的专案之后,就可以在程式码里使用受控识别,建立代表API客户端身分的AccessToken,用来通过API服务端的Service身分验证。(必需在Azure资源执行)

// 参数设定
var azureCredential = new DefaultAzureCredential();
var apiProviderURI= "api://xxxxx";
var apiProviderEndpoint= "https://xxxxx/Home/Index";

- API服务端的应用程式识别码URI: apiProviderURI= "api://xxxxx"。(xxxxx填入API服务端的「应用程式识别码URI」)
- API服务端的API服务端点: apiProviderEndpoint= "https://xxxxx/Home/Index"。(https://xxxxx/Home/Index填入API服务端的API服务端点)
// 建立AccessToken
var accessToken = (await azureCredential.GetTokenAsync(new Azure.Core.TokenRequestContext(new string[] { $"{apiProviderURI}/.default" }), default)).Token;
// 呼叫API服务端点
var responseContent = string.Empty;
using (var httpClient = new HttpClient())
{
    // Headers
    httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);

    // Send
    var response = await httpClient.GetAsync(apiProviderEndpoint);
    responseContent = await response?.Content?.ReadAsStringAsync();
}

模组范例

使用AzureAD提供的Service身分验证功能,进行 Service to Service 之间的身分验证,是开发系统时常见的功能需求。本篇范例协助开发人员使用MDP.AspNetCore.Authentication.AzureAD.Services,逐步完成必要的设计和实作。

  • 范例下载:ApiClient.zip

  • 范例下载:ApiProvider.zip

  • 特别说明1:本篇范例的 API客户端必需在Azure环境布署执行、API服务端不限制在Azure环境部署执行。

  • 特别说明2:本篇范例的 API客户端、API服务端,两者皆无需持有Secret。

建立API服务端(API Provider)

1.依照开发一个从GitHub持续布署到Azure Container Apps的Web站台的步骤流程,建立:api-provider容器应用、ApiProvider程式专案,并且取得API服务端的:「应用程式 URL」。

21.申请服务01.png

21.申请服务02.png

2.依照模组使用-API服务端(API Provider)-申请服务的步骤流程,申请AzureAD提供的OAuth服务,并取得API服务端的:「目录 (租用户) 识别码」、「应用程式 (用户端) 识别码」、「应用程式识别码 URI」。

21.申请服务03.png

3.于专案内改写appsettings.json,填入「目录 (租用户) 识别码」、「应用程式 (用户端) 识别码」,用以挂载Service身分验证。

{
  "Authentication": {
    "AzureAD.Services": {
      "TenantId": "xxxxx", // API服务端的「目录 (租用户) 识别码」
      "ClientId": "xxxxx"  // API服务端的「应用程式 (用户端) 识别码」
    }
  }
}

4.改写专案内的Controllers\HomeController.cs,提供一个必须通过身分验证才能使用的API服务端点:\Home\Index。

using MDP.AspNetCore.Authentication.AzureAD.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Linq;
using System.Security.Claims;

namespace ApiProvider
{
    public class HomeController : Controller
    {
        // Methods
        [Authorize]
        public string Index()
        {
            // ClaimsIdentity
            var claimsIdentity = this.User.Identity as ClaimsIdentity;
            if (claimsIdentity == null) throw new InvalidOperationException($"{nameof(claimsIdentity)}=null");
            Console.WriteLine($"this.User.AuthenticationType = {claimsIdentity.AuthenticationType}");
            Console.WriteLine($"this.User.TenantId = {claimsIdentity.FindFirst(AzureServicesAuthenticationClaimTypes.TenantId)?.Value}");
            Console.WriteLine($"this.User.ClientId = {claimsIdentity.FindFirst(AzureServicesAuthenticationClaimTypes.ClientId)?.Value}");
            Console.WriteLine($"this.User.Roles = {String.Join(",", claimsIdentity.FindAll(System.Security.Claims.ClaimTypes.Role).Select(o => o.Value))}");
            Console.WriteLine();
            
            // Return
            return "Hello World";
        }
    }
}

5.完成专案程式码改写的步骤之后,将程式码签入GitHub用以启动GitHub Action流程,编译并部署API服务端程式到Azure Container Apps。

21.申请服务04.png

建立API客户端(API Client)

1.依照开发一个从GitHub持续布署到Azure Container Apps的Web站台的步骤流程,建立:api-client容器应用、ApiClient程式专案,并且取得API客户端的:「应用程式 URL」。

22.申请服务01.png

22.申请服务02.png

2.依照模组使用-API客户端(API Client)-申请服务的步骤流程,开启API客户端的受控识别。

21.申请服务03.png

3.改写专案内的Controllers\HomeController.cs、Views\Home\Index.cshtml,提供Home页面。并于Home页面使用受控识别凭证,建立代表API客户端身分的AccessToken,用来通过API服务端的Service身分验证后,取得资料显示于页面。

using Azure.Identity;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace ApiClient
{
    public class HomeController : Controller
    {
        // Methods
        public async Task<ActionResult> Index()
        {
            // Variables
            var azureCredential = new DefaultAzureCredential();
            var apiProviderURI = "api://xxxxx";                   // API服务端的「应用程式识别码 URI」
            var apiProviderEndpoint = "https://xxxxx/Home/Index"; // API服务端的「应用程式 URL」+/Home/Index

            // AccessToken
            var accessToken = (await azureCredential.GetTokenAsync(new Azure.Core.TokenRequestContext(new string[] { $"{apiProviderURI}/.default" }), default)).Token;
            if (string.IsNullOrEmpty(accessToken) == true) throw new InvalidOperationException($"{nameof(accessToken)}=null");

            // Call API
            var responseContent = string.Empty;
            using (var httpClient = new HttpClient())
            {
                // Headers
                httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);

                // Send
                var response = await httpClient.GetAsync(apiProviderEndpoint);
                responseContent = await response?.Content?.ReadAsStringAsync();
            }
            if (string.IsNullOrEmpty(responseContent) == true) throw new InvalidOperationException($"{nameof(responseContent)}=null");

            // ViewBag
            this.ViewBag.Message = responseContent;

            // Return
            return View();
        }
    }
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>ApiClient</title>
</head>
<body>

    <!--Title-->
    <h2>ApiClient</h2>
    <hr />

    <!--Message-->
    <h3>@ViewBag.Message</h3>

</body>
</html>

5.完成专案程式码改写的步骤之后,将程式码签入GitHub用以启动GitHub Action流程,编译并部署API客户端程式到Azure Container Apps。

22.申请服务04.png

范例执行

1.使用Browser视窗,开启API客户端的「应用程式 URL」。于开启的Browser视窗内,可以看到系统画面进入到Home页面,并且显示API服务端回传的”Hello World”。

23.执行结果01.png

2.登入Microsoft Azure Portal。于API服务端的容器应用页面,进入纪录资料流页签,可以看到通过Service身分验证的API客户端身分资料(Controller.User属性),并且包含「应用程式角色」的资料。

23.执行结果02.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章