เพิ่มการยืนยันตัวตนให้กับแอป .NET Core (Razor Pages) ของคุณ (Add authentication to your .NET Core (Razor Pages) application)
- ตัวอย่างสาธิตต่อไปนี้สร้างขึ้นบน .NET Core 8.0 โดย SDK สามารถใช้งานร่วมกับ .NET 6.0 ขึ้นไปได้
- โปรเจกต์ตัวอย่าง .NET Core มีให้ใน GitHub repository
ข้อกำหนดเบื้องต้น
- บัญชี Logto Cloud หรือ Logto แบบโฮสต์เอง
- สร้างแอปพลิเคชันเว็บแบบดั้งเดิมของ Logto แล้ว
การติดตั้ง
เพิ่มแพ็กเกจ NuGet ลงในโปรเจกต์ของคุณ:
dotnet add package Logto.AspNetCore.Authentication
การผสานรวม
เพิ่มการยืนยันตัวตนของ Logto
เปิด Startup.cs
(หรือ Program.cs
) และเพิ่มโค้ดต่อไปนี้เพื่อจดทะเบียนบริการการยืนยันตัวตน Logto:
using Logto.AspNetCore.Authentication;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddLogtoAuthentication(options =>
{
options.Endpoint = builder.Configuration["Logto:Endpoint"]!;
options.AppId = builder.Configuration["Logto:AppId"]!;
options.AppSecret = builder.Configuration["Logto:AppSecret"];
});
เมธอด AddLogtoAuthentication
จะดำเนินการดังต่อไปนี้:
- กำหนดสคีมการยืนยันตัวตนเริ่มต้นเป็น
LogtoDefaults.CookieScheme
- กำหนดสคีม challenge เริ่มต้นเป็น
LogtoDefaults.AuthenticationScheme
- กำหนดสคีม sign-out เริ่มต้นเป็น
LogtoDefaults.AuthenticationScheme
- เพิ่ม handler สำหรับ cookie และ OpenID Connect ลงในสคีมการยืนยันตัวตน
กระบวนการลงชื่อเข้าใช้และออกจากระบบ
ก่อนที่เราจะดำเนินการต่อ มีคำศัพท์ที่อาจสร้างความสับสนอยู่ 2 คำใน middleware การยืนยันตัวตนของ .NET Core ที่ควรชี้แจงให้ชัดเจน:
- CallbackPath: URI ที่ Logto จะเปลี่ยนเส้นทาง (redirect) ผู้ใช้กลับมาหลังจากที่ผู้ใช้ลงชื่อเข้าใช้แล้ว (คือ "redirect URI" ใน Logto)
- RedirectUri: URI ที่จะถูกเปลี่ยนเส้นทางไปหลังจากดำเนินการที่จำเป็นใน middleware การยืนยันตัวตนของ Logto เสร็จสิ้น
กระบวนการลงชื่อเข้าใช้สามารถอธิบายได้ดังนี้:
ในทำนองเดียวกัน .NET Core ยังมี SignedOutCallbackPath และ RedirectUri สำหรับขั้นตอนการลงชื่อออก
เพื่อความชัดเจน เราจะอ้างอิงคำเหล่านี้ดังนี้:
คำที่เราใช้ | คำศัพท์ใน .NET Core |
---|---|
Logto redirect URI | CallbackPath |
Logto post sign-out redirect URI | SignedOutCallbackPath |
Application redirect URI | RedirectUri |
เกี่ยวกับการลงชื่อเข้าใช้แบบเปลี่ยนเส้นทาง (redirect-based sign-in)
- กระบวนการยืนยันตัวตนนี้เป็นไปตามโปรโตคอล OpenID Connect (OIDC) และ Logto บังคับใช้มาตรการรักษาความปลอดภัยอย่างเข้มงวดเพื่อปกป้องการลงชื่อเข้าใช้ของผู้ใช้
- หากคุณมีหลายแอป คุณสามารถใช้ผู้ให้บริการข้อมูลระบุตัวตน (Logto) เดียวกันได้ เมื่อผู้ใช้ลงชื่อเข้าใช้แอปหนึ่งแล้ว Logto จะดำเนินการลงชื่อเข้าใช้โดยอัตโนมัติเมื่อผู้ใช้เข้าถึงแอปอื่น
หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับเหตุผลและประโยชน์ของการลงชื่อเข้าใช้แบบเปลี่ยนเส้นทาง โปรดดูที่ อธิบายประสบการณ์การลงชื่อเข้าใช้ของ Logto
กำหนดค่า redirect URI
ในตัวอย่างโค้ดต่อไปนี้ เราถือว่าแอปของคุณกำลังทำงานอยู่ที่ http://localhost:3000/
ก่อนอื่น มาตั้งค่า Logto redirect URI กันก่อน เพิ่ม URI ต่อไปนี้ลงในรายการ "Redirect URIs" ในหน้ารายละเอียดแอป Logto:
http://localhost:3000/Callback
หากต้องการตั้งค่า Logto post sign-out redirect URI ให้เพิ่ม URI ต่อไปนี้ลงในรายการ "Post sign-out redirect URIs" ในหน้ารายละเอียดแอป Logto:
http://localhost:3000/SignedOutCallback
เปลี่ยนเส้นทางเริ่มต้น
Logto redirect URI มีเส้นทางเริ่มต้นเป็น /Callback
และ Logto post sign-out redirect URI มีเส้นทางเริ่มต้นเป็น /SignedOutCallback
คุณสามารถปล่อยไว้ตามเดิมหากไม่มีข้อกำหนดพิเศษ หากต้องการเปลี่ยน สามารถตั้งค่าคุณสมบัติ CallbackPath
และ SignedOutCallbackPath
สำหรับ LogtoOptions
ได้ดังนี้:
builder.Services.AddLogtoAuthentication(options =>
{
// การตั้งค่าอื่น ๆ...
options.CallbackPath = "/Foo";
options.SignedOutCallbackPath = "/Bar";
});
อย่าลืมอัปเดตค่าดังกล่าวในหน้ารายละเอียดแอป Logto ให้ตรงกันด้วย
สร้างปุ่มลงชื่อเข้าใช้และออกจากระบบ
ก่อนอื่น ให้เพิ่มเมธอด handler ลงใน PageModel
ของคุณ ตัวอย่างเช่น:
// เพิ่มเมธอด handler สำหรับการลงชื่อเข้าใช้และออกจากระบบ
public class IndexModel : PageModel
{
public async Task OnPostSignInAsync()
{
await HttpContext.ChallengeAsync(new AuthenticationProperties
{
RedirectUri = "/"
});
}
public async Task OnPostSignOutAsync()
{
await HttpContext.SignOutAsync(new AuthenticationProperties
{
RedirectUri = "/"
});
}
}
จากนั้น เพิ่มปุ่มลงในหน้า Razor ของคุณ:
<p>ตรวจสอบการยืนยันตัวตน: @User.Identity?.IsAuthenticated</p>
<form method="post">
@if (User.Identity?.IsAuthenticated == true) {
<button type="submit" asp-page-handler="SignOut">ออกจากระบบ</button>
} else {
<button type="submit" asp-page-handler="SignIn">ลงชื่อเข้าใช้</button>
}
</form>
ระบบจะแสดงปุ่ม "ลงชื่อเข้าใช้" หากผู้ใช้ยังไม่ได้รับการยืนยันตัวตน และจะแสดงปุ่ม "ออกจากระบบ" หากผู้ใช้ได้รับการยืนยันตัวตนแล้ว
จุดตรวจสอบ: ทดสอบแอปพลิเคชันของคุณ
ตอนนี้คุณสามารถทดสอบแอปพลิเคชันของคุณได้แล้ว:
- รันแอปพลิเคชันของคุณ คุณจะเห็นปุ่มลงชื่อเข้าใช้
- คลิกปุ่มลงชื่อเข้าใช้ SDK จะเริ่มกระบวนการลงชื่อเข้าใช้และเปลี่ยนเส้นทางคุณไปยังหน้าลงชื่อเข้าใช้ของ Logto
- หลังจากที่คุณลงชื่อเข้าใช้แล้ว คุณจะถูกเปลี่ยนเส้นทางกลับไปยังแอปพลิเคชันของคุณและเห็นปุ่มลงชื่อออก
- คลิกปุ่มลงชื่อออกเพื่อเคลียร์ที่เก็บโทเค็นและออกจากระบบ
รับข้อมูลผู้ใช้
แสดงข้อมูลผู้ใช้
หากต้องการทราบว่าผู้ใช้ได้รับการยืนยันตัวตนหรือไม่ คุณสามารถตรวจสอบพร็อพเพอร์ตี้ User.Identity?.IsAuthenticated
หากต้องการรับการอ้างสิทธิ์ (claims) โปรไฟล์ของผู้ใช้ คุณสามารถใช้พร็อพเพอร์ตี้ User.Claims
ได้ดังนี้:
var claims = User.Claims;
// รับ user ID
var userId = claims.FirstOrDefault(c => c.Type == LogtoParameters.Claims.Subject)?.Value;
ดู LogtoParameters.Claims
สำหรับรายชื่อการอ้างสิทธิ์และความหมาย
ขอการอ้างสิทธิ์เพิ่มเติม
คุณอาจพบว่าข้อมูลผู้ใช้บางอย่างหายไปในอ็อบเจกต์ที่ส่งคืนจาก User.Claims
สาเหตุเนื่องจาก 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
ในอ็อบเจกต์ options
ได้ดังนี้:
builder.Services.AddLogtoAuthentication(options =>
{
// ...
options.Scopes = new string[] {
LogtoParameters.Scopes.Email,
LogtoParameters.Scopes.Phone
}
});
จากนั้นคุณสามารถเข้าถึงการอ้างสิทธิ์เพิ่มเติมผ่าน User.Claims
ได้:
var claims = User.Claims;
// รับอีเมลของผู้ใช้
var email = claims.FirstOrDefault(c => c.Type == LogtoParameters.Claims.Email)?.Value;
การอ้างสิทธิ์ที่ต้องร้องขอผ่านเครือข่าย
เพื่อป้องกันไม่ให้อ็อบเจกต์ผู้ใช้มีขนาดใหญ่เกินไป การอ้างสิทธิ์บางรายการจำเป็นต้องร้องขอผ่านเครือข่าย เช่น การอ้างสิทธิ์ custom_data จะไม่ถูกรวมอยู่ในอ็อบเจกต์ผู้ใช้ แม้ว่าจะร้องขอไว้ใน scopes ก็ตาม หากต้องการดึงการอ้างสิทธิ์เหล่านี้ คุณสามารถตั้งค่า GetClaimsFromUserInfoEndpoint
เป็น true
ในอ็อบเจกต์ options
ได้ดังนี้:
builder.Services.AddLogtoAuthentication(options =>
{
// ...
options.GetClaimsFromUserInfoEndpoint = true;
});
ขอบเขตและการอ้างสิทธิ์ {scopes-and-claims}
Logto ใช้มาตรฐาน ขอบเขต (scopes) และ การอ้างสิทธิ์ (claims) ของ OIDC เพื่อกำหนดขอบเขตและการอ้างสิทธิ์สำหรับดึงข้อมูลผู้ใช้จากโทเค็น ID (ID token) และ OIDC userinfo endpoint ทั้ง "ขอบเขต (scope)" และ "การอ้างสิทธิ์ (claim)" เป็นคำศัพท์จากข้อกำหนดของ OAuth 2.0 และ OpenID Connect (OIDC)
ต่อไปนี้คือรายการขอบเขต (Scopes) ที่รองรับและการอ้างสิทธิ์ (Claims) ที่เกี่ยวข้อง:
openid
ชื่อการอ้างสิทธิ์ | ประเภท | คำอธิบาย | ต้องใช้ userinfo หรือไม่? |
---|---|---|---|
sub | string | ตัวระบุที่ไม่ซ้ำของผู้ใช้ | ไม่ |
profile
ชื่อการอ้างสิทธิ์ | ประเภท | คำอธิบาย | ต้องใช้ userinfo หรือไม่? |
---|---|---|---|
name | string | ชื่อเต็มของผู้ใช้ | ไม่ |
username | string | ชื่อผู้ใช้ของผู้ใช้ | ไม่ |
picture | string | URL ของรูปโปรไฟล์ของผู้ใช้ปลายทาง 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
ชื่อการอ้างสิทธิ์ | ประเภท | คำอธิบาย | ต้องใช้ userinfo หรือไม่? |
---|---|---|---|
string | อีเมลของผู้ใช้ | ไม่ | |
email_verified | boolean | อีเมลได้รับการยืนยันแล้วหรือไม่ | ไม่ |
phone
ชื่อการอ้างสิทธิ์ | ประเภท | คำอธิบาย | ต้องใช้ userinfo หรือไม่? |
---|---|---|---|
phone_number | string | หมายเลขโทรศัพท์ของผู้ใช้ | ไม่ |
phone_number_verified | boolean | หมายเลขโทรศัพท์ได้รับการยืนยันแล้วหรือไม่ | ไม่ |
address
โปรดดูรายละเอียดของการอ้างสิทธิ์ที่อยู่ได้ที่ OpenID Connect Core 1.0
custom_data
ชื่อการอ้างสิทธิ์ | ประเภท | คำอธิบาย | ต้องใช้ userinfo หรือไม่? |
---|---|---|---|
custom_data | object | ข้อมูลกำหนดเองของผู้ใช้ | ใช่ |
identities
ชื่อการอ้างสิทธิ์ | ประเภท | คำอธิบาย | ต้องใช้ userinfo หรือไม่? |
---|---|---|---|
identities | object | ข้อมูลตัวตนที่เชื่อมโยงของผู้ใช้ | ใช่ |
sso_identities | array | ข้อมูล SSO ที่เชื่อมโยงของผู้ใช้ | ใช่ |
roles
ชื่อการอ้างสิทธิ์ | ประเภท | คำอธิบาย | ต้องใช้ userinfo หรือไม่? |
---|---|---|---|
roles | string[] | บทบาทของผู้ใช้ | ไม่ |
urn:logto:scope:organizations
ชื่อการอ้างสิทธิ์ | ประเภท | คำอธิบาย | ต้องใช้ userinfo หรือไม่? |
---|---|---|---|
organizations | string[] | รหัสองค์กรที่ผู้ใช้สังกัด | ไม่ |
organization_data | object[] | ข้อมูลขององค์กรที่ผู้ใช้สังกัด | ใช่ |
urn:logto:scope:organization_roles
ชื่อการอ้างสิทธิ์ | ประเภท | คำอธิบาย | ต้องใช้ userinfo หรือไม่? |
---|---|---|---|
organization_roles | string[] | บทบาทของผู้ใช้ในแต่ละองค์กรในรูปแบบ <organization_id>:<role_name> | ไม่ |
เพื่อประสิทธิภาพและขนาดข้อมูล หาก "ต้องใช้ userinfo หรือไม่?" เป็น "ใช่" หมายความว่าการอ้างสิทธิ์นั้นจะไม่ปรากฏในโทเค็น ID แต่จะถูกส่งกลับใน response ของ userinfo endpoint
ทรัพยากร API (API resources)
เราแนะนำให้อ่าน 🔐 การควบคุมการเข้าถึงตามบทบาท (RBAC) ก่อน เพื่อทำความเข้าใจแนวคิดพื้นฐานของ RBAC ใน Logto และวิธีตั้งค่าทรัพยากร API อย่างถูกต้อง
กำหนดค่าทรัพยากร API ในแอปของคุณ
เมื่อคุณตั้งค่า ทรัพยากร API (API resources) เรียบร้อยแล้ว คุณสามารถเพิ่มทรัพยากรเหล่านี้ขณะกำหนดค่า Logto ในแอปของคุณได้:
builder.Services.AddLogtoAuthentication(options =>
{
// ...
// กำหนดตัวบ่งชี้ทรัพยากร API ของคุณ
options.Resource = "https://<your-api-resource-indicator>";
});
แต่ละ ทรัพยากร API (API resource) จะมี สิทธิ์ (scopes) ของตัวเอง
ตัวอย่างเช่น ทรัพยากร https://shopping.your-app.com/api
มีสิทธิ์ shopping:read
และ shopping:write
และทรัพยากร https://store.your-app.com/api
มีสิทธิ์ store:read
และ store:write
หากต้องการร้องขอสิทธิ์เหล่านี้ คุณสามารถเพิ่มขณะกำหนดค่า Logto ในแอปของคุณได้:
builder.Services.AddLogtoAuthentication(options =>
{
// ...
// กำหนดทรัพยากร API ที่ต้องการ
options.Resource = "https://shopping.your-app.com/api";
// กำหนดขอบเขต (scopes) ที่ต้องการ
options.Scopes = new string[] {
"openid",
"profile",
"offline_access",
"read",
"write"
};
});
คุณอาจสังเกตได้ว่า ขอบเขต (scopes) ถูกกำหนดแยกจาก ทรัพยากร API (API resources) นี่เป็นเพราะ Resource Indicators for OAuth 2.0 ระบุว่า ขอบเขตสุดท้ายสำหรับคำขอจะเป็นผลคูณคาร์ทีเซียนของขอบเขตทั้งหมดในบริการเป้าหมายทั้งหมด
คุณสามารถร้องขอขอบเขต (scopes) ที่ไม่ได้กำหนดไว้ใน ทรัพยากร API (API resources) ได้ เช่น คุณสามารถร้องขอขอบเขต email
ได้ แม้ว่า ทรัพยากร API (API resources) จะไม่มีขอบเขต email
ให้ ขอบเขตที่ไม่มีจะถูกละเว้นอย่างปลอดภัย
หลังจากลงชื่อเข้าใช้สำเร็จ Logto จะออกขอบเขตที่เหมาะสมให้กับ ทรัพยากร API (API resources) ตามบทบาทของผู้ใช้
ดึงโทเค็น (Fetch tokens)
บางครั้งคุณอาจจำเป็นต้องดึงโทเค็นการเข้าถึง (Access token) หรือโทเค็น ID (ID token) สำหรับการเรียก API คุณสามารถใช้เมธอด GetTokenAsync
เพื่อดึงโทเค็นเหล่านี้ได้:
var accessToken = await HttpContext.GetTokenAsync(LogtoParameters.Tokens.AccessToken);
var idToken = await HttpContext.GetTokenAsync(LogtoParameters.Tokens.IdToken);
ไม่จำเป็นต้องกังวลเกี่ยวกับการหมดอายุของโทเค็น มิดเดิลแวร์การยืนยันตัวตนจะรีเฟรชโทเค็นโดยอัตโนติเมื่อจำเป็น
แม้มิดเดิลแวร์การยืนยันตัวตนจะรีเฟรชโทเค็นโดยอัตโนมัติ แต่การอ้างสิทธิ์ (Claims) ในอ็อบเจกต์ผู้ใช้จะไม่ถูกอัปเดตเนื่องจากข้อจำกัดของตัวจัดการการยืนยันตัวตน OpenID Connect ที่อยู่เบื้องหลัง ปัญหานี้จะแก้ไขได้เมื่อเราเขียนตัวจัดการการยืนยันตัวตนของเราเอง
โปรดทราบว่าโทเค็นการเข้าถึง (Access token) ข้างต้นเป็นโทเค็นทึบ (Opaque token) สำหรับ userinfo endpoint ใน OpenID Connect ซึ่งไม่ใช่ JWT หากคุณได้ระบุทรัพยากร API (API resource) แล้ว คุณต้องใช้ LogtoParameters.Tokens.AccessTokenForResource
เพื่อดึงโทเค็นการเข้าถึงสำหรับทรัพยากร API:
var accessToken = await HttpContext.GetTokenAsync(LogtoParameters.Tokens.AccessTokenForResource);
โทเค็นนี้จะเป็น JWT โดยมีทรัพยากร API เป็นผู้รับ (audience)