diff --git a/docs/en/Community-Articles/2020-08-07-Passwordless-Authentication/passwordless-authentication.md b/docs/en/Community-Articles/2020-08-07-Passwordless-Authentication/passwordless-authentication.md new file mode 100644 index 0000000000..1f8be3a7f4 --- /dev/null +++ b/docs/en/Community-Articles/2020-08-07-Passwordless-Authentication/passwordless-authentication.md @@ -0,0 +1,279 @@ +# Implementing Passwordless Authentication in ASP.NET Core Identity + +To allow a user login with a magic URL, you need to implement a custom token provider. +In this tutorial, we will show you how to add a custom token provider to authenticate a user with a link. + +> The completed sample is available: [passwordless-authentication.zip](https://abp.io/Account/Login?returnUrl=/api/download/samples/passwordless-authentication). To be able to download this sample, you need to hold an active ABP Commercial license. + +## Creating the Solution + +Before starting to the development, create a new solution named `PasswordlessAuthentication` and run it by following the [getting started tutorial](https://docs.abp.io/en/commercial/latest/getting-started?UI=MVC&DB=EF&Tiered=No). + +## Step-1 + +Create a class named **PasswordlessLoginProvider** in your ***.Web** project. + +**PasswordlessLoginProvider.cs** + +```csharp +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; + +namespace PasswordlessAuthentication.Web +{ + public class PasswordlessLoginProvider : TotpSecurityStampBasedTokenProvider + where TUser : class + { + public override Task CanGenerateTwoFactorTokenAsync(UserManager manager, TUser user) + { + return Task.FromResult(false); + } + + //We need to override this method as well. + public override async Task GetUserModifierAsync(string purpose, UserManager manager, TUser user) + { + var userId = await manager.GetUserIdAsync(user); + + return "PasswordlessLogin:" + purpose + ":" + userId; + } + } +} +``` +## Step-2 + +Create **IdentityBuilderExtensions.cs** in your ***.Web** project. + +We will use this extension method in `ConfigureServices`. + +```csharp +using Microsoft.AspNetCore.Identity; + +namespace PasswordlessAuthentication.Web +{ + public static class IdentityBuilderExtensions + { + public static IdentityBuilder AddPasswordlessLoginProvider(this IdentityBuilder builder) + { + var userType = builder.UserType; + var totpProvider = typeof(PasswordlessLoginProvider<>).MakeGenericType(userType); + return builder.AddTokenProvider("PasswordlessLoginProvider", totpProvider); + } + } +} +``` +## Step-3 + +Add the token provider to the `Identity` middleware. To do this, find the module class (eg: `PasswordlessAuthenticationWebModule.cs` in here) in your ***.Web** project and add the below into the `ConfigureServices()` method. + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + //... + context.Services + .GetObject() + .AddDefaultTokenProviders() + .AddPasswordlessLoginProvider(); +} +``` + +## Step-4 + +We need to create a user interface to be able to generate the magic login link. To do this quickly, open your existing **Index.cshtml.cs** in your ***.Web** project. It's under `Pages` folder. And copy-paste the below content. + +**Index.cshtml.cs** + +```csharp +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.Identity; + +namespace PasswordlessAuthentication.Web.Pages +{ + public class IndexModel : PasswordlessAuthenticationPageModel + { + protected IdentityUserManager UserManager { get; } + + private readonly IIdentityUserRepository _userRepository; + + public string PasswordlessLoginUrl { get; set; } + + public string Email { get; set; } + + public IndexModel(IdentityUserManager userManager, IIdentityUserRepository userRepository) + { + UserManager = userManager; + _userRepository = userRepository; + } + + public ActionResult OnGet() + { + if (!CurrentUser.IsAuthenticated) + { + return Redirect("/Account/Login"); + } + + return Page(); + } + + //added for passwordless authentication + public async Task OnPostGeneratePasswordlessTokenAsync() + { + var adminUser = await _userRepository.FindByNormalizedUserNameAsync("admin"); + + var token = await UserManager.GenerateUserTokenAsync(adminUser, "PasswordlessLoginProvider", + "passwordless-auth"); + + PasswordlessLoginUrl = Url.Action("Login", "Passwordless", + new {token = token, userId = adminUser.Id.ToString()}, Request.Scheme); + + return Page(); + } + } +} +``` + +We added `OnPostGeneratePasswordlessTokenAsync()` action to generate the link. We will generate a link for the **admin** user. Therefore, we injected `IIdentityUserRepository` to get admin user Id. Using the `UserManager.GenerateUserTokenAsync()` method, we generated a token. After that, we created the URL with the admin user Id and the token. Now we will show the `PasswordlessLoginUrl` on the page. + +## Step-5 + +Create a class named **PasswordlessAuthenticationMenus** under `Menus` folder in your ***.Web** project. And set the content as below. + +```csharp +namespace PasswordlessAuthentication.Web.Menus +{ + public class PasswordlessAuthenticationMenus + { + public const string GroupName = "PasswordlessAuthentication"; + + public const string Home = GroupName + ".Home"; + } +} +``` + +## Step-6 + +Open your **Index.cshtml** and set the content as below. We added a form that posts to `GeneratePasswordlessToken` action in the razor page. And it will set the `PasswordlessLoginUrl` field. + +```html +@page +@inject IHtmlLocalizer L +@using Microsoft.AspNetCore.Mvc.Localization +@using PasswordlessAuthentication.Localization +@using PasswordlessAuthentication.Web.Menus +@using Volo.Abp.AspNetCore.Mvc.UI.Layout +@model PasswordlessAuthentication.Web.Pages.IndexModel + +@{ + ViewBag.PageTitle = "Home"; +} +@inject IPageLayout PageLayout; +@{ + PageLayout.Content.Title = L["Home"].Value; + PageLayout.Content.BreadCrumb.Add(L["Menu:Home"].Value); + PageLayout.Content.MenuItemName = PasswordlessAuthenticationMenus.Home; +} + + + +
+ + Generate passwordless token link + + @if (Model.PasswordlessLoginUrl != null) + { + + [@Model.PasswordlessLoginUrl](/en/commercial/latest/how-to/@Model.PasswordlessLoginUrl) + + } + +
+
+
+``` +## Step-7 + +We implemented token generation infrastructure, now it's time validate the token and let the user in. To do this create a folder named `Controllers` in your ***.Web** project and add the below controller. + +**PasswordlessController.cs** + +```csharp +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.Identity; +using Volo.Abp.Security.Claims; +using Volo.Abp.Users; + +namespace PasswordlessAuthentication.Web.Controllers +{ + public class PasswordlessController : AbpController + { + protected IdentityUserManager UserManager { get; } + + public PasswordlessController(IdentityUserManager userManager) + { + UserManager = userManager; + } + + public virtual async Task Login(string token, string userId) + { + var user = await UserManager.FindByIdAsync(userId); + + var isValid = await UserManager.VerifyUserTokenAsync(user, "PasswordlessLoginProvider", "passwordless-auth", token); + if (!isValid) + { + throw new UnauthorizedAccessException("The token " + token + " is not valid for the user " + userId); + } + + await UserManager.UpdateSecurityStampAsync(user); + + var roles = await UserManager.GetRolesAsync(user); + + var principal = new ClaimsPrincipal( + new ClaimsIdentity(CreateClaims(user, roles), IdentityConstants.ApplicationScheme) + ); + + await HttpContext.SignInAsync(IdentityConstants.ApplicationScheme, principal); + + return Redirect("/"); + } + + private static IEnumerable CreateClaims(IUser user, IEnumerable roles) + { + var claims = new List + { + new Claim("sub", user.Id.ToString()), + new Claim(AbpClaimTypes.UserId, user.Id.ToString()), + new Claim(AbpClaimTypes.Email, user.Email), + new Claim(AbpClaimTypes.UserName, user.UserName), + new Claim(AbpClaimTypes.EmailVerified, user.EmailConfirmed.ToString().ToLower()), + }; + + if (!string.IsNullOrWhiteSpace(user.PhoneNumber)) + { + claims.Add(new Claim(AbpClaimTypes.PhoneNumber, user.PhoneNumber)); + } + + foreach (var role in roles) + { + claims.Add(new Claim(AbpClaimTypes.Role, role)); + } + + return claims; + } + } +} +``` + +We created an endpoint for `/Passwordless/Login` that gets the token and the user Id. In this action, we find the user via repository and validate the token via `UserManager.VerifyUserTokenAsync()` method. If it's valid, we create claims of the user then call `HttpContext.SignInAsync` to be able to create an encrypted cookie and add it to the current response. Finally we redirect the page to the root URL. + +That's all! We created a passwordless login with 7 steps. + + + +> The completed sample is available: [passwordless-authentication.zip](https://abp.io/Account/Login?returnUrl=/api/download/samples/passwordless-authentication). To be able to download this sample, you need to hold an active ABP Commercial license. \ No newline at end of file