Using Nametag for automated account recovery

Overview

This article will teach you how to use Nametag to help your users recover their accounts when they are locked out, either because they forgot their password or because they lost access to a second authentication factor.

We will build a simple app that uses Nametag to verify the identity of existing users who are unable to sign in the normal way. If you’d like to learn how to use Nametag for logins, check out this short tutorial.

In order to use Nametag for account recovery, you must have collected and stored your user’s name at signup time (or at some point before they lose access to their account). The name the user provides doesn’t have to match exactly, but it must be close.

The example code for this demo is available at https://github.com/nametaginc/account-recovery-example-node.

The user experience we will implement will be:

  1. The user provides their email address (or other account ID) and indicates they are locked out and enter your recovery flow.
  2. You redirect the user’s browser to https://nametag.co/authorize with some parameters, including state which tracks the user through the flow.
  3. The user scans a QR code (desktop) or presses a button (mobile) and completes ID verification in Nametag.
  4. The user’s browser is redirected back to your server at a callback URL you specify. This request contains a code query parameter that Nametag provided, as well as the state that you provided.
  5. Your server makes a call to the Nametag API to verify code.
  6. Your server uses state to find the user’s account and fetch the expected name.
  7. Your server makes a call to the Nametag API to verify that the name you expect matches the verified name the user provided to Nametag.
  8. Your server resets their password or MFA.

Configuring Nametag

To get started, visit https://console.nametag.co and scan the QR code with your mobile phone. You authenticate to Nametag using Nametag itself, so you will go through the same ID verification flow as your users will.

Creating a request template

We suggest that you begin by creating a new request template for account recovery. You can create a new template by following these steps:

  1. Navigate to Request templates in the Configure sidebar menu
  2. Select Create a template
  3. Click the template name to rename it Account recovery

Adding scopes

Scopes are the specific data points that people are asked to provide during verification. Different types of requests may ask for different sets of scopes, so you can create as many request templates as you need.

You can add scopes to a request template by following these steps:

  1. Navigate to Request templates in the Configure sidebar menu
  2. Select an existing template or create a new one
  3. Select the Scope field to add or remove scopes

Add the following two scopes (API documentation):

  1. Sign in (login) This scope is required because we are going to bind a verified user to a browser session using a code parameter. (There are some use cases where login is not required, but this isn’t one.)
  2. Legal Name (nt:legal_name) This scope is required so that the user can consent to sharing their verified legal name with you for the purposes of comparison. Without it, the name comparison API will reject your request.

Setting an expiration time

Directly below the scopes, you’ll see Expiration time. This represents the amount of time that you will retain access to a user’s data after a request has been completed. Because we are performing account recovery automatically, this time period can a very short interval, like 5 minutes.

Make sure you press Save to save the changes you made to your template

Template page

Configuring OAuth

Nametag implements OAuth 2.0 to allow user to approve of sharing their information with requestors. In order to use that flow, configuration of a few settings in Nametag is necessary. Navigate to the OAuth page from the left hand panel.

OAuth page

Client ID & Client Secret

The OAuth 2.0 client ID and secret are used to identity your server to Nametag. As the name suggests, the client ID is the public part of and is safe to reveal to user’s browsers, while the client secret should be known only to your server.

You can obtain your Client ID by following these steps:

  1. Navigate to OAuth in the Configure sidebar menu
  2. Copy the Client ID to your clipboard and save it somewhere (we’ll use it in just a moment)

You can generate a new client secret by following these steps:

  1. Navigate to OAuth in the Configure sidebar menu
  2. Select Create new API key
  3. Accept the default name for the key
  4. Copy the key to your clipboard and save it somewhere (we’ll use it in just a moment).

Note: Don’t forget to copy your client secret before moving on. This key will only be shown once, and it cannot be recovered later. (If you do forget you can always delete and recreate the secret if you need to)

Callback URL

The callback URL is where Nametag will send your users after they finish verifying their identity. We’ll implement a handler for this URL in the next section. For now, enter http://localhost:6060/recover/finish for the callback URL.

