How to use Nametag for sign in with ID

Welcome

This tutorial will walk you through using Nametag to identify users to your web site using their ID. We don’t expect any prior experience with Nametag, OAuth2, or any particular Node.js libraries. However, we will be using Node.js and Javascript to serve our very simple web site, so having Node installed on your computer is required; some experience with that will be useful.

There are two parts to setting up this example

  1. Creating a Nametag organization and configuring it for your application.
  2. Creating the application and configuring it to use Nametag.

Configuring Nametag

To get started, go to https://console.nametag.co. Because we use our own sign in with ID to control access to the developer console, you will first need to scan the QR code and provide your ID to access the site.

Since we are testing, switch to the Sandbox environment.

Switch to the Sandbox environment

On the App Customization page, edit the public name and icon for your app.

  1. Set the public name to “Candy Shop”

  2. Set the description to “The world’s best candy store”

  3. Download the sample logo for the Candy Shop application and use it as the logo.

When the App Customization page is complete, it should look something like this:

The App Customization page filled out

On the Data Requests tab we define which data we will request from our users:

  1. Note that the nt:legal_name scope has already been selected.
  2. Add Login (login) with an expiration of 1 day

When the Data Requests page is complete, it should look something like this:

The Data Requests page filled out

Nametag uses OAuth 2.0 for sign in. We need to configure an OAuth 2.0 Client ID and Secret which we’ll use later. On the OAuth tab:

  1. Make a note of your Client ID.
  2. Press Create a new API key, and make a note of your API Key. (Note: you’ll only be able to access this API key once, if you lose it, you’ll have to generate a new one.)
  3. Press Create a callback URL and enter “http://localhost:8000”

The OAUth page filled out

Creating an application

Now that we’ve configured Nametag for our application and have our client ID and secret, we can start implementing a web site that uses Nametag to allow people to log in. We won’t use any fancy frameworks or anything, just standard node libraries.

Check that you have node installed:

$ node -p true
true

Create a file called tutorial.js with the following content, for a very basic web server:

const http = require("http");
const host = 'localhost';
const port = 8000;

const requestListener = function (req, res) {
    res.writeHead(200);
    res.end("Hello, World!");
};

const server = http.createServer(requestListener);
server.listen(port, host, () => {
    console.log(`listening on http://${host}:${port}`);
});

Run the server with:

$ node tutorial.js
listening on http://localhost:8000

Open your browser to http://localhost:8000 and you should see our greeting.

Crafting an authorization request

Nametag uses OAuth 2.0 to negotiate credentials between the end-user, Nametag, and your web application. The OAuth exchange is shown in the diagram in Nametag Authentication Flow Sequence Diagram.

img

Our simple web application needs to implement those steps. To start, we need to craft an authorization request URL and direct our users there. Note that authorization requests are valid for 168 hours (7 days) from their creation; if the end-user responds to the authorization request after that time, they will receive a message indicating that the request has expired.

const client_id = "CLIENT-ID"
const client_secret = "CLIENT-SECRET"

// requestListener is the main request handler
async function requestListener(req, res) {
    // craft an authorize URL that requests the `login` and `nt:legal_name` scopes
    // We encode the current URL as `state` so that we know where to redirect, but you
    // can use whatever you like there.
    let query = new URLSearchParams()
    query.set("client_id", client_id)
    query.set("redirect_uri", "http://localhost:8000/callback")
    query.set("state", req.url)
    query.set("scope", "login nt:legal_name")
    let authorize_url = `https://nametag.co/authorize?` + query.toString()

    res.writeHead(200, {"Content-type": "text/html"});
    res.write(`<html><a href="${authorize_url}">Log in</a></html>`)
    res.end()
}

Kill and restart your server process. Then direct your browser to http://localhost:8000 and click “Log in”. You will be directed to Nametag and prompted to scan a QR code.

Note that the Candy Shop logo and the text on the upper right that says “Back to Candy Shop”. This means we’ve done the branding correctly.

Handling the OAuth 2.0 callback

If you click “Log in” and complete the process, you’ll end up right back where you started, with the “Log in” link. But notice that this time the URL contains a bunch of query parameters. It might look something like this:

