Skip to content
Open
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
81 changes: 52 additions & 29 deletions apps/server/src/auth/PairingGrantStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,39 +47,56 @@ const layerPairingGrantStore = (
);

it.layer(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))("PairingGrantStore.layer", (it) => {
it.effect("accepts lowercase one-time tokens exactly once", () =>
Effect.gen(function* () {
const bootstrapCredentials = yield* PairingGrantStore.PairingGrantStore;
const issued = yield* bootstrapCredentials.issueOneTimeToken({ label: "Julius iPhone" });
const first = yield* bootstrapCredentials.consume(issued.credential.toLowerCase());
const second = yield* Effect.flip(bootstrapCredentials.consume(issued.credential));

expect(first.method).toBe("one-time-token");
expect(first.scopes).toEqual(AuthStandardClientScopes);
expect(first.subject).toBe("one-time-token");
expect(first.label).toBe("Julius iPhone");
expect(issued.label).toBe("Julius iPhone");
expect(second._tag).toBe("UnknownBootstrapCredentialError");
expect(second.message).toContain("Unknown bootstrap credential");
}).pipe(
Effect.provide(layerPairingGrantStore()),
Effect.provideService(
SecureRandom.SecureRandom,
SecureRandom.make((size) => new Uint8Array(size).fill(8)),
it.effect.each(["aaaaaaaaaaaa", "aaaa-aaaa-aaaa", "aaaa aaaa aaaa", "\taaaa - AAAA - aaaa\n"])(
"accepts formatted one-time tokens exactly once: %j",
(credential) =>
Effect.gen(function* () {
const bootstrapCredentials = yield* PairingGrantStore.PairingGrantStore;
const issued = yield* bootstrapCredentials.issueOneTimeToken({ label: "Julius iPhone" });
for (const invalid of [
`${issued.credential}A`,
issued.credential.slice(1),
issued.credential.replace("A", "I"),
"AAAA/AAAA/AAAA",
"AAAA–AAAA–AAAA",
]) {
const rejected = yield* Effect.flip(bootstrapCredentials.consume(invalid));
expect(rejected._tag).toBe("UnknownBootstrapCredentialError");
}
const first = yield* bootstrapCredentials.consume(credential);
const second = yield* Effect.flip(bootstrapCredentials.consume(issued.credential));

expect(first.method).toBe("one-time-token");
expect(first.scopes).toEqual(AuthStandardClientScopes);
expect(first.subject).toBe("one-time-token");
expect(first.label).toBe("Julius iPhone");
expect(issued.label).toBe("Julius iPhone");
expect(second._tag).toBe("UnknownBootstrapCredentialError");
expect(second.message).toContain("Unknown bootstrap credential");
}).pipe(
Effect.provide(layerPairingGrantStore()),
Effect.provideService(
SecureRandom.SecureRandom,
SecureRandom.make((size) => new Uint8Array(size).fill(8)),
),
),
),
);

it.effect("atomically consumes a one-time token when multiple requests race", () =>
Effect.gen(function* () {
const bootstrapCredentials = yield* PairingGrantStore.PairingGrantStore;
const token = yield* bootstrapCredentials.issueOneTimeToken();
const results = yield* Effect.all(
Array.from({ length: 8 }, () =>
Array.from({ length: 8 }, (_, index) =>
Effect.result(
bootstrapCredentials.consume(token.credential, {
requestedScopes: ["orchestration:read"],
}),
bootstrapCredentials.consume(
index % 2 === 0
? token.credential
: `${token.credential.slice(0, 4)}-${token.credential.slice(4, 8)}-${token.credential.slice(8)}`,
{
requestedScopes: ["orchestration:read"],
},
),
),
),
{
Expand All @@ -99,28 +116,32 @@ it.layer(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))("PairingGrantStor
}).pipe(Effect.provide(layerPairingGrantStore())),
);

it.effect("requires the bound proof key thumbprint when present", () =>
it.effect.each(["raw", "formatted"])("requires the bound proof key for %s codes", (format) =>
Effect.gen(function* () {
const bootstrapCredentials = yield* PairingGrantStore.PairingGrantStore;
const token = yield* bootstrapCredentials.issueOneTimeToken({
proofKeyThumbprint: "client-proof-key-thumbprint",
scopes: ["orchestration:read"],
});
const credential =
format === "formatted"
? `${token.credential.slice(0, 4)}-${token.credential.slice(4, 8)}-${token.credential.slice(8)}`
: token.credential;

const missing = yield* Effect.flip(bootstrapCredentials.consume(token.credential));
const missing = yield* Effect.flip(bootstrapCredentials.consume(credential));
const wrong = yield* Effect.flip(
bootstrapCredentials.consume(token.credential, {
bootstrapCredentials.consume(credential, {
proofKeyThumbprint: "other-proof-key-thumbprint",
requestedScopes: ["access:write"],
}),
);
const forbiddenScope = yield* Effect.flip(
bootstrapCredentials.consume(token.credential, {
bootstrapCredentials.consume(credential, {
proofKeyThumbprint: "client-proof-key-thumbprint",
requestedScopes: ["access:write"],
}),
);
const consumed = yield* bootstrapCredentials.consume(token.credential, {
const consumed = yield* bootstrapCredentials.consume(credential, {
proofKeyThumbprint: "client-proof-key-thumbprint",
requestedScopes: ["orchestration:read"],
});
Expand All @@ -137,9 +158,11 @@ it.layer(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))("PairingGrantStor
const bootstrapCredentials = yield* PairingGrantStore.PairingGrantStore;
const exact = yield* bootstrapCredentials.consume("abcdefghjkmn");
const otherCase = yield* Effect.flip(bootstrapCredentials.consume("ABCDEFGHJKMN"));
const formatted = yield* Effect.flip(bootstrapCredentials.consume("abcd-efgh-jkmn"));

expect(exact.method).toBe("desktop-bootstrap");
expect(otherCase._tag).toBe("UnknownBootstrapCredentialError");
expect(formatted._tag).toBe("UnknownBootstrapCredentialError");
}).pipe(
Effect.provide(
layerPairingGrantStore({
Expand Down
7 changes: 4 additions & 3 deletions apps/server/src/auth/PairingGrantStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,9 +546,10 @@ const make = Effect.gen(function* () {
return yield* seededResult.error;
}

// Only credentials in the issued pairing-code format are case-insensitive.
const pairingCredential = PAIRING_TOKEN_PATTERN.test(credential)
? credential.toUpperCase()
// Only issued pairing codes tolerate case and presentation separators.
const unformattedCredential = credential.replace(/[-\s]/g, "");
const pairingCredential = PAIRING_TOKEN_PATTERN.test(unformattedCredential)
? unformattedCredential.toUpperCase()
: credential;

// Check scopes in the UPDATE so rejected requests cannot spend one-time links.
Expand Down
Loading