mirror of https://github.com/abpframework/abp.git
1 changed files with 279 additions and 0 deletions
@ -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<TUser> : TotpSecurityStampBasedTokenProvider<TUser> |
||||
|
where TUser : class |
||||
|
{ |
||||
|
public override Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<TUser> manager, TUser user) |
||||
|
{ |
||||
|
return Task.FromResult(false); |
||||
|
} |
||||
|
|
||||
|
//We need to override this method as well. |
||||
|
public override async Task<string> GetUserModifierAsync(string purpose, UserManager<TUser> 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<IdentityBuilder>() |
||||
|
.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<IActionResult> 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<PasswordlessAuthenticationResource> 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; |
||||
|
} |
||||
|
|
||||
|
<abp-card> |
||||
|
<abp-card-body> |
||||
|
<form asp-page-handler="GeneratePasswordlessToken" method="post"> |
||||
|
|
||||
|
<abp-button button-type="Dark" type="submit">Generate passwordless token link</abp-button> |
||||
|
|
||||
|
@if (Model.PasswordlessLoginUrl != null) |
||||
|
{ |
||||
|
<abp-card class="mt-3 p-3"> |
||||
|
[@Model.PasswordlessLoginUrl](/en/commercial/latest/how-to/@Model.PasswordlessLoginUrl) |
||||
|
</abp-card> |
||||
|
} |
||||
|
|
||||
|
</form> |
||||
|
</abp-card-body> |
||||
|
</abp-card> |
||||
|
``` |
||||
|
## 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<IActionResult> 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<Claim> CreateClaims(IUser user, IEnumerable<string> roles) |
||||
|
{ |
||||
|
var claims = new List<Claim> |
||||
|
{ |
||||
|
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. |
||||
Loading…
Reference in new issue