Guide Integrating with NuID


In the Intro to NuID guide we showed you the set of problems we've set out to solve, namely better identity management starting with trustless authentication. In this guide we'll walk you through the process of integrating NuID's trustless authentication protocol into an existing password-based authentication flow.

Although this guide shows an example Node.js application with a React.js front-end, you can use any backend language or framework that supports HTTP calls with headers. Similarly, React is not a necessity, any front-end setup will do so long as you can intercept form submit events and make HTTP calls from your client to your server using JavaScript. Support for client and SDK libraries in other languages is coming, be sure to regularly check the Resources section of our documentation for the latest.

This guide is meant to minimally convey the code necessary to implement NuID for authentication in the interest of space. A complete runnable example can be found in our NuID/examples repository on GitHub. Additional browse links will be placed below each code example so you can get more context for the supporting code.

Important Note: The user's password should never leave your client application (i.e. in-browser or on-device memory) to go to your server or the NuID Auth API. The @nuid/zk npm package should be used to generate credentials and proofs from challenges.

1. Configuration


Install the @nuid/zk client library

A JavaScript implementation of proof generation and verification is available for the browser. The @nuid/zk package can be found on npm. zk is short for 'zero knowledge'.

cd my-app/client
npm install -s @nuid/zk
# or
yarn add @nuid/zk

Using the @nuid/zk client npm package

You'll need to require/import the package in your client application.

// my-app/client/src/app.js
const Zk = require('@nuid/zk') // commonjs require format
import Zk from '@nuid/zk'      // or es6 import syntax

Install the @nuid/zk-react-native client library

A JavaScript implementation of proof generation and verification is available for React Native. The @nuid/zk-react-native package can be found on npm. zk is short for 'zero knowledge'.

1. First you'll need to install a few dependencies:

  • Install the @nuid/zk-react-native package instead of @nuid/zk. This is a react-native specific version of NuID's zk package (which has an identical JS interface to @nuid/zk).
  • You'll also need react-native-randombytes as a top-level dependency in your app so that react-native will link it correctly.
  • And finally, node-libs-react-native for shimming node dependencies in react-native which is necessary to support zk credential and proof generation.
$ yarn add @nuid/zk-react-native node-libs-react-native react-native-randombytes
# if RN < 0.60
$ react-native link react-native-randombytes
# else RN >= 0.60, instead do
$ cd iOS && pod install

2. Next, modify metro.config.js to add extraNodeModules configuration to wire up node-libs-react-native correctly:

// metro.config.js
module.exports = {
  // ...
  resolver: {
    extraNodeModules: require('node-libs-react-native')
  }
};

3. Import node-libs-react-native/globals before you import @nuid/zk-react-native:

// my-rn-project/index.js
// Add globals here (or anywhere _before_ importing @nuid/zk-react-native)
import 'node-libs-react-native/globals';
import { registerRootComponent } from 'expo';
import App from './src/app';
registerRootComponent(App);

Add your NuID API key to your server configuration

You’ll use your NuID API key when configuring the SDK library to talk to the NuID Auth API. You should protect this private API key from being published in a publicly accessible repository or system configuration.

Install the @nuid/sdk-nodejs library on the server

The JavaScript/TypeScript SDK package is provided to ease integration with the NuID Auth API. Using an SDK is not required, you can call our APIs directly with any general purpose HTTP library available in your backend language if you prefer. The @nuid/sdk-nodejs package can be found on npm. Docs for the package are available here. We're continually adding packages for newly supported languages, check our Resources documentation often for updates.

cd my-app/server
npm install -s @nuid/sdk-nodejs
# or
yarn add @nuid/sdk-nodejs

Using the @nuid/sdk-nodejs package

And finally, let's configure the SDK package to talk to the NUID Auth API. Below is an example of how to implement your own server API using express.js.

# Add the API key to your environment configuration
export NUID_API_KEY="<API KEY>"

// my-app/server/src/app.js

const express = require('express')
const fetch = require('node-fetch')
const nuidApi = require('@nuid/sdk-nodejs').default({
  auth: { apiKey: process.env.NUID_API_KEY } // '<API KEY>'
})

