Skip to content

Instantly share code, notes, and snippets.

@ebicoglu
Last active October 23, 2025 21:20
Show Gist options
  • Save ebicoglu/04cedc99d0365f4d20a6233cca69cf5b to your computer and use it in GitHub Desktop.
Save ebicoglu/04cedc99d0365f4d20a6233cca69cf5b to your computer and use it in GitHub Desktop.

Revisions

  1. ebicoglu revised this gist Mar 31, 2020. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions passwordless-token-provider.md
    Original file line number Diff line number Diff line change
    @@ -215,3 +215,4 @@ We created an endpoint for `/Passwordless/Login` that gets the token and the use

    That's all! We created a passwordless login with 6 steps.

    You can also create the links with a lifetime so that it'll not be active forever. To do this check out [this page](https://andrewlock.net/implementing-custom-token-providers-for-passwordless-authentication-in-asp-net-core-identity/).
  2. ebicoglu renamed this gist Mar 31, 2020. 1 changed file with 0 additions and 0 deletions.
  3. ebicoglu created this gist Mar 31, 2020.
    217 changes: 217 additions & 0 deletions passwordless-token-provider
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,217 @@
    # 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.
    I'll show you how to add a custom token provider to authenticate a user with a link.

    ## Step-1

    Create a class named **PasswordlessLoginProvider** in your ***.Web** project.

    **PasswordlessLoginProvider.cs**

    ```csharp
    public class PasswordlessLoginProvider<TUser> : TotpSecurityStampBasedTokenProvider<TUser>
    where TUser : class
    {
    public override Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<TUser> manager, TUser user)
    {
    return Task.FromResult(false);
    }

    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
    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: `YourProjectWebModule.cs`) 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
    public class IndexModel : MyBookStorePageModel
    {
    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

    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
    @inherits MyBookStore.Web.Pages.MyBookStorePage
    @using MyBookStore.Web.Menus
    @using Volo.Abp.AspNetCore.Mvc.UI.Layout
    @model MyBookStore.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 = MyBookStoreMenus.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">
    <a href="@Model.PasswordlessLoginUrl">@Model.PasswordlessLoginUrl</a>
    </abp-card>
    }
    </form>
    </abp-card-body>
    </abp-card>
    ```

    ## Step-6

    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
    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 validating 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 6 steps.