http://localhost:8000/callback?code=1c20d6c1dc05190b8b45db47e13442e1&state=%2F

This is our callback URL, and we must handle those query parameters.

First we will add some simple http routing to our server:

// requestListener is the main request handler
async function requestListener(req, res) {
    // Very simple request routing. In real life, you'd use something
    // from a framework or library.
    const url = new URL(req.url, `http://${req.headers.host}`)
    if (url.pathname == "/callback") {
        return handleCallback(url, req, res)
    }

    // ... the rest of our requestListener function 
}

In handleCallback() we will execute an HTTP request against the token endpoint to trade our code for an id_token

async function handleCallback(url, req, res) {
    const code = url.searchParams.get("code")

    // make a request to the token endpoint to exchange the code for an
    // id_token and other information about the person.
    let requestBody = new FormData()
    requestBody.append("grant_type", "authorization_code")
    requestBody.append("client_id", client_id)
    requestBody.append("client_secret", client_secret)
    requestBody.append("redirect_uri", "http://localhost:8000/callback")
    requestBody.append("code", code)
    let tokenResponse = await fetch("https://nametag.co/token", {
        method: "POST",
        body: requestBody
    })
    let tokenResponseBody = await tokenResponse.json()
}

Restart the server and run through the login flow again, you will get output that looks something like:

{
  "access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJvYXQiLCJleHAiOjE2NzMwMjUzMDcsImlhdCI6MTY3MzAyMTcwNywiaXNzIjoicm9zcy5uYW1ldGFnZGV2LmNvbSIsIm5iZiI6MTY3MzAyMTcwNywic3ViIjoicnFwMmE3ZDM3cXFxZmFqZWp3YWJycDJ0NzRAbDc5YzB6YjB4YXVlbXdtLnJvc3MubmFtZXRhZ2Rldi5jb20ifQ.oc1-rAA-2SRlSKDoR-RPG-n6WKxjRifMF_YIETRnVRm-SYpnlnjWVL9FqrKcssHtDidFZFojHb62Tnzw_Owoxw",
  "refresh_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJvcnQiLCJleHAiOjIxNDc0ODM2NDcsImlhdCI6MTY3MzAyMTcwNywiaXNzIjoicm9zcy5uYW1ldGFnZGV2LmNvbSIsIm5iZiI6MTY3MzAyMTcwNywic3ViIjoicnFwMmE3ZDM3cXFxZmFqZWp3YWJycDJ0NzRAbDc5YzB6YjB4YXVlbXdtLnJvc3MubmFtZXRhZ2Rldi5jb20ifQ.XNPVsqjH0OZEUcaE0FauoCKwKnLWCC7cvW7RSm3xkccCh-ZsKFHDV7reZ9R97HNOFlHS94tQKaKSDwE_ET-C8w",
  "id_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJvaXQiLCJleHAiOjIxNDc0ODM2NDcsImlhdCI6MTY3MzAyMTcwNywiaXNzIjoicm9zcy5uYW1ldGFnZGV2LmNvbSIsIm5iZiI6MTY3MzAyMTcwNywic3ViIjoicnFwMmE3ZDM3cXFxZmFqZWp3YWJycDJ0NzRAbDc5YzB6YjB4YXVlbXdtLnJvc3MubmFtZXRhZ2Rldi5jb20ifQ.T8RCC1XADLobbXSIpdvzF7TPi0ajXMxXcBTloc4FMzKZIMJlxBbHNSEso953z7GeQ84b--JUQFfc31wlYfmSig",
  "scope": "login nt:legal_name",
  "expires_in": 3600,
  "token_type": "Bearer",
  "subject": "rqp2a7d37qqqfajejwabrp2t74@l79c0zb0xauemwm.nametag.co"
}

The two fields of interest here are id_token which is suitable for use as an end-user authentication token, and subject, which uniquely identifies the user.

Note: subject is is not suitable for use as end-user authentication by itself because it does not embed any kind of signing. id_token is basically just a signed blob that encodes subject.

Let’s set the ID token as a cookie. Add the following to the end of handleCallback():

