Browse Source

Rename and fix the Passwordless-Authentication article.

pull/5011/head
Halil İbrahim Kalkan 6 years ago
parent
commit
14e209985d
  1. 555
      docs/en/Community-Articles/2020-08-07-Passwordless-Authentication/POST.md

555
docs/en/Community-Articles/2020-08-07-Passwordless-Authentication/passwordless-authentication.md → docs/en/Community-Articles/2020-08-07-Passwordless-Authentication/POST.md

@ -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…
Cancel
Save