MDP.AspNetCore.Authentication.AzureAD.Services for Managed Identity
MDP.AspNetCore.Authentication.AzureAD.Services扩充ASP.NET Core既有的身分验证,加入AzureAD提供的Service身分验证功能。开发人员可以透过Config设定,挂载在专案里使用的Service身分验证,用以验证Azure里的受控识别(Managed Identity)。
-
说明文件:https://clark159.github.io/MDP.AspNetCore.Authentication/
-
程式源码:https://github.com/Clark159/MDP.AspNetCore.Authentication/
-
特别说明1:本篇范例的 API客户端必需在Azure环境布署执行、API服务端不限制在Azure环境部署执行。
-
特别说明2:本篇范例的 API客户端、API服务端,两者皆无需持有Secret。
运作流程
MDP.AspNetCore.Authentication.AzureAD.Services使用AzureAD提供的OAuth服务,透过Client Credentials流程来进行Service身分验证,用以验证Azure里的受控识别(Managed Identity)。下列两个运作流程,说明AzureAD的凭证发放流程、服务验证流程。(内容为简化说明,完整细节可参考AzureAD文件)
凭证发放
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参数。
服务验证
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。
2.建立完毕后,于Application页面,点击新增应用程式识别码 URI按钮,进入公开API页面,然后点击新增,依照页面提示建立一个「应用程式识别码 URI」。建立完毕后,于Application页面取得:API服务端的「目录 (租用户) 识别码」、「应用程式 (用户端) 识别码」、「应用程式识别码 URI」。
3.于Application页面,点击左侧选单的应用程式角色,进入应用程式角色页面。然后点击建立应用程式角色,依照页面提示建立一个「应用程式角色」。建立完毕后,于应用程式角色页面取得:API服务端的「应用程式角色识别码」。
4.于Application页面,点击中页面内容的本机目录中受控的应用程式,进入企业应用程式页面。并于企业应用程式页面,取得:API服务端的「物件识别码」。
加入专案
申请服务完成之后,就可以开始建立专案并且加入模组。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客户端程式。
2.执行API客户端程式的Azure资源,建立完毕之后。进入该Azure资源页面,点击左侧选单的身分识别,进入身分识别页面。然后点击开启,依照页面提示开启系统指派的受控识别,并取得API客户端的「物件 (主体) 识别码」。
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服务端「应用程式角色识别码」)
4.回到Azure Portal。于首页左上角的选单里,点击企业应用程式后,进入企业应用程式页面。于企业应用程式页面,可以找到API客户端(API Client)的受控识别。点击后,进入API客户端(API Client)的受控识别页面,选择左侧选单里的权限页签,可以看到授权给API客户端的权限。(这页也可以撤销权限)
加入专案
申请服务完成之后,就可以开始建立专案并且加入模组。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」。
2.依照模组使用-API服务端(API Provider)-申请服务的步骤流程,申请AzureAD提供的OAuth服务,并取得API服务端的:「目录 (租用户) 识别码」、「应用程式 (用户端) 识别码」、「应用程式识别码 URI」。
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。
建立API客户端(API Client)
1.依照开发一个从GitHub持续布署到Azure Container Apps的Web站台的步骤流程,建立:api-client容器应用、ApiClient程式专案,并且取得API客户端的:「应用程式 URL」。
2.依照模组使用-API客户端(API Client)-申请服务的步骤流程,开启API客户端的受控识别。
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。
范例执行
1.使用Browser视窗,开启API客户端的「应用程式 URL」。于开启的Browser视窗内,可以看到系统画面进入到Home页面,并且显示API服务端回传的”Hello World”。
2.登入Microsoft Azure Portal。于API服务端的容器应用页面,进入纪录资料流页签,可以看到通过Service身分验证的API客户端身分资料(Controller.User属性),并且包含「应用程式角色」的资料。