Implementing the server

This section walk you through how to implement the server components. The code samples are provided in typescript, but any language will work.

Basic scaffolding

Create a new project:

$ mkdir account-recovery-example
$ cd account-recovery-example

Set up a basic package.json:

{
  "name": "account-recovery-example",
  "version": "1.0.0",
  "description": "Example of using Nametag for account recovery",
  "scripts": {
    "dev": "nodemon server.ts"
  },
  "dependencies": {
    "@types/express": "^4.17.17",
    "express": "^4.18.2",
    "nodemon": "^2.0.21",
    "ts-node": "^10.9.1",
    "typescript": "^4.9.5"
  }
}

Install dependencies:

$ npm install

Set up a basic HTTP server in server.ts:

import http from 'http';
import express, { Request, Response, NextFunction } from 'express';

const router = express();
router.use(express.urlencoded({ extended: false }));

const getRoot = async (req: Request, res: Response, next: NextFunction) => {
    return res.status(200).type("text/html").send(`
<html>
    <head>
        <style>input, button, a { display: block; margin: 10px 0;}</style>
    </head>
    <body>
        <h1>Example login page</h1>
        <form method="post" action="/recover/start">
            <input type="text" name="email" placeholder="Email" value="alice@example.com">
            <button>I forgot my password</button>
        </form>
    </body>
</html>`)
};
router.get('/', getRoot)

const httpServer = http.createServer(router);
const PORT = 6060
httpServer.listen(PORT, () => console.log(`The server is running on port ${PORT}`));

Start the server with:

$ npm run dev

Open your browser to http://localhost:6060 and you should see our (very) basic login page.

Login page

Starting the flow

To start the flow, we need to direct the user’s browser to https://nametag.co/authorize with parameters that describe the flow we want. Add the following handler to /recover/start.

// values from Nametag
const CLIENT_ID = "YOUR_CLIENT_ID"
const CLIENT_SECRET = "YOUR_CLIENT_SECRET"

interface Account {
    email: string
    name: string
}

// fake accounts database
var accounts: Account[] = [
    {
        email: "alice@example.com",
        // if you put your name here, recovery will work.
        // or, you can put someone else's name if you want it to fail.
        name: "YOUR_NAME_FOR_TESTING"
    }
]

const handleRecoverStart = async (req: Request, res: Response, next: NextFunction) => {
    const state = req.body["email"]

    const query = new URLSearchParams()
    query.append("client_id", CLIENT_ID)
    query.append("redirect_uri", "http://localhost:6060/recover/finish")
    query.append("scope", "login nt:legal_name")
    query.append("state", state)

    let authorizeURL = "https://nametag.co/authorize?" + query.toString()
    return res.status(200).redirect(authorizeURL.toString())
};
router.post('/recover/start', handleRecoverStart)

This handler constructs a Nametag authorization URL, specifying the parameters we will need. The value YOUR_CLIENT_ID identifies your server to Nametag. The redirect_uri parameter tells us where to sent your user when they’re done. scope tells us which information you need (nt:legal_name) and also the kind of flow we’re asking for (login). Finally we are using the state parameter to store the account identifier of the person we’re trying to recover.

Make sure you put your name in YOUR_NAME_FOR_TESTING so that you can test the flow for yourself. This is the name we will compare against.

If you now click “I forgot my password,” you will see your browser get redirected to Nametag and display a QR code.

Authorize

You can scan the QR code with your mobile phone. You will re-use your stored ID from signing in to Nametag, so you’ll just need to press accept.

Request Request Complete

When you press “accept” your browser will automatically advance to your callback URL. You’ll see an error because we haven’t implemented yet.

Handling the callback