async function handleCallback(url, req, res) {
    // ... beginning of handleCallback

    const id_token = tokenResponseBody.id_token
    res.setHeader("Content-type", "text/html")
    res.setHeader("Set-Cookie", `id_token=${id_token}`)
    res.writeHead(200)

    // recall that for us, state is the URL we are going back to after
    // authentication. You can use state for whatever you like.
    const state = url.searchParams.get("state")
    res.write(`<html><a href="${state}">Continue</a></html>`)
    res.end()
}

Now when we restart the server, sign in, and click “Continue” we are back at the root page of our app, but this time we have a cookie set.

ID token cookie is set after sign in

Let’s improve our root page to extract the id token:

function getIDTokenFromCookie(req) {
    const cookie = req.headers["cookie"]
    if (!cookie) {
        return null
    }
    const cookieParts = cookie.split(";")
    for (const cookiePart of cookieParts) {
        if (cookiePart.startsWith("id_token=")) {
            return cookiePart.replace(/^id_token=/, "")
        }
    }
    return null
}

function requestListener(req, res) {
    const url = new URL(req.url, `http://${req.headers.host}`)
    if (url.pathname == "/callback") {
        return handleCallback(url, req, res)
    }

    const idToken = getIDTokenFromCookie(req)
    if (idToken) {
        res.writeHead(200, {"Content-type": "text/html"});
        res.write(`<html>id_token is <code>${idToken}</html>`)
        res.end()
        return
    }
    //...
}

This time when we restart the program, and re-load the page, we’ll see something like:

id token is eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJvaXQiLCJleHAiOjIxNDc0ODM2NDcsImlhdCI6MTY3MzA5NzEzMCwiaXNzIjoicm9zcy5uYW1ldGFnZGV2LmNvbSIsIm5iZiI6MTY3MzA5NzEzMCwic3ViIjoicnFwMmE3ZDM3cXFxZmFqZWp3YWJycDJ0NzRAbDc5YzB6YjB4YXVlbXdtLnJvc3MubmFtZXRhZ2Rldi5jb20ifQ.p4dbsxFEJ-GqBlmqRZ7hQXyLUPquuEUmtUfZaSnrcjPu2BsTDhY-93m98f1Yf_P7KZWIDVp7xHLW4JlWlPC9IA

Fetching validated properties

Now, lets use the Get Properties endpoint to gather some information about the user so we can greet them properly.


async function requestListener(req, res) {
    // ...
    const idToken = getIDTokenFromCookie(req)
    if (idToken) {
        // use the id token to fetch information about the person.
        // this has the import side effect of validating the id token
        const resp = await fetch("https://nametag.co/people/me/properties/nt:legal_name", {
            headers: {
                "Authorization": "Bearer " + idToken
            }
        })
        const responseBody = await resp.json()
        const nameProperty = responseBody.properties.filter(p => p.scope == "nt:legal_name").pop()
        if (nameProperty.status == 200) {
            res.writeHead(200, {"Content-type": "text/html"});
            res.write(`<html>Hello, ${nameProperty.value}!</html>`)
            res.end()
            return
        }
    }
  
  // ...
}

By using the id_token to authenticate the request to Nametag, we verify that the ID token is indeed valid, at the same time as we gather verified information about the person.

The response from this API looks something like this:

{
  "subject": "rqp2a7d37qqqfajejwabrp2t74@l79c0zb0xauemwm.nametag.co",
  "properties": [
    {
      "expires": "2023-01-17T13:27:50Z",
      "scope": "nt:legal_name",
      "value": "Alice Smith",
      "status": 200
    }
  ]
}

Recall that subject is the unique identifier for the person.

For each property that has been verified and which the user has consented to share appears in the properties list. If the data has not be verified, or if the user has not consented to share the data, the status will be 403, rather than 200.

Putting it all together

Here is the full example program that you can run on your computer using Node.

/**
 * Copyright 2023 Nametag Inc.
 *
 * All information contained herein is the property of Nametag Inc.. The
 * intellectual and technical concepts contained herein are proprietary, trade
 * secrets, and/or confidential to Nametag, Inc. and may be covered by U.S.
 * and Foreign Patents, patents in process, and are protected by trade secret or
 * copyright law. Reproduction or distribution, in whole or in part, is
 * forbidden except by express written permission of Nametag, Inc.
 */

const http = require("http");
const host = "localhost";
const port = 8000;

