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.
- clientUser visits your application registration page.
- clientUser enters their email and password and clicks submit. Optionally, you can perform local form validation regarding your own password requirements, if any.
- clientGenerate a verifiedCredential using
const verifiedCredential = Zk.verifiableFromSecret(password)
- clientPost the email and verifiedCredential to your server registration endpoint. Do not send the user's password.
- 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.
- 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.
- 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.
- clientUser visits your application login page without a session.
- clientUser enters their email and password and clicks submit.
- clientPost the email to your server's challenge endpoint, but don't send the password.
- serverLookup the 'nu/id' for that user's email.
- serverGet the credential from the SDK by calling the auth.credentialGet function with the user's nuid.
- serverGet a challenge for the credential from the SDK by calling the auth.challengeGet function with the credential.
- 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.
- serverReturn the challenge JWT to the client.
- clientDecode the challenge JWT claims.
- clientCreate a proof from the password
and challenge JWT claims using
const proof = Zk.proofFromSecretAndChallenge(password, challengeClaims)
- clientSend the challenge JWT and proof back to your server's login endpoint.
- serverPass the challenge JWT and proof to the SDK auth.challengeVerify function.
- serverWhen the SDK returns a successful response, the user has been authenticated correctly, all without sending their password over the network.
- 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:
- Registration process is integrated as described above. All new users now have a 'nu/id' credential.
- Later, a User attempts login with username/password.
- The login submit handler attempts to get a challenge for the user.
- If the user already has a 'nu/id', login proceeds according to changes outlined above.
- If the user does not have a 'nu/id' value:
- Authenticate the user with the username and password as before.
- 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.
- 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.