Now we need to implement a handler for /recover/finish. If the request is successful, the query will contain code (which Nametag issued) and state (which you passed to authorize). We have to do a few things in this handler:

  1. Call the Nametag API to verify code and fetch subject. (A subject is a unique identifier for the person, which we’ll need in step 3.

  2. Use state to find the user’s account and fetch the expected name.

  3. Make a call to the Nametag API to verify that the name we expect matches the verified name the user provided to Nametag.

  4. Reset the password.

const handleRecoverFinish = async (req: Request, res: Response, next: NextFunction) => {
    if (req.query["error"]) {
        return res.status(400).send(`Error: ${req.query["error"]}`)
    }
    const code = req.query["code"] as string
    const state = req.query["state"] as string

    // call the Nametag API to verify code and fetch subject
    let subject: string
    try {
        subject = await verifyCode(code)
    } catch (e) {
        return res.status(400).send(`Error: ${e}`)
    }

    // Use state to find the user's account
    const account = accounts.find(a => a.email === state)
    if (!account) {
        return res.status(400).send(`Error: account does not exist`)
    }

    // call the Nametag API to compare the expected name in account to the one on their ID.
    const matchConfidence = await compareName(subject, account.name)
    if (matchConfidence < 0.75) {
        return res.status(403).send(`Error: name does not match`)
    }

    // TODO: reset the password

    return res.status(200).send(`Success! Your account is reset.`)
};
router.get('/recover/finish', handleRecoverFinish)

You will notice that we are also handling an error query parameter. This parameter is present when code is not present, and indicates that the user did not accept the request. The OAuth 2.0 standard specifies a few values for this parameter, but you will most commonly see access_denied which means that the user cancelled the flow without completing it.

The function to verify code uses CLIENT_ID and CLIENT_SECRET so Nametag knows it’s talking to your server. If code is invalid, the response from the server will contain an error key, or subject if the code was valid. There are other fields too, but we don’t need them right now. For details, see also the token endpoint in the API docs.

const verifyCode = async (code: string): Promise<string> => {
    const tokenRequest = new URLSearchParams();
    tokenRequest.set("grant_type",  "authorization_code")
    tokenRequest.set("client_id", CLIENT_ID)
    tokenRequest.set("client_secret",  CLIENT_SECRET)
    tokenRequest.set("redirect_uri",  "http://localhost:6060/recover/finish")
    tokenRequest.set("code",  code)
    const tokenHttpResponse = await fetch("https://nametag.co/token", {
        method: "POST",
        body: tokenRequest
    })
    const tokenResponse = await tokenHttpResponse.json()
    if (tokenResponse["error"]) {
        throw new Error(`${tokenResponse["error"]} ${tokenResponse["error_description"]}`)
    }
    return tokenResponse["subject"] as string
}

Now that we have a subject, we can use it to verify that the person’s name is what we expect. The compare values endpoint returns a value between 0.0 (not matched) and 1.0 (matched exactly).

const compareName = async (subject: string, expectedName: string): Promise<number> => {
    const compareHttpResponse = await fetch(`https://nametag.co/people/${encodeURIComponent(subject)}/compare?token=${encodeURI(CLIENT_SECRET)}`,
        {
            method: "POST",
            body: JSON.stringify({
                expectations: [
                    {
                        "scope": "nt:legal_name",
                        "value": expectedName
                    }
                ]
            })
        })

    const compareResponse = await compareHttpResponse.json()
    return compareResponse["confidence"] as number
}

Recover finish

Putting it all together

The fully worked example is at https://github.com/nametaginc/account-recovery-example-node/blob/main/server.ts.

You should be able to pass all the way through the flow and see your success message at the end.

import http from 'http';
import express, { Request, Response, NextFunction } from 'express';

const router = express();
router.use(express.urlencoded({ extended: false }));

const CLIENT_ID = "YOUR_CLIENT_ID"
const CLIENT_SECRET = "YOUR_CLIENT_SECRET"

interface Account {
    email: string
    name: string
}

// fake accounts database
var accounts: Account[] = [
    {
        email: "alice@example.com",
        // if you put your name here, recovery will work.
        // or, you can put someone else's name if you want it to fail.
        name: "YOUR_NAME_FOR_TESTING",
    },
]

const getRoot = async (req: Request, res: Response, next: NextFunction) => {
    return res.status(200).type("text/html").send(`
<html>
    <head>
        <style>input, button, a { display: block; margin: 10px 0 }</style>
    </head>
    <body>
        <h1>Example login page</h1>
        <form method="post" action="/recover/start">
            <input type="text" name="email" placeholder="Email" value="alice@example.com">
            <button>I forgot my password</button>

            <div><strong>Demo note(s)</strong></div>
            ${accounts.map(a => `<div>${a.email} can be recovered only by ${a.name}</div>`)}
        </form>
    </body>
</html>`)
};
router.get('/', getRoot)

const handleRecoverStart = async (req: Request, res: Response, next: NextFunction) => {
    const state = req.body["email"]

    const query = new URLSearchParams()
    query.append("client_id", CLIENT_ID)
    query.append("redirect_uri", "http://localhost:6060/recover/finish")
    query.append("scope", "login nt:legal_name")
    query.append("state", state)

    let authorizeURL = "https://nametag.co/authorize?" + query.toString()
    return res.status(200).redirect(authorizeURL.toString())
};
router.post('/recover/start', handleRecoverStart)

const verifyCode = async (code: string): Promise<string> => {
    const tokenRequest = new URLSearchParams();
    tokenRequest.set("grant_type",  "authorization_code")
    tokenRequest.set("client_id", CLIENT_ID)
    tokenRequest.set("client_secret",  CLIENT_SECRET)
    tokenRequest.set("redirect_uri",  "http://localhost:6060/recover/finish")
    tokenRequest.set("code",  code)
    const tokenHttpResponse = await fetch("https://nametag.co/token", {
        method: "POST",
        body: tokenRequest
    })
    const tokenResponse = await tokenHttpResponse.json()
    if (tokenResponse["error"]) {
        throw new Error(`${tokenResponse["error"]} ${tokenResponse["error_description"]}`)
    }
    return tokenResponse["subject"] as string
}

const compareName = async (subject: string, expectedName: string): Promise<number> => {
    const compareHttpResponse = await fetch(`https://nametag.co/people/${encodeURIComponent(subject)}/compare?token=${encodeURI(CLIENT_SECRET)}`,
        {
            method: "POST",
            body: JSON.stringify({
                expectations: [
                    {
                        "scope": "nt:legal_name",
                        "value": expectedName
                    }
                ]
            })
        })

    const compareResponse = await compareHttpResponse.json()
    return compareResponse["confidence"] as number
}

const handleRecoverFinish = async (req: Request, res: Response, next: NextFunction) => {
    if (req.query["error"]) {
        return res.status(400).send(`Error: ${req.query["error"]}`)
    }
    const code = req.query["code"] as string
    const state = req.query["state"] as string

    // call the Nametag API to verify code and fetch subject
    let subject: string
    try {
        subject = await verifyCode(code)
    } catch (e) {
        return res.status(400).send(`Error: ${e}`)
    }

    // Use state to find the user's account
    const account = accounts.find(a => a.email === state)
    if (!account) {
        return res.status(400).send(`Error: account does not exist`)
    }

    // call the Nametag API to compare the expected name in account to the one on their ID.
    const matchConfidence = await compareName(subject, account.name)
    if (matchConfidence < 0.75) {
        return res.status(403).send(`Error: name does not match`)
    }

    // TODO: reset the password

    return res.status(200).send(`Success! Your account is reset.`)
};
router.get('/recover/finish', handleRecoverFinish)

const httpServer = http.createServer(router);
const PORT = 6060
httpServer.listen(PORT, () => console.log(`The server is running on port ${PORT}`));

Next steps

How that we have the basic flow working, you’ll want to make a few adjustments before making it real:

  • You could use a session token or account identifier for state rather than an email address, to avoid passing the email address around.
  • If you know the user’s birth date, you can check that it matches with the get properties endpoint.
  • Replace your callback URL with the real URL of your server.
  • Upload your logo and configure branding for a customized experience.