const server = express()
server.listen(process.env.PORT)

// see endpoint examples below

Install the nuid/sdk library

The nuid/sdk Clojure package is provided to ease integration with the NuID Auth API. The package can be found on Clojars (or GitHub if you're using tools.deps).

;; deps.edn
{:deps
 {;; ...
  nuid/sdk {:git/url "https://github.com/nuid/sdk-clojure" :sha "..."}}}

Configuring the nuid/sdk package

Now let's configure the SDK package to talk to the NUID Auth API. Below is an example of how to implement your own server API using ring. You can view the full example in our examples repo.

# Add the API key to your environment configuration
export NUID_API_KEY="<API KEY>"
;; my-app/server/src/my_app/server.clj

(ns my-app.server
  (:require
   [bidi.ring :as bidi.ring]
   [nuid.sdk.api.auth :as auth]
   [ring.adapter.jetty :as jetty]))

(def not-found
  (constantly {:status 404}))

;; see route handlers in later sections
(def routes
  ["/"
   [["challenge" challenge-handler]
    ["login"     login-handler]
    ["register"  register-handler]
    [true        not-found]]
   [true         not-found]])

(def handler
  (->
   routes
   (bidi.ring/make-handler)
   ;; other middleware e.g. content type handling, cors, sessions, etc
   ))

(defn start!
  []
  (auth/merge-opts! {::auth/api-key (System/getenv "NUID_API_KEY")}) ;; <API KEY>
  (jetty/run-jetty
   handler
   {:port  (Integer/parseInt (System/getenv "PORT"))
    :join? false})))

Add the nuid-sdk gem to your app

The Ruby SDK gem is provided to ease integration with the NuID Auth API. Using the gem is not required, you can call our APIs directly with any general purpose HTTP library available in your backend language if you prefer. The nuid-sdk gem can be found on npm. Docs for the package are available here. We're continually adding packages for newly supported languages, check our Resources documentation often for updates.

All Ruby examples in this guide presume you're using Rails, though of course that is not a requirement. Add the gem to your Gemfile and bundle install to get started.

# my-rails-app/Gemfile
gem "nuid-sdk"
$ bundle install

Using the nuid-sdk gem

Now let's configure the SDK gem to talk to the NUID Auth API.

# my-rails-app/config/application.rb
config.x.nuid.auth_api_key = ENV['NUID_API_KEY'] // '<API KEY>'
require 'nuid/sdk'

class ApplicationController < ActionController::Base
  def nuid_api
    @nuid_api ||= ::NuID::SDK::API::Auth.new(Rails.configuration.x.nuid.auth_api_key)
  end

  def render_error(error, status)
    render(json: {errors: [error]}, status: status)
  end

  #...
end

Add the github.com/NuID/sdk-go package to your app

The Go SDK package is provided to ease integration with the NuID Auth API. Using the package is not required, you can call our APIs directly with any general purpose HTTP library available in your backend language if you prefer. The package can be found on GitHub. Docs for the package are available here. We're continually adding packages for newly supported languages, check our Resources documentation often for updates.

cd my-go-app
GO111MODULE=on go get github.com/NuID/sdk-go
# Add the API key and host to your environment configuration
export NUID_API_KEY="<API KEY>"

And then import the auth package into your server and create a var for the API instance:

// server.go
package main

import (
	"fmt"
	"net/http"
	"os"
	"gorm.io/gorm"
	"github.com/rs/cors"
	"github.com/NuID/sdk-go/api/auth" // import the auth package
)

type Server struct {
	db *gorm.DB
	api *auth.APIClient
}

func main() {
	fmt.Println("Initializing...")
	srv := &Server{
		api: auth.NewAPIClient(os.Getenv("NUID_API_KEY")), // wire it up
		db: initDB(),
	}
	mux := http.NewServeMux()
	mux.HandleFunc("/register", srv.registerHandler)
	mux.HandleFunc("/challenge", srv.challengeHandler)
	mux.HandleFunc("/login", srv.loginHandler)
	port := os.Getenv("PORT")
	addr := "127.0.0.1:" + port
	server := &http.Server{
		Addr: addr,
		Handler: cors.Default().Handler(mux),
	}
	fmt.Printf("Listening at %s\n", addr)
	server.ListenAndServe()
}

