Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions clients/passkeys-browser/test/ceremonies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
newHttpHandler
} from "./helpers/httpServer";
import * as http from "node:http";
import {RegistrationService} from "./helpers/registration";
import {CeremonyService} from "./helpers/ceremony";
import {FakeAuthenticator} from "./helpers/fakeAuthenticator";

const CREATE_OPTIONS_JSON: PublicKeyCredentialCreationOptionsJson = {
Expand Down Expand Up @@ -158,11 +158,11 @@ describe("PkAuthCeremonyClient http test", () => {

await expect(client.register({ username: "alice", label: "key" })).rejects.toThrow("HTTP 500: unit test error from errorHttpHandler");

expect(credentialsContainer.create).not.toHaveBeenCalled(); // this proves the client did not attempt to create a credential upon failure to start registration.ts
expect(credentialsContainer.create).not.toHaveBeenCalled(); // this proves the client did not attempt to create a credential upon failure to start ceremony.ts
})

it("when start registration succeeds but no credential is created, then the client fails with credential cancellation", async() => {
const registration = new RegistrationService("localhost", "alice", "bob")
const registration = new CeremonyService("localhost", "alice", "bob")
using server= await startHttpServer({
[startPath]: newStartHandler(registration),
[finishPath]: newFinishHandler(registration)
Expand All @@ -178,7 +178,7 @@ describe("PkAuthCeremonyClient http test", () => {
})

it("when finish registration fails, the credential was still created", async() => {
const registration = new RegistrationService("localhost", "alice", "bob")
const registration = new CeremonyService("localhost", "alice", "bob")
using server= await startHttpServer({
[startPath]: newStartHandler(registration),
[finishPath]: errorHttpHandler
Expand All @@ -196,7 +196,7 @@ describe("PkAuthCeremonyClient http test", () => {
})

it("when registering a credential, then the server returns a finished response", async() => {
const registration = new RegistrationService("localhost", "alice", "bob")
const registration = new CeremonyService("localhost", "alice", "bob")
using server = await startHttpServer({
[startPath]: newStartHandler(registration),
[finishPath]: newFinishHandler(registration)
Expand Down Expand Up @@ -237,17 +237,17 @@ describe("PkAuthCeremonyClient http test", () => {
res.end("unit test error from errorHttpHandler")
}

function newStartHandler(registration: RegistrationService) : HttpHandler{
function newStartHandler(registration: CeremonyService) : HttpHandler{
const endpoint = async (req: StartRegistrationRequest) : Promise<StartRegistrationResponse> => {
return registration.start(req);
return registration.startRegistration(req);
};
const handler = new Handler(decodePost, endpoint, encodeJson);
return newHttpHandler(handler);
}

function newFinishHandler(registration: RegistrationService) : HttpHandler{
function newFinishHandler(registration: CeremonyService) : HttpHandler{
const endpoint = async (req: FinishRegistrationRequest) : Promise<FinishRegistrationResponse> => {
return registration.finish(req);
return registration.finishRegistration(req);
};
const handler = new Handler(decodePost, endpoint, encodeJson);
return newHttpHandler(handler);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@ import { Encoder } from "cbor-x";
import * as b64u from "../../src/base64url";
import { decodeCreationOptions, encodeRegistrationResponse } from "../../src/ceremonies";
import { FakeAuthenticator } from "./fakeAuthenticator";
import { RegistrationService } from "./registration";
import { CeremonyService } from "./ceremony";

describe("RegistrationService", () => {
describe("start a registration", () => {
it("returns 403 when a username is not in the allowlist", async () => {
const service = new RegistrationService("localhost", "alice");
const service = new CeremonyService("localhost", "alice");
await expect(
service.start({ username: "eve", displayName: null, label: null, challenge: null }),
service.startRegistration({ username: "eve", displayName: null, label: null, challenge: null }),
).rejects.toThrow("HTTP 403");
});

it("returns a StartRegistrationResponse when the request is valid", async () => {
const service = new RegistrationService("localhost", "alice")
const service = new CeremonyService("localhost", "alice")

const response = await service.start({ username: "alice", displayName: null, label: null, challenge: null })
const response = await service.startRegistration({ username: "alice", displayName: null, label: null, challenge: null })

expect(response.challengeId).toBeTypeOf("string");
expect(response.publicKey.user.id).toBeTypeOf("string");
Expand All @@ -33,9 +33,9 @@ describe("RegistrationService", () => {

describe("finish a registration", () => {
it("returns 400 for an unknown challengeId", async () => {
const service = new RegistrationService("localhost", "alice");
const service = new CeremonyService("localhost", "alice");
await expect(
service.finish({
service.finishRegistration({
challengeId: "does-not-exist",
username: "alice",
label: "key",
Expand All @@ -50,27 +50,27 @@ describe("RegistrationService", () => {
});

it("returns 400 for a username that does not match the started session", async () => {
const service = new RegistrationService("localhost", "alice", "bob");
const { challengeId, publicKey } = await service.start({ username: "alice", displayName: null, label: null, challenge: null });
const service = new CeremonyService("localhost", "alice", "bob");
const { challengeId, publicKey } = await service.startRegistration({ username: "alice", displayName: null, label: null, challenge: null });

const auth = new FakeAuthenticator();
const credential = await auth.create({ publicKey: decodeCreationOptions(publicKey) });
const encoded = encodeRegistrationResponse(credential);

await expect(
service.finish({ challengeId, username: "bob", label: null, response: encoded }),
service.finishRegistration({ challengeId, username: "bob", label: null, response: encoded }),
).rejects.toThrow("HTTP 400");
});

it("verifies the attestation and returns credential metadata", async () => {
const service = new RegistrationService("localhost", "alice");
const { challengeId, publicKey } = await service.start({ username: "alice", displayName: "Alice", label: null, challenge: null });
const service = new CeremonyService("localhost", "alice");
const { challengeId, publicKey } = await service.startRegistration({ username: "alice", displayName: "Alice", label: null, challenge: null });

const auth = new FakeAuthenticator();
const credential = await auth.create({ publicKey: decodeCreationOptions(publicKey) });
const encoded = encodeRegistrationResponse(credential);

const { credential: cred } = await service.finish({ challengeId, username: "alice", label: "my-key", response: encoded });
const { credential: cred } = await service.finishRegistration({ challengeId, username: "alice", label: "my-key", response: encoded });

expect(cred.credentialId).toBeTypeOf("string");
expect(cred.userHandle).toBeTypeOf("string");
Expand All @@ -81,8 +81,8 @@ describe("RegistrationService", () => {
});

it("returns 400 when the attestation signature is tampered", async () => {
const service = new RegistrationService("localhost", "alice");
const { challengeId, publicKey } = await service.start({ username: "alice", displayName: null, label: null, challenge: null });
const service = new CeremonyService("localhost", "alice");
const { challengeId, publicKey } = await service.startRegistration({ username: "alice", displayName: null, label: null, challenge: null });

const auth = new FakeAuthenticator();
const credential = await auth.create({ publicKey: decodeCreationOptions(publicKey) });
Expand All @@ -99,22 +99,22 @@ describe("RegistrationService", () => {
const tampered = { ...encoded, response: { ...encoded.response, attestationObject: b64u.encode(cbor.encode(attObj) as Uint8Array) } };

await expect(
service.finish({ challengeId, username: "alice", label: null, response: tampered }),
service.finishRegistration({ challengeId, username: "alice", label: null, response: tampered }),
).rejects.toThrow("HTTP 400");
});

it("returns 400 when the challenge in clientDataJSON belongs to a different session", async () => {
const service = new RegistrationService("localhost", "alice");
const s1 = await service.start({ username: "alice", displayName: null, label: null, challenge: null });
const s2 = await service.start({ username: "alice", displayName: null, label: null, challenge: null });
const service = new CeremonyService("localhost", "alice");
const s1 = await service.startRegistration({ username: "alice", displayName: null, label: null, challenge: null });
const s2 = await service.startRegistration({ username: "alice", displayName: null, label: null, challenge: null });

const auth = new FakeAuthenticator();
const credential = await auth.create({ publicKey: decodeCreationOptions(s2.publicKey) });
const encoded = encodeRegistrationResponse(credential);

// finish using s1 challengeId but with s2's credential -- this should fail
await expect(
service.finish({ challengeId: s1.challengeId, username: "alice", label: null, response: encoded }),
service.finishRegistration({ challengeId: s1.challengeId, username: "alice", label: null, response: encoded }),
).rejects.toThrow("HTTP 400");
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ interface PendingEntry {
}

/*
RegistrationService is a server side service that can start and finish
client registrations.
CeremonyService is a server side service that can:
- start registration
- finish registration
- start authentication
- finish authentication
Instantiate the service with a relying party id [rpId] and a list of allowed users.
A registration is started with a call to start which will produce a challenge that
the client must respond to with a call to finish.
A registration is started with a call to startRegistration which will produce a challenge that
the client must respond to with a call to finishRegistration.
*/
export class RegistrationService {
export class CeremonyService {
// pending is the map of registrations which have been started; but not yet finished.
// Maps challenge id to a PendingEntry structure.
private readonly pending : Map<string, PendingEntry>;
Expand All @@ -38,7 +41,7 @@ export class RegistrationService {
this.allowedUsernames = allowedUsernames ?? [];
}

async start(req : StartRegistrationRequest) : Promise<StartRegistrationResponse> {
async startRegistration(req : StartRegistrationRequest) : Promise<StartRegistrationResponse> {
if (!this.allowedUsernames.includes(req.username)) {
throw new HttpError(403, { error: `username '${req.username}' not allowed` });
}
Expand All @@ -60,7 +63,7 @@ export class RegistrationService {
return newStartRegistrationResponse(pendingEntry)
}

async finish(req: FinishRegistrationRequest): Promise<FinishRegistrationResponse> {
async finishRegistration(req: FinishRegistrationRequest): Promise<FinishRegistrationResponse> {
const entry = this.pending.get(req.challengeId);
if (!entry) {
throw new HttpError(400, { error: `unknown challengeId: ${req.challengeId}` });
Expand Down
Loading