mirror of https://github.com/abpframework/abp.git
1 changed files with 276 additions and 279 deletions
@ -1,279 +1,276 @@ |
|||||
# Implementing Passwordless Authentication in ASP.NET Core Identity |
# 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. |
## Introduction |
||||
In this tutorial, we will show you how to add a custom token provider to authenticate a user with a link. |
|
||||
|
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. |
||||
> 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. |
|
||||
|
### Source Code |
||||
## Creating the Solution |
|
||||
|
The completed sample is available on [the GitHub repository](https://github.com/abpframework/abp-samples/tree/master/PasswordlessAuthentication). |
||||
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). |
|
||||
|
## Creating the Solution |
||||
## Step-1 |
|
||||
|
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). |
||||
Create a class named **PasswordlessLoginProvider** in your ***.Web** project. |
|
||||
|
## Step-1 |
||||
**PasswordlessLoginProvider.cs** |
|
||||
|
Create a class named **PasswordlessLoginProvider** in your ***.Web** project: |
||||
```csharp |
|
||||
using System.Threading.Tasks; |
```csharp |
||||
using Microsoft.AspNetCore.Identity; |
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
namespace PasswordlessAuthentication.Web |
|
||||
{ |
namespace PasswordlessAuthentication.Web |
||||
public class PasswordlessLoginProvider<TUser> : TotpSecurityStampBasedTokenProvider<TUser> |
{ |
||||
where TUser : class |
public class PasswordlessLoginProvider<TUser> : TotpSecurityStampBasedTokenProvider<TUser> |
||||
{ |
where TUser : class |
||||
public override Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<TUser> manager, TUser user) |
{ |
||||
{ |
public override Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<TUser> manager, TUser user) |
||||
return Task.FromResult(false); |
{ |
||||
} |
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) |
//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); |
{ |
||||
|
var userId = await manager.GetUserIdAsync(user); |
||||
return "PasswordlessLogin:" + purpose + ":" + userId; |
|
||||
} |
return "PasswordlessLogin:" + purpose + ":" + userId; |
||||
} |
} |
||||
} |
} |
||||
``` |
} |
||||
## Step-2 |
``` |
||||
|
## Step-2 |
||||
Create **IdentityBuilderExtensions.cs** in your ***.Web** project. |
|
||||
|
Create **IdentityBuilderExtensions.cs** in your ***.Web** project. We will use this extension method in the `ConfigureServices`. |
||||
We will use this extension method in `ConfigureServices`. |
|
||||
|
```csharp |
||||
```csharp |
using Microsoft.AspNetCore.Identity; |
||||
using Microsoft.AspNetCore.Identity; |
|
||||
|
namespace PasswordlessAuthentication.Web |
||||
namespace PasswordlessAuthentication.Web |
{ |
||||
{ |
public static class IdentityBuilderExtensions |
||||
public static class IdentityBuilderExtensions |
{ |
||||
{ |
public static IdentityBuilder AddPasswordlessLoginProvider(this IdentityBuilder builder) |
||||
public static IdentityBuilder AddPasswordlessLoginProvider(this IdentityBuilder builder) |
{ |
||||
{ |
var userType = builder.UserType; |
||||
var userType = builder.UserType; |
var totpProvider = typeof(PasswordlessLoginProvider<>).MakeGenericType(userType); |
||||
var totpProvider = typeof(PasswordlessLoginProvider<>).MakeGenericType(userType); |
return builder.AddTokenProvider("PasswordlessLoginProvider", totpProvider); |
||||
return builder.AddTokenProvider("PasswordlessLoginProvider", totpProvider); |
} |
||||
} |
} |
||||
} |
} |
||||
} |
``` |
||||
``` |
## Step-3 |
||||
## 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. |
||||
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 |
||||
```csharp |
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
public override void ConfigureServices(ServiceConfigurationContext context) |
{ |
||||
{ |
//... |
||||
//... |
context.Services |
||||
context.Services |
.GetObject<IdentityBuilder>() |
||||
.GetObject<IdentityBuilder>() |
.AddDefaultTokenProviders() |
||||
.AddDefaultTokenProviders() |
.AddPasswordlessLoginProvider(); |
||||
.AddPasswordlessLoginProvider(); |
} |
||||
} |
``` |
||||
``` |
|
||||
|
## Step-4 |
||||
## 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. |
||||
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** |
||||
**Index.cshtml.cs** |
|
||||
|
```csharp |
||||
```csharp |
using System.Threading.Tasks; |
||||
using System.Threading.Tasks; |
using Microsoft.AspNetCore.Mvc; |
||||
using Microsoft.AspNetCore.Mvc; |
using Volo.Abp.Identity; |
||||
using Volo.Abp.Identity; |
|
||||
|
namespace PasswordlessAuthentication.Web.Pages |
||||
namespace PasswordlessAuthentication.Web.Pages |
{ |
||||
{ |
public class IndexModel : PasswordlessAuthenticationPageModel |
||||
public class IndexModel : PasswordlessAuthenticationPageModel |
{ |
||||
{ |
protected IdentityUserManager UserManager { get; } |
||||
protected IdentityUserManager UserManager { get; } |
|
||||
|
private readonly IIdentityUserRepository _userRepository; |
||||
private readonly IIdentityUserRepository _userRepository; |
|
||||
|
public string PasswordlessLoginUrl { get; set; } |
||||
public string PasswordlessLoginUrl { get; set; } |
|
||||
|
public string Email { get; set; } |
||||
public string Email { get; set; } |
|
||||
|
public IndexModel(IdentityUserManager userManager, IIdentityUserRepository userRepository) |
||||
public IndexModel(IdentityUserManager userManager, IIdentityUserRepository userRepository) |
{ |
||||
{ |
UserManager = userManager; |
||||
UserManager = userManager; |
_userRepository = userRepository; |
||||
_userRepository = userRepository; |
} |
||||
} |
|
||||
|
public ActionResult OnGet() |
||||
public ActionResult OnGet() |
{ |
||||
{ |
if (!CurrentUser.IsAuthenticated) |
||||
if (!CurrentUser.IsAuthenticated) |
{ |
||||
{ |
return Redirect("/Account/Login"); |
||||
return Redirect("/Account/Login"); |
} |
||||
} |
|
||||
|
return Page(); |
||||
return Page(); |
} |
||||
} |
|
||||
|
//added for passwordless authentication |
||||
//added for passwordless authentication |
public async Task<IActionResult> OnPostGeneratePasswordlessTokenAsync() |
||||
public async Task<IActionResult> OnPostGeneratePasswordlessTokenAsync() |
{ |
||||
{ |
var adminUser = await _userRepository.FindByNormalizedUserNameAsync("admin"); |
||||
var adminUser = await _userRepository.FindByNormalizedUserNameAsync("admin"); |
|
||||
|
var token = await UserManager.GenerateUserTokenAsync(adminUser, "PasswordlessLoginProvider", |
||||
var token = await UserManager.GenerateUserTokenAsync(adminUser, "PasswordlessLoginProvider", |
"passwordless-auth"); |
||||
"passwordless-auth"); |
|
||||
|
PasswordlessLoginUrl = Url.Action("Login", "Passwordless", |
||||
PasswordlessLoginUrl = Url.Action("Login", "Passwordless", |
new {token = token, userId = adminUser.Id.ToString()}, Request.Scheme); |
||||
new {token = token, userId = adminUser.Id.ToString()}, Request.Scheme); |
|
||||
|
return Page(); |
||||
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. |
||||
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 |
||||
## Step-5 |
|
||||
|
Create a class named **PasswordlessAuthenticationMenus** under `Menus` folder in your ***.Web** project. And set the content as below. |
||||
Create a class named **PasswordlessAuthenticationMenus** under `Menus` folder in your ***.Web** project. And set the content as below. |
|
||||
|
```csharp |
||||
```csharp |
namespace PasswordlessAuthentication.Web.Menus |
||||
namespace PasswordlessAuthentication.Web.Menus |
{ |
||||
{ |
public class PasswordlessAuthenticationMenus |
||||
public class PasswordlessAuthenticationMenus |
{ |
||||
{ |
public const string GroupName = "PasswordlessAuthentication"; |
||||
public const string GroupName = "PasswordlessAuthentication"; |
|
||||
|
public const string Home = GroupName + ".Home"; |
||||
public const string Home = GroupName + ".Home"; |
} |
||||
} |
} |
||||
} |
``` |
||||
``` |
|
||||
|
## Step-6 |
||||
## 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. |
||||
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 |
||||
```html |
@page |
||||
@page |
@inject IHtmlLocalizer<PasswordlessAuthenticationResource> L |
||||
@inject IHtmlLocalizer<PasswordlessAuthenticationResource> L |
@using Microsoft.AspNetCore.Mvc.Localization |
||||
@using Microsoft.AspNetCore.Mvc.Localization |
@using PasswordlessAuthentication.Localization |
||||
@using PasswordlessAuthentication.Localization |
@using PasswordlessAuthentication.Web.Menus |
||||
@using PasswordlessAuthentication.Web.Menus |
@using Volo.Abp.AspNetCore.Mvc.UI.Layout |
||||
@using Volo.Abp.AspNetCore.Mvc.UI.Layout |
@model PasswordlessAuthentication.Web.Pages.IndexModel |
||||
@model PasswordlessAuthentication.Web.Pages.IndexModel |
|
||||
|
@{ |
||||
@{ |
ViewBag.PageTitle = "Home"; |
||||
ViewBag.PageTitle = "Home"; |
} |
||||
} |
@inject IPageLayout PageLayout; |
||||
@inject IPageLayout PageLayout; |
@{ |
||||
@{ |
PageLayout.Content.Title = L["Home"].Value; |
||||
PageLayout.Content.Title = L["Home"].Value; |
PageLayout.Content.BreadCrumb.Add(L["Menu:Home"].Value); |
||||
PageLayout.Content.BreadCrumb.Add(L["Menu:Home"].Value); |
PageLayout.Content.MenuItemName = PasswordlessAuthenticationMenus.Home; |
||||
PageLayout.Content.MenuItemName = PasswordlessAuthenticationMenus.Home; |
} |
||||
} |
|
||||
|
<abp-card> |
||||
<abp-card> |
<abp-card-body> |
||||
<abp-card-body> |
<form asp-page-handler="GeneratePasswordlessToken" method="post"> |
||||
<form asp-page-handler="GeneratePasswordlessToken" method="post"> |
|
||||
|
<abp-button button-type="Dark" type="submit">Generate passwordless token link</abp-button> |
||||
<abp-button button-type="Dark" type="submit">Generate passwordless token link</abp-button> |
|
||||
|
@if (Model.PasswordlessLoginUrl != null) |
||||
@if (Model.PasswordlessLoginUrl != null) |
{ |
||||
{ |
<abp-card class="mt-3 p-3"> |
||||
<abp-card class="mt-3 p-3"> |
[@Model.PasswordlessLoginUrl](/en/commercial/latest/how-to/@Model.PasswordlessLoginUrl) |
||||
[@Model.PasswordlessLoginUrl](/en/commercial/latest/how-to/@Model.PasswordlessLoginUrl) |
</abp-card> |
||||
</abp-card> |
} |
||||
} |
|
||||
|
</form> |
||||
</form> |
</abp-card-body> |
||||
</abp-card-body> |
</abp-card> |
||||
</abp-card> |
``` |
||||
``` |
## Step-7 |
||||
## 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: |
||||
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. |
|
||||
|
```csharp |
||||
**PasswordlessController.cs** |
using System; |
||||
|
using System.Collections.Generic; |
||||
```csharp |
using System.Security.Claims; |
||||
using System; |
using System.Threading.Tasks; |
||||
using System.Collections.Generic; |
using Microsoft.AspNetCore.Authentication; |
||||
using System.Security.Claims; |
using Microsoft.AspNetCore.Identity; |
||||
using System.Threading.Tasks; |
using Microsoft.AspNetCore.Mvc; |
||||
using Microsoft.AspNetCore.Authentication; |
using Volo.Abp.AspNetCore.Mvc; |
||||
using Microsoft.AspNetCore.Identity; |
using Volo.Abp.Identity; |
||||
using Microsoft.AspNetCore.Mvc; |
using Volo.Abp.Security.Claims; |
||||
using Volo.Abp.AspNetCore.Mvc; |
using Volo.Abp.Users; |
||||
using Volo.Abp.Identity; |
|
||||
using Volo.Abp.Security.Claims; |
namespace PasswordlessAuthentication.Web.Controllers |
||||
using Volo.Abp.Users; |
{ |
||||
|
public class PasswordlessController : AbpController |
||||
namespace PasswordlessAuthentication.Web.Controllers |
{ |
||||
{ |
protected IdentityUserManager UserManager { get; } |
||||
public class PasswordlessController : AbpController |
|
||||
{ |
public PasswordlessController(IdentityUserManager userManager) |
||||
protected IdentityUserManager UserManager { get; } |
{ |
||||
|
UserManager = userManager; |
||||
public PasswordlessController(IdentityUserManager userManager) |
} |
||||
{ |
|
||||
UserManager = userManager; |
public virtual async Task<IActionResult> Login(string token, string userId) |
||||
} |
{ |
||||
|
var user = await UserManager.FindByIdAsync(userId); |
||||
public virtual async Task<IActionResult> Login(string token, string userId) |
|
||||
{ |
var isValid = await UserManager.VerifyUserTokenAsync(user, "PasswordlessLoginProvider", "passwordless-auth", token); |
||||
var user = await UserManager.FindByIdAsync(userId); |
if (!isValid) |
||||
|
{ |
||||
var isValid = await UserManager.VerifyUserTokenAsync(user, "PasswordlessLoginProvider", "passwordless-auth", token); |
throw new UnauthorizedAccessException("The token " + token + " is not valid for the user " + userId); |
||||
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); |
||||
await UserManager.UpdateSecurityStampAsync(user); |
|
||||
|
var principal = new ClaimsPrincipal( |
||||
var roles = await UserManager.GetRolesAsync(user); |
new ClaimsIdentity(CreateClaims(user, roles), IdentityConstants.ApplicationScheme) |
||||
|
); |
||||
var principal = new ClaimsPrincipal( |
|
||||
new ClaimsIdentity(CreateClaims(user, roles), IdentityConstants.ApplicationScheme) |
await HttpContext.SignInAsync(IdentityConstants.ApplicationScheme, principal); |
||||
); |
|
||||
|
return Redirect("/"); |
||||
await HttpContext.SignInAsync(IdentityConstants.ApplicationScheme, principal); |
} |
||||
|
|
||||
return Redirect("/"); |
private static IEnumerable<Claim> CreateClaims(IUser user, IEnumerable<string> roles) |
||||
} |
{ |
||||
|
var claims = new List<Claim> |
||||
private static IEnumerable<Claim> CreateClaims(IUser user, IEnumerable<string> roles) |
{ |
||||
{ |
new Claim("sub", user.Id.ToString()), |
||||
var claims = new List<Claim> |
new Claim(AbpClaimTypes.UserId, user.Id.ToString()), |
||||
{ |
new Claim(AbpClaimTypes.Email, user.Email), |
||||
new Claim("sub", user.Id.ToString()), |
new Claim(AbpClaimTypes.UserName, user.UserName), |
||||
new Claim(AbpClaimTypes.UserId, user.Id.ToString()), |
new Claim(AbpClaimTypes.EmailVerified, user.EmailConfirmed.ToString().ToLower()), |
||||
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)); |
||||
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)); |
||||
foreach (var role in roles) |
} |
||||
{ |
|
||||
claims.Add(new Claim(AbpClaimTypes.Role, role)); |
return claims; |
||||
} |
} |
||||
|
} |
||||
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. |
||||
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. |
|
||||
|
## Source Code |
||||
That's all! We created a passwordless login with 7 steps. |
|
||||
|
The completed sample is available on [the GitHub repository](https://github.com/abpframework/abp-samples/tree/master/PasswordlessAuthentication). |
||||
|
|
||||
|
|
||||
> 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