2. User Registration - Creating Credentials


The user registration process is not so different than what your app is already doing, namely: Take user info (email, name, password), post it to your registration endpoint, issue a session. To integrate your registration process you simply need to create a credential on the client directly from the user secret (in this case, the password).

Instead of sending the password to the backend, you send the verified credential. Your server endpoint is then responsible for sending that credential to the NuID Auth API. If successful, NuID will return you the credential and a 'nu/id' that should be stored with the user data to be referenced in subsequent authentication challenges.

  1. clientUser visits your application registration page.
  2. clientUser enters their email and password and clicks submit. Optionally, you can perform local form validation regarding your own password requirements, if any.
  3. clientGenerate a verifiedCredential using
    const verifiedCredential = Zk.verifiableFromSecret(password)
  4. clientPost the email and verifiedCredential to your server registration endpoint. Do not send the user's password.
  5. serverCreate a credential by passing the verifiedCredential to the SDK library function auth.credentialCreate which returns a Promise containing the 'nu/id', an encoded string value used to uniquely identify the public credential.
  6. serverStore the 'nu/id' in your database referenced to the newly created user record. You can fetch the credential at any time from the NuID Auth API using the NuID Auth API Get Credential endpoint.
  7. serverclientThe rest of your registration flow continues (e.g. issuing a session, sending email notifications, etc.).
// my-app/client/src/register.js

// Registration form submit handler
const onSubmit = async (formState, event) => { // 02
  const email = formState.email
  const password = formState.password
  const verifiedCredential = Zk.verifiableFromSecret(password) // 03
  const res = await serverPost('/register', { // 04
    credential: verifiedCredential,
    email
  })
  if (res.ok) { /* registration successful */ } // 07
  else        { /* registration failed */ }
}

// ... component code ...
// my-app/client/src/register.js
import Zk from '@nuid/zk-react-native'

// Registration form submit handler
const onSubmit = async (formState, event) => { // 02
  const email = formState.email
  const password = formState.password
  const verifiedCredential = Zk.verifiableFromSecret(password) // 03
  const res = await serverPost('/register', { // 04
    credential: verifiedCredential,
    email
  })
  if (res.ok) { /* registration successful */ } // 07
  else        { /* registration failed */ }
}

// ... component code ...
// my-app/server/src/app.js

// ... server setup code ...

// Registration route handler
server.post('/register', async (req, res) => {
  try {
    const email = req.body.email
    const createRes = await nuidApi.auth.credentialCreate(req.body.credential) // 05
    const nuid = createRes.parsedBody['nu/id']
    const user = await db.save('user', { email, nuid }) // 06
    res.status(204).send({ user }) // 07
  } catch (res) {
    res.sendStatus(400)
  }
})
;; my-app/server/src/my_app/server.clj

(defn register-handler
  [{:keys [body-params]}]
  (if-let [_ (db/find-by-email (:email body-params))]
    (fail-res 400 "Email address already taken")
    (let [register-res (auth/credential-create (:credential body-params))
          nuid         (get-in register-res [:body "nu/id"])
          user-params  (-> (select-keys body-params [:email :firstName :lastName])
                           (assoc :nuid nuid))
          user         (when (= 201 (:status register-res))
                         (db/user-insert! user-params)
                         (db/find-by-email (:email body-params)))]
      (if user
        {:status 201
         :body   {:user (user->from-db user)}}
        (fail-res 400 "Invalid request")))))
class UsersController < ApplicationController
  def create
    credential_res = nuid_api.credential_create(params[:credential])
    unless credential_res.code == 201
      return render_error("Unable to create the credential", :bad_request)
    end

    user = User.create!({
      email: params[:email].strip.downcase,
      first_name: params[:firstName],
      last_name: params[:lastName],
      nuid: credential_res.parsed_response["nu/id"]
    })

    render(json: { user: user }, status: :created)
  rescue => exception
    render_error(exception.message, 500)
  end