const client_id = "CLIENT-ID";
const client_secret = "CLIENT-SECRET";

// handleCallback handles the OAuth 2.0 completion.
// This code runs when the user is redirected back to our service from Nametag
// after having completed authentication.
async function handleCallback(url, req, res) {
  // fetch parameters to the callback URL
  // Note: if authentication failed, `code` will not be set and `error` will contain a message.
  //   In real life, you will need to handle this case.
  const code = url.searchParams.get("code");
  const state = url.searchParams.get("state");

  // make a request to the token endpoint to exchange the code for an
  // id_token and other information about the person.
  let requestBody = new FormData();
  requestBody.append("grant_type", "authorization_code");
  requestBody.append("client_id", client_id);
  requestBody.append("client_secret", client_secret);
  requestBody.append("redirect_uri", "http://localhost:8000/callback");
  requestBody.append("code", code);
  let tokenResponse = await fetch("https://nametag.co/token", {
    method: "POST",
    body: requestBody,
  });
  let tokenResponseBody = await tokenResponse.json();

  // `id_token` is suitable for use as end-user authentication, so
  // extract it from the response and set it as a cookie.
  //
  // Note: `subject` is not suitable for use as end-user authentication by itself
  // because it does not embed any kind of signature. `id_token` is basically just
  // a signed blob that encodes `subject`.
  //
  // Note: Cookies are hard to get secure and correct. This is just a demo.
  // In real life you probably need to think a little harder about cookie
  // parameters, lifetime, etc. Or better yet, avoid them all together.
  const id_token = tokenResponseBody.id_token;
  res.setHeader("Content-type", "text/html");
  res.setHeader("Set-Cookie", `id_token=${id_token}`);
  res.writeHead(200);

  // recall that for us, state is the URL we are going back to after
  // authentication. You can use state for whatever you like.
  res.write(`<html><a href="${state}">Continue</a></html>`);
  res.end();
}

// getAuthTokenFromCookie is way-too-simple parser for the HTTP Cookie header.
//
// Note: In real life you are probably using a library to parse cookies, such
// as https://github.com/expressjs/cookie-parser.
function getIDTokenFromCookie(req) {
  const cookie = req.headers["cookie"];
  if (!cookie) {
    return null;
  }
  const cookieParts = cookie.split(";");
  for (const cookiePart of cookieParts) {
    if (cookiePart.startsWith("id_token=")) {
      return cookiePart.replace(/^id_token=/, "");
    }
  }
  return null;
}

// requestListener is the main request handler
async function requestListener(req, res) {
  // Very simple request routing. In real life, you'd use something
  // from a framework or library.
  const url = new URL(req.url, `http://${req.headers.host}`);
  if (url.pathname == "/callback") {
    return handleCallback(url, req, res);
  }

  const idToken = getIDTokenFromCookie(req);
  if (idToken) {
    // use the id token to fetch information about the person.
    // this has the import side effect of validating the id token
    const resp = await fetch(
      "https://nametag.co/people/me/properties/nt:legal_name",
      {
        headers: {
          Authorization: "Bearer " + idToken,
        },
      },
    );
    const responseBody = await resp.json();
    const nameProperty = responseBody.properties
      .filter((p) => p.scope == "nt:legal_name")
      .pop();
    if (nameProperty.status == 200) {
      res.writeHead(200, { "Content-type": "text/html" });
      res.write(`<html>Hello, ${nameProperty.value}!</html>`);
      res.end();
      return;
    }
  }

  // craft an authorize URL that requests the `login` and `nt:legal_name` scopes
  // We encode the current URL as `state` so that we know where to redirect, but you
  // can use whatever you like there.
  let query = new URLSearchParams();
  query.set("client_id", client_id);
  query.set("redirect_uri", "http://localhost:8000/callback");
  query.set("state", req.url);
  query.set("scope", "login nt:legal_name");
  let authorize_url = `https://nametag.co/authorize?` + query.toString();

  res.writeHead(200, { "Content-type": "text/html" });
  res.write(`<html><a href="${authorize_url}">Log in</a></html>`);
  res.end();
}

const server = http.createServer(requestListener);
server.listen(port, host, () => {
  console.log(`Server listening on http://${host}:${port}`);
});