(ASP.NET Razor) Sử dụng tài khoản Google để xác thực trong Identity
Hướng dẫn cấu hình sử dụng dịch vụ ngoài để xác thực trong Identity, đăng ký OAuth của Google áp dụng vào dự án, tùy biến trang ExternalLogin của Identity
Giới thiệu về tính năng đăng ký và xác thực từ dịch vụ ngoài
Ứng dụng ASP.NET Core 3.x cho phép bạn sử dụng đăng nhập bằng OAuth 2.0
(API gọi dịch vụ xác thực từ bên thứ 3 - nhà cung cấp provider). Những provider có có sẵn thư viện để sử dụng trong Identity là Facebook, Google, Twitter, Microsoft. Nếu muốn sử dụng các provider khác như GitHub, Yahoo, Zalo ... thì nên sử dụng package AspNet.Security.OAuth.Providers
Hãy thêm Package tương ứng với Provider mà bạn muốn sử dụng:
dotnet add package Microsoft.AspNetCore.Authentication.Facebook dotnet add package Microsoft.AspNetCore.Authentication.Google dotnet add package Microsoft.AspNetCore.Authentication.MicrosoftAccount dotnet add package Microsoft.AspNetCore.Authentication.Twitter
Sau đó - để sử dụng Provider nào thì cần cấu hình dịch vụ cho provider đó vào Identity. Thực hiện điều này bằng cập nhật ConfigureServices
của lớp Startup
services.AddAuthentication() .AddMicrosoftAccount(microsoftOptions => { ... }) // thêm provider Microsoft và cấu hình .AddGoogle(googleOptions => { ... }) // thêm provider Google và cấu hình .AddTwitter(twitterOptions => { ... }) // thêm provider Twitter và cấu hình .AddFacebook(facebookOptions => { ... }); // thêm provider Facebook và cấu hình
Tùy thuộc vào Provider mà có thể có các cấu hình riêng, tuy nhiên điểm chung là ta phải thiết lập thông tin để truy cập API của Provider đó gồm có:
client-id
: ID của ứng dụng (ứng dụng bạn phải tạo ra trên Googe, Facebook ...) tương ứng với nhà cung cấp dịch vụclient-secret
: chuỗi mã bí mật để xác thực ứng dụng được truy cập API của nhà cung cấp
client-secret
và client-id
là các thông tin nhạy cảm, ASP.NET khuyên nên lưu thông tin này ở Secret Manager cho từng loại ứng dụng để tăng độ bảo mật. Ở ví dụ này tạm thời ta không dùng đến Secret mà sẽ lưu client-id, client-secret ở file config appsettings.json
Thư viện Identity cũng cung cấp sẵn trang ExternalLogin
với code mẫu nếu bạn sử dụng xác thực từ dịch vụ ngoài, tương tự ở các trang Register, Login ở bài trước - nếu dịch vụ ngoài kích hoạt thì cũng đã có sẵn code sử dụng nó.
Tiến trình sử lý chung khi sử dụng OAuth trong Identity
- 1) Cần truy cập vào nhà cung cấp dịch vụ (Google, Microsoft, Facebook ...) tạo ra một ứng dụng trên nền tảng đó. Từ đó có được Id của ứng dụng và mã sử dụng ứng dụng (ClientId,ClientSecret)
- 2) Khi cần xác thực (Login, Register) với thông tin từ dịch vụ ngoài, thì ứng dụng sẽ sử dụng (ClientId,ClientSecret) để kết nối đến Provider, Provider sẽ hỏi người dùng đồng ý kết nối. Provider sẽ chuyển lại thông tin để ứng dụng có thể truy cập được thông tin User (như Email, UserName, ....).
Các provider gửi lại thông tin trên một Url do ứng dụng thiết lập (gọi là CallbackPath), tại đây ứng dụng có được Token để đọc thông tin User
Các package trên: Microsoft.AspNetCore.Authentication.Facebook, Microsoft.AspNetCore.Authentication.Google ... nếu ứng dụng sử dụng nó, thì nó có sẵn trang CallbackPath này, ứng dụng không phải xây dựng, có thể chỉ cần tùy biến Url đến trang
Sau đây thực hành tích hợp xác thực từ Google, các provider khác như Facebook, Twitter, Microsoft ... làm tương tự
Đăng ký dịch vụ xác thực từ Google - OAuth của Google
Trước tiên bạn cần có tài khoản Google (gmail). Sau đó truy cập vào trang console.developers.google.com, nếu chưa có dự án mới bấm vào mũi tên đổ xuống trên menu (xem hình) để bắt đầu tạo dự án mới.
Các bước thực hành như các hình sau:
Chọn New Project
Điền tên dự án, ví dụ xuanthulabasp, sau đó bấm vào Create
Sau khi dự án tạo ra, chọn dự án và bấm vào OAuth consent screen, chọn Externals rồi bấm vào Create
Màn hình tiếp nhập tên ứng dụng, ví dụ xuannthulab (nhập các thông tin khác nếu bạn muốn nhập, như logo ...), sau đó lưu lại.
Chọn mục Credentials, rồi bấm vào Create Credentials, menu đổ xuống chọn OAuth client ID
Màn hình tiếp theo bạn điền các thông tin như - chọn Web Applicationn, đặt tên nhập vào ví dụ Web client 1
Riêng mục Authorized redirect Urls cần lưu ý như sau:
Đây là danh sách các Url mà sau khi google xác thực theo theo tài khoản người dùng, nó sẽ được chuyển hướng đến. Khi chuyển hướng - nó sẽ kèm thông tin để cho phép truy cập thông tin cơ bản của người dùng (tài khoản google) ví dụ lấy địa chỉ email.
Do đó, những Url nào được phép chuyển hướng hướng đến phải khai báo vào đây. Trong ứng dụng ASP.NET Core ví dụ của chúng ta, sau này sẽ cấu hình Url chuyển hướng từ google là https://localhost:5001/dang-nhap-tu-google
, nên sẽ điền Url này vào, sau đó bấm vào Create
Sau này bạn có thể quay lại cập nhật các Url này vào những trang chuyển hướng đến ứng dụng thực tế của bạn.
Cuối cùng bấm vào Credentials, chọn tên Client của bạn, ở đây là Web client1, bạn sẽ có thông tin để truy cập là: Client ID và Client secret, hãy bảo mật kỹ hai thông tin này - bạn copy lại và sẽ sử dụng nó trong ứng dụng ASP.NET ngay sau đây.
Đăng ký sử dụng OAuth của Google vào Identity ASP.NET
Mở file appsettings.js
của dự án ra, cho vào thông tin sau
{ // Các cấu hình khác "Authentication": { "Google": { "ClientId": "điền ClientID lấy được từ google", "ClientSecret": "điền ClientSecret" } } }
Tiếp theo trong ConfigureServices của Startup cập nhật đăng ký xác thực từ Google bằng đoạn code
services.AddAuthentication() .AddGoogle(googleOptions => { // Đọc thông tin Authentication:Google từ appsettings.json IConfigurationSection googleAuthNSection = Configuration.GetSection("Authentication:Google"); // Thiết lập ClientID và ClientSecret để truy cập API google googleOptions.ClientId = googleAuthNSection["ClientId"]; googleOptions.ClientSecret = googleAuthNSection["ClientSecret"]; // Cấu hình Url callback lại từ Google (không thiết lập thì mặc định là /signin-google) googleOptions.CallbackPath = "/dang-nhap-tu-google"; });
Nếu mọi cấu hình chính xác, thì Identity của bạn đã có thể đăng ký, đăng nhập từ dịch vụ google
Tùy biến trang ExternalLogin - xác thực từ dịch vụ ngoài
Khi đã đăng ký có sử dụng xác thực từ ngoài như, Gooogle services.AddAuthentication().AddGoogl()
, thì từ SignManager lấy được danh sách các Provider này bằng
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync ()).ToList ();
Đoạn Code trên có trong các trang như Register.cshtml.cs, Login.cshtml.cs ...
Từ danh sách này, các trang Register.cshtml hay Login.cshtml dựng nên HTML có các nút bấm để theo tên Provider đó, ví dụ trong - Login.cshtml và Register.cshtml sửa cột thứ 2 của giao diện thành
<div class="col-md-6 col-md-offset-2"> @if ((Model.ExternalLogins?.Count ?? 0) != 0) { <section> <h4>Sử dụng dịch vụ</h4> <hr /> <form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal"> <div> <p> @foreach (var provider in Model.ExternalLogins) { <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button> } </p> </div> </form> </section> } </div>
Khi bấm vào Google thì thi hành Post Form thông tin, như đoạn mã trên là sẽ Post đến trang ExternalLogin, với dữ liệu name="provider" value="@provider.Name"
Phương thức OnPost trong ExternalLogin.cshtml.cs định nghĩa như sau:
// Post yêu cầu login bằng dịch vụ ngoài // Provider = Google, Facebook ... public IActionResult OnPost(string provider, string returnUrl = null) { // Kiểm tra yêu cầu dịch vụ provider tồn tại var listprovider = (await _signInManager.GetExternalAuthenticationSchemesAsync ()).ToList (); var provider_process = listprovider.Find ((m) => m.Name == provider); if (provider_process == null) { return NotFound ("Dịch vụ không chính xác: " + provider); } // redirectUrl - là Url sẽ chuyển hướng đến - sau khi CallbackPath (/dang-nhap-tu-google) thi hành xong // nó bằng identity/account/externallogin?handler=Callback // tức là gọi OnGetCallbackAsync var redirectUrl = Url.Page ("./ExternalLogin", pageHandler: "Callback", values : new { returnUrl }); // Cấu hình var properties = _signInManager.ConfigureExternalAuthenticationProperties (provider, redirectUrl); // Chuyển hướng đến dịch vụ ngoài (Googe, Facebook) return new ChallengeResult (provider, properties); }
Phương thức này sẽ cấu hình API để truy vấn được đến dịch vụ ngoài và chuyển hướng đến trang Login tài khoản của dịch vụ đó (như Google, Facebook ...).
Sau khi dịch vụ ngoài xác thực xong, nó sẽ chuyển thông tin về CallbackPath (như /dang-nhap-tu-google), tại callback nó sẽ tạo được thông tin ExternalLoginInfo lưu trong Session, sau này có thể lấy lại thông tin này bằng
var info = await _signInManager.GetExternalLoginInfoAsync();
CallbackPath (như /dang-nhap-tu-google) thi hành xong sẽ chuyển hướng đến Url tương ứng gọi OnGetCallbackAsync
của trang ExternalLogin
Nội dung toàn bộ của ExternalLogin.cshtml.cs như sau
ExternalLogin.cshtml.cs
namespace Album.Areas.Identity.Pages.Account { [AllowAnonymous] public class ExternalLoginModel : PageModel { private readonly SignInManager<AppUser> _signInManager; private readonly UserManager<AppUser> _userManager; private readonly IEmailSender _emailSender; private readonly ILogger<ExternalLoginModel> _logger; public ExternalLoginModel ( SignInManager<AppUser> signInManager, UserManager<AppUser> userManager, ILogger<ExternalLoginModel> logger, IEmailSender emailSender) { _signInManager = signInManager; _userManager = userManager; _logger = logger; _emailSender = emailSender; } [BindProperty] public InputModel Input { get; set; } public string ProviderDisplayName { get; set; } public string ReturnUrl { get; set; } [TempData] public string ErrorMessage { get; set; } public class InputModel { [Required] [EmailAddress] [Display (Name = "Địa chỉ email")] public string Email { get; set; } } public IActionResult OnGetAsync () { return RedirectToPage ("./Login"); } // Post yêu cầu login bằng dịch vụ ngoài // Provider = Google, Facebook ... public async Task<IActionResult> OnPost (string provider, string returnUrl = null) { // Kiểm tra yêu cầu dịch vụ provider tồn tại var listprovider = (await _signInManager.GetExternalAuthenticationSchemesAsync ()).ToList (); var provider_process = listprovider.Find ((m) => m.Name == provider); if (provider_process == null) { return NotFound ("Dịch vụ không chính xác: " + provider); } // redirectUrl - là Url sẽ chuyển hướng đến - sau khi CallbackPath (/dang-nhap-tu-google) thi hành xong // nó bằng identity/account/externallogin?handler=Callback // tức là gọi OnGetCallbackAsync var redirectUrl = Url.Page ("./ExternalLogin", pageHandler: "Callback", values : new { returnUrl }); // Cấu hình var properties = _signInManager.ConfigureExternalAuthenticationProperties (provider, redirectUrl); // Chuyển hướng đến dịch vụ ngoài (Googe, Facebook) return new ChallengeResult (provider, properties); } public async Task<IActionResult> OnGetCallbackAsync (string returnUrl = null, string remoteError = null) { returnUrl = returnUrl ?? Url.Content ("~/"); if (remoteError != null) { ErrorMessage = $"Lỗi provider: {remoteError}"; return RedirectToPage ("./Login", new { ReturnUrl = returnUrl }); } // Lấy thông tin do dịch vụ ngoài chuyển đến var info = await _signInManager.GetExternalLoginInfoAsync (); if (info == null) { ErrorMessage = "Lỗi thông tin từ dịch vụ đăng nhập."; return RedirectToPage ("./Login", new { ReturnUrl = returnUrl }); } // Đăng nhập bằng thông tin LoginProvider, ProviderKey từ info cung cấp bởi dịch vụ ngoài // User nào có 2 thông tin này sẽ được đăng nhập - thông tin này lưu tại bảng UserLogins của Database // Trường LoginProvider và ProviderKey ---> tương ứng UserId var result = await _signInManager.ExternalLoginSignInAsync (info.LoginProvider, info.ProviderKey, isPersistent : false, bypassTwoFactor : true); if (result.Succeeded) { // User đăng nhập thành công vào hệ thống theo thông tin info _logger.LogInformation ("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider); return LocalRedirect (returnUrl); } if (result.IsLockedOut) { // Bị tạm khóa return RedirectToPage ("./Lockout"); } else { var userExisted = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey); if (userExisted != null) { // Đã có Acount, đã liên kết với tài khoản ngoài - nhưng không đăng nhập được // có thể do chưa kích hoạt email return RedirectToPage ("./RegisterConfirmation", new { Email = userExisted.Email }); } // Chưa có Account liên kết với tài khoản ngoài // Hiện thị form để thực hiện bước tiếp theo ở OnPostConfirmationAsync ReturnUrl = returnUrl; ProviderDisplayName = info.ProviderDisplayName; if (info.Principal.HasClaim (c => c.Type == ClaimTypes.Email)) { // Có thông tin về email từ info, lấy email này hiện thị ở Form Input = new InputModel { Email = info.Principal.FindFirstValue (ClaimTypes.Email) }; } return Page (); } } public async Task<IActionResult> OnPostConfirmationAsync (string returnUrl = null) { returnUrl = returnUrl ?? Url.Content ("~/"); // Lấy lại Info var info = await _signInManager.GetExternalLoginInfoAsync (); if (info == null) { ErrorMessage = "Không có thông tin tài khoản ngoài."; return RedirectToPage ("./Login", new { ReturnUrl = returnUrl }); } if (ModelState.IsValid) { string externalMail = null; if (info.Principal.HasClaim (c => c.Type == ClaimTypes.Email)) { externalMail = info.Principal.FindFirstValue (ClaimTypes.Email); } var userWithexternalMail = (externalMail != null) ? (await _userManager.FindByEmailAsync (externalMail)) : null; // Xử lý khi có thông tin về email từ info, đồng thời có user với email đó // trường hợp này sẽ thực hiện liên kết tài khoản ngoài + xác thực email luôn if ((userWithexternalMail != null) && (Input.Email == externalMail)) { // xác nhận email luôn nếu chưa xác nhận if (!userWithexternalMail.EmailConfirmed) { var codeactive = await _userManager.GenerateEmailConfirmationTokenAsync (userWithexternalMail); await _userManager.ConfirmEmailAsync (userWithexternalMail, codeactive); } // Thực hiện liên kết info và user var resultAdd = await _userManager.AddLoginAsync (userWithexternalMail, info); if (resultAdd.Succeeded) { // Thực hiện login await _signInManager.SignInAsync (userWithexternalMail, isPersistent : false); return ViewComponent (MessagePage.COMPONENTNAME, new MessagePage.Message () { title = "LIÊN KẾT TÀI KHOẢN", urlredirect = returnUrl, htmlcontent = $"Liên kết tài khoản {userWithexternalMail.UserName} với {info.ProviderDisplayName} thành công" }); } else { return ViewComponent (MessagePage.COMPONENTNAME, new MessagePage.Message () { title = "LIÊN KẾT TÀI KHOẢN", urlredirect = Url.Page ("Index"), htmlcontent = $"Liên kết thất bại" }); } } // Tài khoản chưa có, tạo tài khoản mới var user = new AppUser { UserName = Input.Email, Email = Input.Email }; var result = await _userManager.CreateAsync (user); if (result.Succeeded) { // Liên kết tài khoản ngoài với tài khoản vừa tạo result = await _userManager.AddLoginAsync (user, info); if (result.Succeeded) { _logger.LogInformation ("Đã tạo user mới từ thông tin {Name}.", info.LoginProvider); // Email tạo tài khoản và email từ info giống nhau -> xác thực email luôn if (user.Email == externalMail) { var codeactive = await _userManager.GenerateEmailConfirmationTokenAsync (user); await _userManager.ConfirmEmailAsync (user, codeactive); await _signInManager.SignInAsync (user, isPersistent : false, info.LoginProvider); return ViewComponent (MessagePage.COMPONENTNAME, new MessagePage.Message () { title = "TẠO VÀ LIÊN KẾT TÀI KHOẢN", urlredirect = returnUrl, htmlcontent = $"Đã tạo và liên kết tài khoản, kích hoạt email thành công" }); } // Trường hợp này Email tạo User khác với Email từ info (hoặc info không có email) // sẽ gửi email xác để người dùng xác thực rồi mới có thể đăng nhập var userId = await _userManager.GetUserIdAsync (user); var code = await _userManager.GenerateEmailConfirmationTokenAsync (user); code = WebEncoders.Base64UrlEncode (Encoding.UTF8.GetBytes (code)); var callbackUrl = Url.Page ( "/Account/ConfirmEmail", pageHandler : null, values : new { area = "Identity", userId = userId, code = code }, protocol : Request.Scheme); await _emailSender.SendEmailAsync (Input.Email, "Xác nhận địa chỉ email", $"Hãy xác nhận địa chỉ email bằng cách <a href='{callbackUrl}'>bấm vào đây</a>."); // Chuyển đến trang thông báo cần kích hoạt tài khoản if (_userManager.Options.SignIn.RequireConfirmedEmail) { return RedirectToPage ("./RegisterConfirmation", new { Email = Input.Email }); } // Đăng nhập ngay do không yêu cầu xác nhận email await _signInManager.SignInAsync (user, isPersistent : false, info.LoginProvider); return LocalRedirect (returnUrl); } } foreach (var error in result.Errors) { ModelState.AddModelError (string.Empty, error.Description); } } ProviderDisplayName = info.ProviderDisplayName; ReturnUrl = returnUrl; return Page (); } } }
ExternalLogin.cshtml
@page @model ExternalLoginModel @{ ViewData["Title"] = "ĐĂNG KÝ TÀI KHOẢN"; } <h1>@ViewData["Title"]</h1> <h4 id="external-login-title">Thực hiện liên kết với tài khoản @Model.ProviderDisplayName.</h4> <hr /> <p id="external-login-description" class="text-info"> Bạn đã xác thực với tài khoản <strong>@Model.ProviderDisplayName</strong>. Vui lòng nhập (hoặc xác nhận) chính xác email để liên kết và đăng nhập. </p> <div class="row"> <div class="col-md-4"> <form asp-page-handler="Confirmation" asp-route-returnUrl="@Model.ReturnUrl" method="post"> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.Email"></label> <input asp-for="Input.Email" class="form-control" /> <span asp-validation-for="Input.Email" class="text-danger"></span> </div> <button type="submit" class="btn btn-primary">Đăng ký</button> </form> </div> </div> @section Scripts { <partial name="_ValidationScriptsPartial" /> }
Một số phương thức đã dùng
-
var listprovider = (await _signInManager.GetExternalAuthenticationSchemesAsync ()).ToList ();
Lấy các dịch vụ xác thực ngoài -
ExternalLoginInfo info = await _signInManager.GetExternalLoginInfoAsync ();
Lấy thông tin đăng nhập từ tài khoản của phiên hiện tại (sau khi được chuyển hướng từ dịch vụ ngoài về ứng dụng) -
var result = await _signInManager.ExternalLoginSignInAsync (info.LoginProvider, info.ProviderKey, isPersistent : false, bypassTwoFactor : true);
Đăng nhập tài khoản bằng thông tin từ dịch vụ ngoài -
var user = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey);
Tìm User theo thông tin liên kết với dịch vụ ngoài -
var rs = await _userManager.AddLoginAsync (user, info);
Liên kết User với dịch vụ ngoài