end
// register.go
type RegistrationReq struct {
	Credential map[string]interface{} `json:"credential"`
	FirstName  string `json:"firstName"`
	LastName   string `json:"lastName"`
	Email      string `json:"email"`
}

type RegistrationRes struct {
	User *User `json:"user"`
}

func (srv *Server) registerHandler(res http.ResponseWriter, req *http.Request) {
	res.Header().Set("Content-Type", "application/json")
	var body RegistrationReq
	json.NewDecoder(req.Body).Decode(&body)

	email := sanitizeEmail(body.Email)
	if len(email) <= 0 {
		requestFailed(res, 400, "Invalid Request")
		return
	}

	_, err = srv.FindByEmail(email)
	if err == nil {
		requestFailed(res, 400, "Email address already in use")
		return
	}

	// Create the NuID Credential on the API
	_, credentialBody, err := srv.api.CredentialCreate(body.Credential)
	if err != nil || credentialBody == nil{
		requestFailed(res, 500, "Unable to create credential")
		return
	}

	user := &User{
		NuID: credentialBody.NuID, // store the NuID on the user record
		Email: email,
		FirstName: body.FirstName,
		LastName: body.LastName,
	}
	dbRes := srv.db.Create(user)
	if dbRes.RowsAffected != 1 {
		requestFailed(res, 500, "Unable to create user")
		return
	}
	res.WriteHeader(201)
	result := &RegistrationRes{User: user}
	json.NewEncoder(res).Encode(result)
}

3. User Login - Proving Credential Challenges


The login process differs from registration in that the user is attempting to prove that they know their secret (password) without revealing it. This is accomplished by getting a credential Challenge for from the NuID Auth API and then proving that challenge.

Because of the extra Challenge step, we'll add a new endpoint to our server backend which will ferry the user's credential challenge back to the client login submit handler. Once we decode the challenge JWT, it's claims can be used to generate the proof with the password. After we have a proof, we send the challenge and proof back to the server with the email to be verified. If the proof and challenge are verified by NuID, the authentication has succeeded and you should then issue the user session.

  1. clientUser visits your application login page without a session.
  2. clientUser enters their email and password and clicks submit.
  3. clientPost the email to your server's challenge endpoint, but don't send the password.
  4. serverLookup the 'nu/id' for that user's email.
  5. serverGet the credential from the SDK by calling the auth.credentialGet function with the user's nuid.
  6. serverGet a challenge for the credential from the SDK by calling the auth.challengeGet function with the credential.
  7. serverThe SDK responds with a one-time challenge for that credential represented as a JWT that is only valid for a short period of time.
  8. serverReturn the challenge JWT to the client.
  9. clientDecode the challenge JWT claims.
  10. clientCreate a proof from the password and challenge JWT claims using
    const proof = Zk.proofFromSecretAndChallenge(password, challengeClaims)
  11. clientSend the challenge JWT and proof back to your server's login endpoint.
  12. serverPass the challenge JWT and proof to the SDK auth.challengeVerify function.
  13. serverWhen the SDK returns a successful response, the user has been authenticated correctly, all without sending their password over the network.
  14. clientserverYour client login flow continues (e.g. redirect to dashboard, issuing session tokens, etc).
// my-app/client/src/login.js

// Login form submit handler
const onSubmit = async (formState, event) => { // 02
  const email = formState.email
  const password = formState.password

  const challengeRes = await serverPost('/challenge', { email: email }) // 03
  const body = await challengeRes.json()
  const jwt = body.challengeJwt // 08
  const challengeClaims = decodeJwt(jwt) // 09
  const proof = Zk.proofFromSecretAndChallenge(password, challengeClaims) // 10

  const loginRes = await serverPost('/login', { // 11
    email: email,
    proof: proof,
    challengeJwt: jwt
  })
  if (loginRes.ok) { /* login successful */ } // 14
  else             { /* login failed */ }
}
// my-app/client/src/login.js
import Zk from '@nuid/zk-react-native'

