There are tons of new apps launching every day, so you'll want to make yours stand out. It should have unique features, and it should be easy and convenient to use.

One of the major pain points for many apps is that they require a username and a password to login. I personally have to remember 10-15 passwords for apps like Gmail, Facebook, Instagram, and more. You get the idea.

In this article we are going to create a solution for your APIs that will allow your users to login without a password.

How to go password-less

In order to omit the need for a password, your app should generate some type of token for the user.

This token then gets sent to the user where only they can access it – for example in their email or via their phone. Here is an overview of the flow.

No Password Login Flow

.NET Identity is a package which provides ways to manage users, passwords, profile data, roles, claims, tokens, and more.

In addition, Identity provides ways to generate tokens for email confirmation or for changing the user's email or phone. We will be using the tokens generated by Identity to verify our users.

There are two main token providers available:

  • TotpSecurityStampBasedTokenProvider (Time-based One Time Password).
  • DataProtectionTokenProvider

TotpSecurityStampBasedTokenProvider

This token provider generates time-based tokens which are valid for around 3 minutes (you can reference the source code here). Based on the token provider, the tokens are generated from the email, phone number, or user id as well as the user's security stamp.

Dotnet Identity provides the utility classes EmailTokenProvider and PhoneNumberTokenProvider that are subclasses of TotpSecurityStampBasedTokenProvider.

DataProtectorTokenProvider

If you want to generate a token that doesn't expire for a long time, DataProtectorTokenProvider is the way to go.

DataProtectorTokenProvider generates tokens using a DataProtector and cryptographic algorithms. You can check out the implementation for more details here.

In this article we are going to subclass DataProtectorTokenProvider so that our token is valid for 10 minutes.

How to Set Up Identity

Let's start with a new project. Create a project by executing the command dotnet new webapi –-name NoPasswordProject.

dotnet add package Microsoft.EntityFrameworkCore.InMemory --version 5.0.4
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore --version 5.0.4

We are going to create an in-memory database for this tutorial. But you can use a database of your choice and accordingly change the package above.

Note: The in memory database will clear the users every time the server restarts.

How to Create a Custom Token Provider

Let's create a custom token provider that generates tokens that are valid for 10 minutes.

NPTokenProvider

Create a new file called NPTokenProvider.cs. The NP prefix stands for No Password.

using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

public class NPTokenProvider<TUser> : DataProtectorTokenProvider<TUser>
where TUser : IdentityUser
{
    public NPTokenProvider(
        IDataProtectionProvider dataProtectionProvider,
        IOptions<NPTokenProviderOptions> options, ILogger<NPTokenProvider<TUser>> logger)
        : base(dataProtectionProvider, options, logger)
    { }
}

Here we're subclassing the DataProtectorTokenProvider. Nothing out of the ordinary, except in the constructor we are passing NPTokenProviderOptions. The options need to be subclasses of DataProtectionTokenProviderOptions.

NPTokenProviderOptions

Create a new file called NPTokenProviderOptions.cs and paste in the below code.

using System;
using Microsoft.AspNetCore.Identity;

public class NPTokenProviderOptions : DataProtectionTokenProviderOptions
{
    public NPTokenProviderOptions()
    {
        Name = "NPTokenProvider";
        TokenLifespan = TimeSpan.FromMinutes(10);
    }
}

We are setting options for the tokens to be created. You can change the Name and TokenLifeSpan to your liking.

DbContext

Almost every project needs a database to store its users and other data related to the project. The Dotnet EF Framework provides a nice helper DbContext to handle sessions with the database and query and save entities.

So create a subclass of IdentityDbContext which is in turn a subclass of DbContext. Name the file NPDataContext.cs.

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

public class NPDataContext : IdentityDbContext
{
    public NPDataContext(DbContextOptions<NPDataContext> options)
        : base(options)
    { }
}

Startup.cs

We have created the classes. Now it's time to configure them in our Startup.cs files. In ConfigureServices add the below code at the start.

