Documentation
Get started
Add to existing project

Add to Existing Project

This tutorial will guide you through the process of setting a client/server authentication flow using Opaque.

Installation

npm install @serenity-kit/opaque

Usage

// server
import * as opaque from "@serenity-kit/opaque";

Opaque loads inlined WebAssembly and therefor won't be ready right on module initialization. Before running any function you need to check that the opaque.ready promise is resolved e.g.

await opaque.ready;
const serverSetup = opaque.server.createSetup();

Note: This is mostly relevant for test environments where you use the functions right away.

Server Setup

The server setup is a one-time operation. It is used to generate the server's long-term private key.

Recommended:

npx @serenity-kit/opaque@latest create-server-setup

The result is a 171 long string. Only store it in a secure location and make sure you have it available in your application e.g. via an environment variable.

const serverSetup = process.env.OPAQUE_SERVER_SETUP;

For development purposes, you can also generate a server setup on the fly:

const serverSetup = opaque.server.createSetup();

Keep in mind that changing the serverSetup will invalidate all existing password files.

Registration Flow

First the client need to generate a registration request using the password. The clientRegistrationState will be used in the next client step to complete the registration. The registrationRequest is sent to the server and usually along with some user identifier e.g. username or email.

// client
const password = "sup-krah.42-UOI"; // user password
 
const { clientRegistrationState, registrationRequest } =
  opaque.client.startRegistration({ password });

The server then generates a registration response using the registrationRequest, the serverSetup and a userIdentifier. The userIdentifier is never exposed to the client and can be anything that uniquely identifies the user e.g. user ID, username, user email and depends on your system. More on this can be the advanced section.

The registrationResponse is sent back to the client.

// server
const userIdentifier = "20e14cd8-ab09-4f4b-87a8-06d2e2e9ff68"; // userId/email/username
 
const { registrationResponse } = opaque.server.createRegistrationResponse({
  serverSetup,
  userIdentifier,
  registrationRequest,
});

The client generates a registration record using the clientRegistrationState from before, the registrationResponse and the password.

// client
const { registrationRecord } = opaque.client.finishRegistration({
  clientRegistrationState,
  registrationResponse,
  password,
});

The registrationRecord then has to be sent again to the server and stored for the user as it is needed for future logins.

Login Flow

In order to authenticate the user, the client needs to generate a login request using the password. The clientLoginState will be used in the next client step to complete the login. The startLoginRequest is sent to the server and usually along with some user identifier e.g. username or email.

// client
const password = "sup-krah.42-UOI"; // user password
 
const { clientLoginState, startLoginRequest } = opaque.client.startLogin({
  password,
});

The server then generates a login response using the startLoginRequest, the serverSetup, the userIdentifier and the registrationRecord. The userIdentifier and the registrationRecord must be the same as in the registration flow.

The generated loginResponse is sent back to the client, but the server must also store the serverLoginState for the next step. This can be done as part of a login attempt in a database or a key value storage like Redis.

// server
const userIdentifier = "20e14cd8-ab09-4f4b-87a8-06d2e2e9ff68"; // userId/email/username
 
const { serverLoginState, loginResponse } = opaque.server.startLogin({
  serverSetup,
  userIdentifier,
  registrationRecord,
  startLoginRequest,
});

The client then generates a finishLoginRequest and sessionKey using the clientLoginState from before, the loginResponse and the password. The sessionKey is a unique per successful login attempt and can be used to authenticate the user after the server completes the login on its side. Therefor the finishLoginRequest is sent back to the server.

// client
const loginResult = opaque.client.finishLogin({
  clientLoginState,
  loginResponse,
  password,
});
if (!loginResult) {
  throw new Error("Login failed");
}
const { finishLoginRequest, sessionKey } = loginResult;

The server then can generate the same sessionKey based on the finishLoginRequest and the previously stored serverLoginState. The sessionKey matches the one generated by the client and can be used to authenticate the user.

// server
const { sessionKey } = opaque.server.finishLogin({
  finishLoginRequest,
  serverLoginState,
});