// Login form submit handler
const onSubmit = async (formState, event) => { // 02
  const email = formState.email
  const password = formState.password

  const challengeRes = await serverPost('/challenge', { email: email }) // 03
  const body = await challengeRes.json()
  const jwt = body.challengeJwt // 08
  const challengeClaims = decodeJwt(jwt) // 09
  const proof = Zk.proofFromSecretAndChallenge(password, challengeClaims) // 10

  const loginRes = await serverPost('/login', { // 11
    email: email,
    proof: proof,
    challengeJwt: jwt
  })
  if (loginRes.ok) { /* login successful */ } // 14
  else             { /* login failed */ }
}
// my-app/server/src/app.js

// Challenge route handler
server.post('/challenge', async (req, res) => {
  const email = req.body.email
  const user = await db.find('user', {email: email}).first // 04
  if (!user) {
    res.sendStatus(401)
    return 
  }

  try {
    const credentialRes = await nuidApi.auth.credentialGet(user.nuid) // 05
    const credential = credentialRes.parsedBody['nuid/credential']
    const challengeRes = await nuidApi.auth.challengeGet(credential) // 06
    const challengeJwt = challengeRes.parsedBody['nuid.credential.challenge/jwt'] // 07
    res.send({challengeJwt: challengeJwt}) // 08
  } catch (res) {
    res.sendStatus(401)
  }
})

// Login route handler
server.post('/login', async (req, res) => {
  const user = await db.find('user', { email: req.body.email }).first
  if (!user) {
    res.sendStatus(401)
    return 
  }

  try {
    const { challengeJwt, proof } = req.body
    await nuidApi.auth.challengeVerify(challengeJwt, proof) // 12
    res.status(201).json({ user }) // 13, 14
  } catch (res) {
    res.sendStatus(401)
  }
})
;; my-app/server/src/my_app/server.clj

(defn challenge-handler
  [{:keys [body-params]}]
  (if-let [user (db/find-by-email (:email body-params))]
    (let [credential-res (auth/credential-get (:users/nuid user))
          credential     (get-in credential-res [:body "nuid/credential"])
          challenge-res  (when (= 200 (:status credential-res))
                           (auth/challenge-get credential))
          challenge-jwt  (get-in challenge-res [:body "nuid.credential.challenge/jwt"])]
      (if (= 201 (:status challenge-res))
        {:status 200
         :body   {:challengeJwt challenge-jwt}}
        (fail-res 401 "Unauthorized")))
    (fail-res 401 "Unauthorized")))

(defn login-handler
  [{:keys [body-params]}]
  (if-let [user (db/find-by-email (:email body-params))]
    (let [{:keys [challengeJwt proof]} body-params
          verify-res                   (auth/challenge-verify challengeJwt proof)]
      (if (= 200 (:status verify-res))
        {:status 200
         :body   {:user (user->from-db user)}}
        (fail-res 401 "Unauthorized")))
    (fail-res 401 "Unauthorized")))
class SessionsController < ApplicationController
  # assuming route is POST /challenge
  def login_challenge
    user = User.find(email: params[:email])
    return render(status: :unauthorized) unless user

    credential_res = nuid_auth_api.credential_get(user.nuid)
    return render(status: :unauthorized) unless credential_res.ok?

    credential = credential_res.parsed_response['nuid/credential']
    challenge_res = nuid_auth_api.challenge_get(credential)
    return render(status: :unauthorized) unless credential_res.ok?

    challenge_jwt = challenge_res.parsed_response['nuid.credential.challenge/jwt']
    render json: {challenge_jwt: challenge_jwt}
  end

  # assuming route is POST /login
  def login_verify
    user = User.find(email: params[:email])
    return render(status: :unauthorized) unless user

    verify_res = nuid_auth_api.challenge_verify(params[:challenge_jwt], params[:proof])
    if res.ok?
      @current_user = user
      # issue session ...
      render(json: @current_user)
    else
      render(status: :unathorized)
    end
  end
end
// challenge.go
type ChallengeReq struct {
	Email string `json:"email"`
}

type ChallengeRes struct {
	ChallengeJWT auth.JWT `json:"challengeJwt"`
}

