我們公司採用 Microsoft 365 辦公協作環境,這也意味著背後一定使用了 Azure AD 目錄服務,因此我們可以直接拿 Azure AD 作為公司的 OAuth 2.0 + OpenId Connect (OIDC) 認證與授權平台,因此開發公司內部應用程式就變的非常容易。這篇文章我將說明要將一個 ASP.NET Core 網站加入 Azure AD 的身份驗證流程,以及使用時的注意事項。
建立全新 ASP.NET Core MVC 專案並設定 Azure AD 登入整合 (OpenId Connect)
-
先註冊應用程式
https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps
依照下圖進行設定,尤其是 Redirect URI 千萬別忘了,網址要先設定為 https://localhost:9001/signin-oidc
:
建立好之後,記得要到 Authentication 頁籤勾選 ID tokens
核取項目, 這樣才能在 Implicit Flow 取得使用者身份,這也才能完成 OpenId Connect 的認證流程!
建立完成後,請記得從 Azure AD 取得 Application (client) ID
, Directory (tenant) ID
與 Primary domain
這三個重要參數!
-
建立 ASP.NET Core MVC 專案
依據步驟 1 取得的三項資訊,拿來建立全新的 MVC 應用程式:
dotnet new mvc -n AzureADLoginDemo --auth SingleOrg --tenant-id 76275315-xxxx-xxxx-xxxx-a329d6222150 --domain xxxxxx.onmicrosoft.com --client-id 7332902c-xxxx-xxxx-xxxx-63a0dcbf3d80
建立好專案骨架後,先調整專案下的 Properties\launchSettings.json
檔案,將 .profiles.AzureADLoginDemo.applicationUrl
的內容修改為 https://localhost:9001
即可!
-
大功告成!
真的不誇張,就是上面這樣,兩個步驟就可以完成一個 ASP.NET Core 網站整合 Azure AD 的 OpenId Connect 登入!👍
接著啟動 ASP.NET Core 網站:
dotnet run
開啟 https://localhost:9001 就會自動轉向到 Azure AD 進行登入,登入成功後就可以看到首頁顯示出畫面了!
如何將現有網站加入 Azure AD 登入整合 (OpenId Connect)
說實在的,程式碼真的也不多,以下是從無到有的設定步驟:
-
先註冊應用程式 (設定內容與上一段敘述相同,在此省略說明)
-
建立 ASP.NET Core MVC 專案
dotnet new mvc -n AzureADLoginDemo
cd AzureADLoginDemo
code .
建立好專案骨架後,先調整專案下的 Properties\launchSettings.json
檔案,將 .profiles.AzureADLoginDemo.applicationUrl
的內容修改為 https://localhost:9001
即可!
-
修改 appsettings.json
組態設定
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "xxxxx.onmicrosoft.com",
"TenantId": "76275315-xxxx-xxxx-xxxx-a329d6222150",
"ClientId": "7332902c-xxxx-xxxx-xxxx-63a0dcbf3d80",
"CallbackPath": "/signin-oidc"
}
}
-
加入以下 NuGet 套件
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
dotnet add package Microsoft.Identity.Web
dotnet add package Microsoft.Identity.Web.UI
-
修改 Program.cs
加入 Services 與 Middleware 宣告
加入 Service 宣告
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
builder.Services.AddRazorPages()
.AddMicrosoftIdentityUI();
加入 Middleware 宣告
app.UseAuthentication(); // <-- 加入這行
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}")
.RequireAuthorization(); // <-- 加入這行
app.MapRazorPages(); // <-- 加入這行
做到這個步驟基本上就已經完成設定了!
-
加入一些 UI 顯示登入狀態
加入一個 Views/Shared/_LoginPartial.cshtml
檔案
@using System.Security.Principal
<ul class="navbar-nav">
@if (User.Identity?.IsAuthenticated == true)
{
<span class="navbar-text text-dark">Hello @User.Identity?.Name!</span>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignOut">Sign out</a>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignIn">Sign in</a>
</li>
}
</ul>
到 Views/Shared/_Layout.cshtml
加入 Partial View 載入(請加在選單項目的右邊,約第 28 行的位置)
<partial name="_LoginPartial" />
取得一些額外的使用者資訊 (Claims)
-
取得預設使用者名稱
User.Identity.Name
預設為 Azure AD 的 User Principal Name,也就是使用者的 E-mail 地址!
var email = User.Identity?.Name;
-
取得 Azure AD 使用者的顯示名稱
var displayName = User.FindFirst("name")?.Value;
-
取得 Azure AD 使用者的 ObjectId
var objectId = User.FindFirst(Microsoft.Identity.Web.ClaimConstants.ObjectId)?.Value;
-
取得 Azure AD 組織的 TenantId
var tenantId = User.FindFirst(Microsoft.Identity.Web.ClaimConstants.TenantId)?.Value;
-
顯示 OpenId Connect 登入過程中取得的 Claims 資訊
這邊我直接修改 Views\Home\Privacy.cshtml
頁面,範例程式如下:
@using Microsoft.AspNetCore.Authentication
<h2>Claims</h2>
<dl>
@foreach (var claim in User.Claims)
{
<dt>@claim.Type</dt>
<dd>@claim.Value</dd>
}
</dl>
<h2>Properties</h2>
<dl>
@{
var items = (await Context.AuthenticateAsync()).Properties?.Items;
}
@if (items != null) foreach (var prop in items)
{
<dt>@prop.Key</dt>
<dd>@prop.Value</dd>
}
</dl>
取得使用者的 Access Token 並呼叫 Microsoft Graph API
當你取得使用者身份之後,或許你還需要呼叫 Microsoft Graph API 取得更多的使用者資料,這個時候你會需要微調一下專案設定。
-
啟用 Token 獲取機制 (Token Acquisition)
只要修改 Program.cs
註冊 .AddMicrosoftIdentityWebApp()
的地方,加入兩行:
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi() // <!-- 加上這一行
.AddInMemoryTokenCaches(); // <!-- 加上這一行
你可以從 EnableTokenAcquisitionToCallDownstreamApi()
這個 API 的命名,大致得知他的真正用途。這句話的的意思就是「啟用 Token 獲取,然後可以呼叫接下來的的 API」,非常直覺的方法命名。另一行 AddInMemoryTokenCaches()
則是會將使用者的 Access Token 快取在記憶體中,避免過於頻繁的呼叫 Azure AD 的 Token Endpoint!
-
你必須要從 appsettings.json
加入 ClientSecret
否則將無法取得 Access Token
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "xxxxx.onmicrosoft.com",
"TenantId": "76275315-xxxx-xxxx-xxxx-a329d6222150",
"ClientId": "7332902c-xxxx-xxxx-xxxx-63a0dcbf3d80",
"ClientSecret": "2JY7Q~xxxx~u-xxxxxx-xxxx_mnz2VX4a-", // <-- 加上這一行
"CallbackPath": "/signin-oidc"
}
}
-
修改 HomeController
並注入 ITokenAcquisition
服務
private readonly ITokenAcquisition tokenAcquisition;
public HomeController(ITokenAcquisition tokenAcquisition)
{
this.tokenAcquisition = tokenAcquisition;
}
-
取得 Access Token 的方法
在 ASP.NET Core MVC 的 Controller 中,你通常會需要套用一個 [AuthorizeForScopes(Scopes = new[] { "user.read" })]
屬性 (Attribute),這是為了避免當執行 tokenAcquisition.GetAccessTokenForUserAsync(scopes);
的時候如果發生例外,會將使用者重新導向到 Azure AD 的授權頁面,重新走一次 OAuth 2.0 授權流程,讓使用者同意你可以取得 user.read
範圍的資料(就是使用者所有個人資料)。
[AuthorizeForScopes(Scopes = new[] { "user.read" })]
public async Task<IActionResult> GetMyProfile()
{
string[] scopes = new []{"user.read"};
string token = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);
ViewBag.token = token;
return View();
}
-
呼叫 Microsoft Graph API 取得完整的個人資料
這裡我打算拿 Access Token 呼叫 https://graph.microsoft.com/v1.0/me 這個端點,取得完整的個人資料!
我們先安裝 IdentityServer4 提供的 IdentityModel
好用套件:
dotnet add package IdentityModel
記得先引入 IdentityModel.Client
命名空間:
using IdentityModel.Client;
接著用以下程式碼就可以很漂亮的取得使用者在 Azure AD 的個人資料了!
[AuthorizeForScopes(Scopes = new[] { "user.read" })]
public async Task<IActionResult> GetMyProfile()
{
string[] scopes = new []{"user.read"};
string token = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);
var http = httpClientFactory.CreateClient();
http.SetBearerToken(token);
var result = await http.GetStringAsync("https://graph.microsoft.com/v1.0/me");
return Ok(result);
}
若要取得使用者在 Azure AD 的照片,可以參考以下程式碼,一樣呼叫 Microsoft Graph API 就可以取得!
[AuthorizeForScopes(Scopes = new[] { "user.read" })]
public async Task<IActionResult> GetMyPhoto()
{
string[] scopes = new []{"user.read"};
string token = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);
var http = httpClientFactory.CreateClient();
http.SetBearerToken(token);
var result = await http.GetStreamAsync("https://graph.microsoft.com/v1.0/me/photo/$value");
return File(result, "image/jpeg");
}
相關連結