From 14e209985dcfd63b5830a1aeeafc5a77c37bf794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Sat, 8 Aug 2020 13:57:47 +0300 Subject: [PATCH] Rename and fix the Passwordless-Authentication article. --- ...passwordless-authentication.md => POST.md} | 555 +++++++++--------- 1 file changed, 276 insertions(+), 279 deletions(-) rename docs/en/Community-Articles/2020-08-07-Passwordless-Authentication/{passwordless-authentication.md => POST.md} (88%) 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/POST.md similarity index 88% rename from docs/en/Community-Articles/2020-08-07-Passwordless-Authentication/passwordless-authentication.md rename to docs/en/Community-Articles/2020-08-07-Passwordless-Authentication/POST.md index 1f8be3a7f4..1b7cef0867 100644 --- a/docs/en/Community-Articles/2020-08-07-Passwordless-Authentication/passwordless-authentication.md +++ b/docs/en/Community-Articles/2020-08-07-Passwordless-Authentication/POST.md @@ -1,279 +1,276 @@ -# 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 +# Implementing Passwordless Authentication in ASP.NET Core Identity + +## Introduction + +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, instead of entering a password. + +### Source Code + +The completed sample is available on [the GitHub repository](https://github.com/abpframework/abp-samples/tree/master/PasswordlessAuthentication). + +## 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/abp/latest/Getting-Started?UI=MVC&DB=EF&Tiered=No). + +## Step-1 + +Create a class named **PasswordlessLoginProvider** in your ***.Web** project: + +```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 the `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 create a controller, named **PasswordlessController** inside it: + +```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. + +## Source Code + +The completed sample is available on [the GitHub repository](https://github.com/abpframework/abp-samples/tree/master/PasswordlessAuthentication). \ No newline at end of file