func (srv *Server) challengeHandler(res http.ResponseWriter, req *http.Request) {
	res.Header().Set("Content-Type", "application/json")
	var body ChallengeReq
	json.NewDecoder(req.Body).Decode(&body)

	email := sanitizeEmail(body.Email)
	if len(email) <= 0 {
		requestFailed(res, 401, "Unauthorized")
		return
	}

	user, err := srv.FindByEmail(email)
	if err != nil || user == nil {
		requestFailed(res, 401, "Unauthorized")
		return
	}

	// Fetch the credential from the user's NuID
	_, credentialGetResBody, err := srv.api.CredentialGet(user.NuID)
	if err != nil || credentialGetResBody == nil {
		requestFailed(res, 401, "Unauthorized")
		return
	}

	// Now get a challenge from that credential
	_, challengeGetResBody, err := srv.api.ChallengeGet(credentialGetResBody.Credential)
	if err != nil || challengeGetResBody == nil {
		requestFailed(res, 401, "Unauthorized")
		return
	}

	res.WriteHeader(200)
	result := &ChallengeRes{ChallengeJWT: challengeGetResBody.ChallengeJWT}
	json.NewEncoder(res).Encode(result)
}
// login.go
type LoginReq struct {
	ChallengeJWT auth.JWT `json:"challengeJwt"`
    Email string `json:"email"`
	Proof map[string]interface{} `json:"proof"`
}

type LoginRes struct {
	 User *User `json:"user"`
}

func (srv *Server) loginHandler(res http.ResponseWriter, req *http.Request) {
	res.Header().Set("Content-Type", "application/json")
	var body LoginReq
	json.NewDecoder(req.Body).Decode(&body)

	email := sanitizeEmail(body.Email)
	if len(email) <= 0 {
		requestFailed(res, 401, "Unauthorized")
		return
	}

	user, err := srv.FindByEmail(email)
	if err != nil || user == nil {
		requestFailed(res, 401, "Unauthorized")
		return
	}

	_, err = srv.api.ChallengeVerify(body.ChallengeJWT, body.Proof)
	if err != nil {
		requestFailed(res, 401, "Unauthorized")
		return
	}

	res.WriteHeader(200)
	result := &LoginRes{User: user}
	json.NewEncoder(res).Encode(result)
}

Password Management


Migrating existing passwords

When integrating NuID into an existing authentication system, you will likely already have passwords stored on user accounts. If your passwords hashes are reversible, then your migration should be a fairly straightforward process of decrypting the password, generating a NuID credential, and storing the 'nu/id' value on the user, disposing of the password afterwards.

It is, however, more likely that your passwords are one-way hashed, thus complicating a migration procedure. While such a data migration is out of scope of this document, we suspect that these migrations would follow a path similar to this:

  1. Registration process is integrated as described above. All new users now have a 'nu/id' credential.
  2. Later, a User attempts login with username/password.
  3. The login submit handler attempts to get a challenge for the user.
    1. If the user already has a 'nu/id', login proceeds according to changes outlined above.
    2. If the user does not have a 'nu/id' value:
      1. Authenticate the user with the username and password as before.
      2. Assuming the authentication was succesful, your server login endpoint should take the password and register a 'nu/id' like your registration endpoint, disposing of the password afterwards.
      3. At some later determined date, remove the extra password handling in the login process and from your database, relying solely on the NuID Auth API for verifying credential challenges. This will prevent any user who has not authenticated successfully after your integration from authenticating correctly. This is ultimately up to your team to decide how best to handle this portion of your user base.

Update Password

To update a current user's credential, follow the steps shown in the registration section above, but in the profile section of the authenticated user's account. It is common during password reset that the user needs to authenticate with their current password, providing two additional fields for the new password and a confirmation of the same. To do this you would follow both the login and registration procedures shown above, combining challenge, verify, and new credential registration.

Forgot Password

To help an unauthenticated user reset their password when they've forgotten it, you'll need a way to authenticate the user without their password, usually through some form of tokenized link sent to the user's email address. When the user clicks the link you would present them with a form to take their new password. Submitting that form would follow the same procedure as registering a new credential and storing it on their user record.

Conclusion


Now that you have a good idea of the integration flow, reading the API Documentation is a great next step. We also have growing Resources documentation for links to libraries, video presentations, blog posts, code examples, etc.. Let us know if we can help.