var builder = services
.AddIdentityCore<IdentityUser>()
.AddEntityFrameworkStores<NPDataContext>();

var UserType = builder.UserType;
var provider = typeof(NPTokenProvider<>).MakeGenericType(UserType);
builder.AddTokenProvider("NPTokenProvider", provider);

services.AddDbContext<NPDataContext>(options =>
    options.UseInMemoryDatabase(Guid.NewGuid().ToString()));

services.AddAuthentication(options =>
{
    options.DefaultScheme = IdentityConstants.ExternalScheme;
});

Also add app.UseAuthentication(); above app.UseAuthorization(); in the Configure method.

NoPasswordController.cs

Let's create a controller for our login and verify the APIs. Create a NoPasswordController.cs file in your Controllers folder. Add the below content to the file.

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;

namespace NoPasswordProject.Controllers
{
    [ApiController]
    [Route("[controller]/[action]")]
    public class NoPasswordController : ControllerBase
    {
        private readonly UserManager<IdentityUser> _userManager;

        public NoPasswordController(UserManager<IdentityUser> userManager)
        {
            _userManager = userManager;
        }
    }
}

We are injecting an instance of UserManager in our controller. UserManager is used for CRUD operations for a user as well as generating tokens and validating them.

Login API

Let's add a Login API which accepts an Email as input. The Email is the unique identifier for a user, that is there should be a one-to-one relationship between user and email.

Create a new function in your controller as below.

[HttpGet]
public async Task<ActionResult<String>> Login([FromQuery] string Email)
{
    // Create or Fetch your user from the database
    var User = await _userManager.FindByNameAsync(Email);
    if (User == null)
    {
        User = new IdentityUser();
        User.Email = Email;
        User.UserName = Email;
        var IdentityResult = await _userManager.CreateAsync(User);
        if (IdentityResult.Succeeded == false)
        {
            return BadRequest();
        }
    }

    var Token = await _userManager.GenerateUserTokenAsync(User, "NPTokenProvider", "nopassword-for-the-win");

    // DON'T RETURN THE TOKEN.
    // SEND IT TO THE USER VIA EMAIL.
    return NoContent();
}

Here we are fetching a User from the database. If the user doesn't exist then we create a user. Make sure to set the UserName as well or it will give a runtime error.

Then based on the user, we generate a UserToken. The GenerateUserTokenAsync takes the user, token provider, and the purpose for generating a token.

The token provider string should be the one you have used in NPTokenProviderOptions. The purpose can be anything you want.

Send out the token to the user via a link in a nicely designed email. When the user clicks on the link in the email it will open your front-end page. Consequently this page will request the Verify API.

Verify API

Let's add another API called Verify that takes the Email and Token as query parameters.

[HttpGet]
public async Task<ActionResult<String>> Verify([FromQuery] string Token, [FromQuery] string Email)
{
    // Fetch your user from the database
    var User = await _userManager.FindByNameAsync(Email);
    if (User == null)
    {
        return NotFound();
    }

    var IsValid = await _userManager.VerifyUserTokenAsync(User, "NPTokenProvider", "nopassword-for-the-win", Token);
    if (IsValid)
    {
        // TODO: Generate a bearer token
        var BearerToken = "";
        return BearerToken;
    }
    return Unauthorized();
}

We are again fetching the user based on email. As a result, if we are not able to find the user we'll return 404 Not Found.

We then continue to verify the user. VerifyUserTokenAsync takes user, token provider, purpose, and token as input parameters. The purpose should be the same as the one used while generating the token.

If the token is not valid, return 401 Unauthorised. Otherwise return the bearer token. This is a good article on how to generate a bearer token for the user.

You can find the whole project here.

Conclusion

Good features used to be the main thing that mattered when creating an app. But today, besides having great features convenience is a priority for users.

In this article, we looked at one way you can make your apps more user-friendly. Let me know if you have other ways to improve your apps.

Check here for more tutorials like this.