เพิ่มการยืนยันตัวตนให้กับแอป .NET Core (Blazor WASM) ของคุณ (Add authentication to your .NET Core (Blazor WASM) application)
- ตัวอย่างสาธิตต่อไปนี้สร้างขึ้นบน .NET Core 8.0 และ Blorc.OpenIdConnect
- โปรเจกต์ตัวอย่าง .NET Core มีให้ใน GitHub repository
ข้อกำหนดเบื้องต้น
- บัญชี Logto Cloud หรือ Logto ที่โฮสต์เอง
- สร้างแอป Logto แบบหน้าเดียวแล้ว
การติดตั้ง
เพิ่มแพ็กเกจ NuGet ลงในโปรเจกต์ของคุณ:
dotnet add package Blorc.OpenIdConnect
การผสานรวม
เพิ่มการอ้างอิงสคริปต์
เพิ่ม Blorc.Core/injector.js ลงในไฟล์ index.html:
<head>
<!-- ... -->
<script src="_content/Blorc.Core/injector.js"></script>
<!-- ... -->
</head>
ลงทะเบียนบริการ
เพิ่มโค้ดต่อไปนี้ในไฟล์ Program.cs:
using Blorc.OpenIdConnect;
using Blorc.Services;
builder.Services.AddBlorcCore();
builder.Services.AddAuthorizationCore();
builder.Services.AddBlorcOpenIdConnect(
options =>
{
builder.Configuration.Bind("IdentityServer", options);
});
var webAssemblyHost = builder.Build();
await webAssemblyHost
.ConfigureDocumentAsync(async documentService =>
{
await documentService.InjectBlorcCoreJsAsync();
await documentService.InjectOpenIdConnectAsync();
});
await webAssemblyHost.RunAsync();
ไม่จำเป็นต้องใช้แพ็กเกจ Microsoft.AspNetCore.Components.WebAssembly.Authentication แพ็กเกจ Blorc.OpenIdConnect จะดูแลกระบวนการยืนยันตัวตนให้เอง
กำหนดค่า redirect URI
ก่อนที่เราจะลงลึกในรายละเอียด นี่คือภาพรวมประสบการณ์ของผู้ใช้ปลายทาง กระบวนการลงชื่อเข้าใช้สามารถสรุปได้ดังนี้:
- แอปของคุณเรียกใช้งานเมธอดลงชื่อเข้าใช้
- ผู้ใช้จะถูกเปลี่ยนเส้นทางไปยังหน้าลงชื่อเข้าใช้ของ Logto สำหรับแอปเนทีฟ ระบบจะเปิดเบราว์เซอร์ของระบบ
- ผู้ใช้ลงชื่อเข้าใช้และถูกเปลี่ยนเส้นทางกลับไปยังแอปของคุณ (ตามที่กำหนดไว้ใน redirect URI)
เกี่ยวกับการลงชื่อเข้าใช้แบบเปลี่ยนเส้นทาง (redirect-based sign-in)
- กระบวนการยืนยันตัวตนนี้เป็นไปตามโปรโตคอล OpenID Connect (OIDC) และ Logto บังคับใช้มาตรการรักษาความปลอดภัยอย่างเข้มงวดเพื่อปกป้องการลงชื่อเข้าใช้ของผู้ใช้
- หากคุณมีหลายแอป คุณสามารถใช้ผู้ให้บริการข้อมูลระบุตัวตน (Logto) เดียวกันได้ เมื่อผู้ใช้ลงชื่อเข้าใช้แอปหนึ่งแล้ว Logto จะดำเนินการลงชื่อเข้าใช้โดยอัตโนมัติเมื่อผู้ใช้เข้าถึงแอปอื่น
หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับเหตุผลและประโยชน์ของการลงชื่อเข้าใช้แบบเปลี่ยนเส้นทาง โปรดดูที่ อธิบายประสบการณ์การลงชื่อเข้าใช้ของ Logto
ในตัวอย่างโค้ดต่อไปนี้ เราถือว่าแอปของคุณกำลังทำงานอยู่ที่ http://localhost:3000/
กำหนดค่า Redirect URI
ไปที่หน้ารายละเอียดแอปพลิเคชันใน Logto Console เพิ่ม redirect URI http://localhost:3000/callback
เช่นเดียวกับการลงชื่อเข้าใช้ ผู้ใช้ควรถูกเปลี่ยนเส้นทางไปที่ Logto เพื่อออกจากเซสชันที่ใช้ร่วมกัน เมื่อเสร็จสิ้นแล้ว ควรเปลี่ยนเส้นทางผู้ใช้กลับไปยังเว็บไซต์ของคุณ ตัวอย่างเช่น เพิ่ม http://localhost:3000/ ในส่วน post sign-out redirect URI
จากนั้นคลิก "Save" เพื่อบันทึกการเปลี่ยนแปลง
กำหนดค่าแอปพลิเคชัน
เพิ่มโค้ดต่อไปนี้ในไฟล์ appsettings.json:
{
// ...
IdentityServer: {
Authority: 'https://<your-logto-endpoint>/oidc',
ClientId: '<your-logto-app-id>',
PostLogoutRedirectUri: 'http://localhost:3000/',
RedirectUri: 'http://localhost:3000/callback',
ResponseType: 'code',
Scope: 'openid profile', // เพิ่มขอบเขต (scopes) เพิ่มเติมหากต้องการ
},
}
อย่าลืมเพิ่ม RedirectUri และ PostLogoutRedirectUri ลงในรายการ redirect URI ที่อนุญาตในหน้าตั้งค่าแอป Logto ทั้งสองคือ URL ของแอป WASM ของคุณ
เพิ่มคอมโพเนนต์ AuthorizeView
ในหน้า Razor ที่ต้องการการยืนยันตัวตน ให้เพิ่มคอมโพเนนต์ AuthorizeView สมมติว่าเป็นหน้า Home.razor:
@using Microsoft.AspNetCore.Components.Authorization
@page "/"
<AuthorizeView>
<Authorized>
@* มุมมองเมื่อเข้าสู่ระบบแล้ว *@
<button @onclick="OnLogoutButtonClickAsync">
Sign out
</button>
</Authorized>
<NotAuthorized>
@* มุมมองเมื่อยังไม่ได้เข้าสู่ระบบ *@
<button @onclick="OnLoginButtonClickAsync">
Sign in
</button>
</NotAuthorized>
</AuthorizeView>
ตั้งค่าการยืนยันตัวตน
ในไฟล์ Home.razor.cs (สร้างไฟล์นี้หากยังไม่มี) เพิ่มโค้ดต่อไปนี้:
// using ต่าง ๆ เหมือนเดิม
[Authorize]
public partial class Home : ComponentBase
{
[Inject]
public required IUserManager UserManager { get; set; }
public User<Profile>? User { get; set; }
[CascadingParameter]
protected Task<AuthenticationState>? AuthenticationStateTask { get; set; }
protected override async Task OnInitializedAsync()
{
User = await UserManager.GetUserAsync<User<Profile>>(AuthenticationStateTask!);
}
private async Task OnLoginButtonClickAsync(MouseEventArgs obj)
{
await UserManager.SignInRedirectAsync();
}
private async Task OnLogoutButtonClickAsync(MouseEventArgs obj)
{
await UserManager.SignOutRedirectAsync();
}
}
เมื่อผู้ใช้ได้รับการยืนยันตัวตนแล้ว property User จะถูกเติมข้อมูลผู้ใช้โดยอัตโนมัติ
จุดตรวจสอบ: ทดสอบแอปพลิเคชันของคุณ
ตอนนี้คุณสามารถทดสอบแอปพลิเคชันของคุณได้แล้ว:
- รันแอปพลิเคชันของคุณ คุณจะเห็นปุ่มลงชื่อเข้าใช้
- คลิกปุ่มลงชื่อเข้าใช้ SDK จะเริ่มกระบวนการลงชื่อเข้าใช้และเปลี่ยนเส้นทางคุณไปยังหน้าลงชื่อเข้าใช้ของ Logto
- หลังจากที่คุณลงชื่อเข้าใช้แล้ว คุณจะถูกเปลี่ยนเส้นทางกลับไปยังแอปพลิเคชันของคุณและเห็นปุ่มลงชื่อออก
- คลิกปุ่มลงชื่อออกเพื่อเคลียร์ที่เก็บโทเค็นและออกจากระบบ
รับข้อมูลผู้ใช้
แสดงข้อมูลผู้ใช้
ตัวอย่างต่อไปนี้แสดงวิธีแสดงข้อมูลผู้ใช้ในหน้า Home.razor:
<AuthorizeView>
<Authorized>
@* มุมมองเมื่อเข้าสู่ระบบแล้ว *@
@* ... *@
<p>คุณได้เข้าสู่ระบบในชื่อ @(@User?.Profile?.Name ?? "(ไม่ทราบชื่อ)").</p>
</Authorized>
@* ... *@
</AuthorizeView>
สำหรับพร็อพเพอร์ตี้และการอ้างสิทธิ์ (claims) อื่น ๆ เพิ่มเติม โปรดตรวจสอบคลาส User และ Profile ในแพ็กเกจ Blorc.OpenIdConnect
ขอการอ้างสิทธิ์เพิ่มเติม
คุณอาจพบว่าข้อมูลผู้ใช้บางอย่างหายไปในอ็อบเจกต์ที่ส่งคืนจาก User สาเหตุเนื่องจาก OAuth 2.0 และ OpenID Connect (OIDC) ถูกออกแบบมาให้สอดคล้องกับหลักการสิทธิ์น้อยที่สุด (principle of least privilege; PoLP) และ Logto ถูกสร้างขึ้นบนมาตรฐานเหล่านี้
โดยปกติแล้ว จะมีการส่งคืนการอ้างสิทธิ์ (claim) แบบจำกัด หากคุณต้องการข้อมูลเพิ่มเติม คุณสามารถร้องขอขอบเขต (scope) เพิ่มเติมเพื่อเข้าถึงการอ้างสิทธิ์ (claim) ที่มากขึ้นได้
"การอ้างสิทธิ์ (Claim)" คือการยืนยันข้อมูลบางอย่างเกี่ยวกับผู้ถูกอ้างถึง (subject); "ขอบเขต (Scope)" คือกลุ่มของการอ้างสิทธิ์ (claim) ในกรณีนี้ การอ้างสิทธิ์ (claim) คือข้อมูลบางอย่างเกี่ยวกับผู้ใช้
ตัวอย่างที่ไม่เป็นทางการของความสัมพันธ์ระหว่างขอบเขต (scope) กับการอ้างสิทธิ์ (claim) มีดังนี้:
การอ้างสิทธิ์ (claim) "sub" หมายถึง "ผู้ถูกอ้างถึง (subject)" ซึ่งคือตัวระบุที่ไม่ซ้ำของผู้ใช้ (เช่น user ID)
Logto SDK จะร้องขอขอบเขต (scope) สามรายการเสมอ ได้แก่ openid, profile และ offline_access
หากต้องการขอขอบเขต (scopes) เพิ่มเติม คุณสามารถเพิ่ม scopes ที่ถูกต้องลงในพร็อพเพอร์ตี้ IdentityServer.Scope ในไฟล์ appsettings.json
{
// ...
"IdentityServer": {
// ...
"Scope": "openid profile email phone"
}
}
การอ้างสิทธิ์ที่ต้องร้องขอผ่านเครือข่าย
เพื่อป้องกันไม่ให้วัตถุผู้ใช้มีขนาดใหญ่เกินไป การอ้างสิทธิ์บางอย่างจำเป็นต้องร้องขอผ่านเครือข่าย เช่น การอ้างสิทธิ์ custom_data จะไม่ถูกรวมอยู่ในวัตถุผู้ใช้ แม้ว่าจะร้องขอไว้ใน scopes ก็ตาม หากต้องการดึงการอ้างสิทธิ์เหล่านี้ คุณสามารถตั้งค่า IdentityServer.LoadUserInfo เป็น true ในไฟล์ appsettings.json
ตัวอย่างเช่น หากต้องการดึงที่อยู่อีเมลและ custom data ของผู้ใช้ สามารถใช้การตั้งค่าดังนี้:
{
// ...
"IdentityServer": {
// ...
"Scope": "openid profile email custom_data",
"LoadUserInfo": true
}
}
ขอบเขตและการอ้างสิทธิ์
Logto ใช้แนวทาง ขอบเขต (Scope) และ การอ้างสิทธิ์ (Claim) ของ OIDC เพื่อกำหนดขอบเขตและการอ้างสิทธิ์สำหรับดึงข้อมูลผู้ใช้จากโทเค็น ID (ID token) และ OIDC จุดปลาย userinfo ทั้ง "ขอบเขต (Scope)" และ "การอ้างสิทธิ์ (Claim)" เป็นคำศัพท์จากข้อกำหนดของ OAuth 2.0 และ OpenID Connect (OIDC)
สำหรับการอ้างสิทธิ์ OIDC มาตรฐาน การรวมอยู่ในโทเค็น ID จะถูกกำหนดอย่างเคร่งครัดโดยขอบเขตที่ร้องขอ การอ้างสิทธิ์เพิ่มเติม (เช่น custom_data และ organizations) สามารถตั้งค่าเพิ่มเติมให้ปรากฏในโทเค็น ID ได้ผ่าน การตั้งค่า Custom ID token
ต่อไปนี้คือรายการขอบเขต (Scopes) ที่รองรับและการอ้างสิทธิ์ (Claims) ที่เกี่ยวข้อง:
ขอบเขต OIDC มาตรฐาน
openid (ค่าเริ่มต้น)
| ชื่อการอ้างสิทธิ์ | ประเภท | คำอธิบาย |
|---|---|---|
| sub | string | ตัวระบุที่ไม่ซ้ำของผู้ใช้ |
profile (ค่าเริ่มต้น)
| ชื่อการอ้างสิทธิ์ | ประเภท | คำอธิบาย |
|---|---|---|
| name | string | ชื่อเต็มของผู้ใช้ |
| username | string | ชื่อผู้ใช้ของผู้ใช้ |
| picture | string | URL ของรูปโปรไฟล์ผู้ใช้ปลายทาง (End-User) URL นี้ต้องอ้างถึงไฟล์รูปภาพ (เช่น PNG, JPEG, หรือ GIF) ไม่ใช่หน้าเว็บที่มีรูปภาพ โปรดทราบว่า URL นี้ควรอ้างอิงถึงรูปโปรไฟล์ของผู้ใช้ปลายทางโดยเฉพาะ เหมาะสำหรับแสดงเมื่ออธิบายผู้ใช้ปลายทาง ไม่ใช่รูปภาพใด ๆ ที่ผู้ใช้ถ่ายมาโดยพลการ |
| created_at | number | เวลาที่สร้างผู้ใช้ปลายทาง เวลานี้แสดงเป็นจำนวนมิลลิวินาทีตั้งแต่ Unix epoch (1970-01-01T00:00:00Z) |
| updated_at | number | เวลาที่ข้อมูลของผู้ใช้ปลายทางถูกอัปเดตล่าสุด เวลานี้แสดงเป็นจำนวนมิลลิวินาทีตั้งแต่ Unix epoch (1970-01-01T00:00:00Z) |
การอ้างสิทธิ์มาตรฐาน อื่น ๆ เช่น family_name, given_name, middle_name, nickname, preferred_username, profile, website, gender, birthdate, zoneinfo, และ locale จะถูกรวมอยู่ในขอบเขต profile ด้วยโดยไม่จำเป็นต้องร้องขอ endpoint userinfo ความแตกต่างเมื่อเทียบกับการอ้างสิทธิ์ข้างต้นคือ การอ้างสิทธิ์เหล่านี้จะถูกส่งกลับเมื่อค่าของมันไม่ว่างเปล่าเท่านั้น ในขณะที่การอ้างสิทธิ์ข้างต้นจะคืนค่า null หากค่าว่างเปล่า
ต่างจากการอ้างสิทธิ์มาตรฐาน การอ้างสิทธิ์ created_at และ updated_at ใช้หน่วยเป็นมิลลิวินาทีแทนที่จะเป็นวินาที
email
| ชื่อการอ้างสิทธิ์ | ประเภท | คำอธิบาย |
|---|---|---|
string | อีเมลของผู้ใช้ | |
| email_verified | boolean | อีเมลได้รับการยืนยันแล้วหรือไม่ |
phone
| ชื่อการอ้างสิทธิ์ | ประเภท | คำอธิบาย |
|---|---|---|
| phone_number | string | เบอร์โทรศัพท์ของผู้ใช้ |
| phone_number_verified | boolean | เบอร์โทรศัพท์ได้รับการยืนยันแล้วหรือไม่ |
address
โปรดดูรายละเอียดของการอ้างสิทธิ์ที่อยู่ได้ที่ OpenID Connect Core 1.0
ขอบเขตที่มีเครื่องหมาย (ค่าเริ่มต้น) จะถูกร้องขอเสมอโดย Logto SDK การอ้างสิทธิ์ภายใต้ขอบเขต OIDC มาตรฐานจะถูกรวมอยู่ในโทเค็น ID เสมอเมื่อมีการร้องขอขอบเขตที่เกี่ยวข้อง — ไม่สามารถปิดได้
ขอบเขตเพิ่มเติม
ขอบเขตต่อไปนี้เป็นขอบเขตที่ Logto ขยายขึ้นและจะคืนค่าการอ้างสิทธิ์ผ่าน userinfo endpoint การอ้างสิทธิ์เหล่านี้ยังสามารถตั้งค่าให้ถูกรวมอยู่ในโทเค็น ID ได้โดยตรงผ่าน Console > Custom JWT ดู Custom ID token สำหรับรายละเอียดเพิ่มเติม
custom_data
| ชื่อการอ้างสิทธิ์ | ประเภท | คำอธิบาย | รวมในโทเค็น ID โดยค่าเริ่มต้น |
|---|---|---|---|
| custom_data | object | ข้อมูลกำหนดเองของผู้ใช้ |
identities
| ชื่อการอ้างสิทธิ์ | ประเภท | คำอธิบาย | รวมในโทเค็น ID โดยค่าเริ่มต้น |
|---|---|---|---|
| identities | object | ข้อมูลตัวตนที่เชื่อมโยงของผู้ใช้ | |
| sso_identities | array | ข้อมูล SSO ที่เชื่อมโยงของผู้ใช้ |
roles
| ชื่อการอ้างสิทธิ์ | ประเภท | คำอธิบาย | รวมในโทเค็น ID โดยค่าเริ่มต้น |
|---|---|---|---|
| roles | string[] | บทบาท (Role) ของผู้ใช้ | ✅ |
urn:logto:scope:organizations
| ชื่อการอ้างสิทธิ์ | ประเภท | คำอธิบาย | รวมในโทเค็น ID โดยค่าเริ่มต้น |
|---|---|---|---|
| organizations | string[] | รหัสองค์กรที่ผู้ใช้สังกัด | ✅ |
| organization_data | object[] | ข้อมูลองค์กรที่ผู้ใช้สังกัด |
การอ้างสิทธิ์ขององค์กรเหล่านี้สามารถดึงได้ผ่าน userinfo endpoint เมื่อใช้ โทเค็นทึบ (Opaque token) อย่างไรก็ตาม โทเค็นทึบไม่สามารถใช้เป็นโทเค็นองค์กรสำหรับเข้าถึงทรัพยากรเฉพาะองค์กร ดู โทเค็นทึบและองค์กร สำหรับรายละเอียดเพิ่มเติม
urn:logto:scope:organization_roles
| ชื่อการอ้างสิทธิ์ | ประเภท | คำอธิบาย | รวมในโทเค็น ID โดยค่าเริ่มต้น |
|---|---|---|---|
| organization_roles | string[] | บทบาทขององค์กรที่ผู้ใช้สังกัดในรูปแบบ <organization_id>:<role_name> | ✅ |
ทรัพยากร API
เราแนะนำให้อ่าน 🔐 การควบคุมการเข้าถึงตามบทบาท (RBAC) ก่อน เพื่อทำความเข้าใจแนวคิดพื้นฐานของ RBAC ใน Logto และวิธีตั้งค่าทรัพยากร API อย่างถูกต้อง
โดยปกติ เมื่อคุณเข้าถึง User?.AccessToken คุณจะได้รับโทเค็นทึบ (Opaque token) ซึ่งมีความยาวสั้นและไม่ใช่ JWT (JSON Web Token) โทเค็นนี้ใช้สำหรับเข้าถึง userinfo endpoint
เพิ่มทรัพยากร API (API resource) ลงในการตั้งค่า
หากต้องการรับ JWT ที่สามารถใช้เข้าถึงทรัพยากร API ที่กำหนดไว้ใน Logto ให้เพิ่มพารามิเตอร์ต่อไปนี้ลงในไฟล์ appsettings.json (โดยใช้ https://my-api-resource เป็นตัวอย่าง):
{
// ...
"IdentityServer": {
// ...
"Scope": "openid profile email <your-api-scopes>", // เพิ่มขอบเขต API ของคุณที่นี่
"Resource": "https://my-api-resource",
"ExtraTokenParams": {
"resource": "https://my-api-resource" // ตรวจสอบให้แน่ใจว่า key "resource" เป็นตัวพิมพ์เล็กทั้งหมด
}
}
}
พร็อพเพอร์ตี้ Resource ใช้สำหรับเพิ่มทรัพยากร API ลงในคำขอการยืนยันตัวตน (auth request) ส่วนพร็อพเพอร์ตี้ ExtraTokenParams ใช้สำหรับเพิ่มทรัพยากร API ลงในคำขอโทเค็น (token request) เนื่องจาก Logto ปฏิบัติตาม RFC 8707 สำหรับทรัพยากร API จึงต้องกำหนดทั้งสองพร็อพเพอร์ตี้นี้
เนื่องจาก WebAssembly เป็นแอปพลิเคชันฝั่งไคลเอนต์ คำขอโทเค็นจะถูกส่งไปยังฝั่งเซิร์ฟเวอร์เพียงครั้งเดียว ด้วยเหตุนี้ LoadUserInfo จะขัดแย้งกับการดึงโทเค็นการเข้าถึง (Access token) สำหรับทรัพยากร API
ใช้โทเค็นการเข้าถึง (Access token)
เมื่อผู้ใช้ได้รับการยืนยันตัวตนแล้ว คุณสามารถเข้าถึงทรัพยากร API ได้โดยใช้พร็อพเพอร์ตี้ User?.AccessToken ตัวอย่างเช่น คุณสามารถใช้ HttpClient เพื่อเข้าถึงทรัพยากร API ได้ดังนี้:
using Blorc.OpenIdConnect;
// เพิ่ม HttpClient สำหรับทรัพยากร API ของคุณ
builder.Services
.AddHttpClient("MyApiResource", client =>
{
client.BaseAddress = new Uri("https://my-api-resource");
})
.AddAccessToken(); // เพิ่มโทเค็นการเข้าถึงใน request header