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,
});