Passwords are a pain in the a..
With thousand's of softwares and app's launching everyday, you would like to make your software stand out. Most importantly, it should have some unique USP, but in addition it should provide convenience and ease of use.
For instance, one of the pain points for many apps is they require a username and a password to login. I personally have to remember 10-15 passwords for apps like Gmail, Facebook, Instagram, etc. You get the idea.
In today's article we are going to create a solution for your API's that will allow your users to login without a password.
The How of Going Password less
In order to omit the password there needs to be some type of token generated for a user.
This token will then be sent to a user where only the user can access it e.g. on email or phone number. Here is an overview of the flow.
Dotnet Identity provides ways to generate tokens for email confirmation or changing email or phone. We will see more about it below.
There are mainly two token providers available
- TotpSecurityStampBasedTokenProvider (Time-based One Time Password).
- DataProtectionTokenProvider
TotpSecurityStampBasedTokenProvider
It generates time based tokens which are valid for around 3 minutes (Reference: Source Code). Based on the token provider the tokens are generated from the Email, PhoneNumber or user's id as well as the user's security stamp.
Dotnet Identity provides 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 duration 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.
Setting up Identity
The .NET Identity provides ways to manage users, passwords, profile data, roles, claims, tokens, email confirmation, and more.
Let's start with a scratch project. Create a new 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.
Custom Token Provider
Let's create a custom token provider that generates token that are valid for 10 minutes.
NPTokenProvider
Create a new file called NPTokenProvider.cs. 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)
{ }
}
We are here subclassing the DataProtectorTokenProvider. Nothing out of the ordinary except in the constructor we are passing NPTokenProviderOptions. The options need to be subclass of DataProtectionTokenProviderOptions.
NPTokenProviderOptions
Create a new file 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 database to store it's users and other data related to the project. Dotnet EF Framework provides a nice helper DbContext to handle sessions with the database and query and save entites. 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 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 Configure method.
NoPasswordController.cs
Let's create a controller for our login and verify API's. 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 it.
Login API
Let's add a Login api which accepts an Email as input. The Email is the unique identifier for a user i.e. there should be a one-to-one relation ship 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 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 will click 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 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 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 same as the one used while generating token.
If the token is not valid, return 401 Unauthorised. Otherwise return the bearer token. This is a good article on how to generate bearer token for the user.
In conclusion our article explains on how to allow users to login without a password. You can find the whole project here
Conclusion
Providing features was supposed to be the most important thing in the '90s. But today besides having great features, the convenience for the users is a priority.
We looked at one of the ways of providing convenience. That said, let us know in the comments below for more ways to do it.
Check here for more tutorials like this.