From f5d3b7b9e97bc60c499bf0d7bb112e7307d57ad5 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Fri, 9 Aug 2024 14:30:30 -0700 Subject: [PATCH 01/14] Install SimpleWebAuthn --- package-lock.json | 176 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 + 2 files changed, 176 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2610572..c8b6359 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "demo-rp-api", "version": "0.0.0", "dependencies": { + "@simplewebauthn/server": "^10.0.1", "hono": "^4.5.4", "zod": "^3.23.8", "zod-form-data": "^2.0.2" @@ -15,6 +16,7 @@ "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.4.5", "@cloudflare/workers-types": "^4.20240729.0", + "@simplewebauthn/types": "^10.0.0", "typescript": "^5.5.2", "vitest": "1.5.0", "wrangler": "^3.70.0" @@ -599,6 +601,11 @@ "node": ">=14" } }, + "node_modules/@hexagon/base64": { + "version": "1.1.28", + "resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz", + "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==" + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -636,6 +643,65 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@levischuck/tiny-cbor": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.2.tgz", + "integrity": "sha512-f5CnPw997Y2GQ8FAvtuVVC19FX8mwNNC+1XJcIi16n/LTJifKO6QBgGLgN3YEmqtGMk17SKSuoWES3imJVxAVw==" + }, + "node_modules/@peculiar/asn1-android": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.3.13.tgz", + "integrity": "sha512-0VTNazDGKrLS6a3BwTDZanqq6DR/I3SbvmDMuS8Be+OYpvM6x1SRDh9AGDsHVnaCOIztOspCPc6N1m+iUv1Xxw==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.13", + "asn1js": "^3.0.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.13.tgz", + "integrity": "sha512-3dF2pQcrN/WJEMq+9qWLQ0gqtn1G81J4rYqFl6El6QV367b4IuhcRv+yMA84tNNyHOJn9anLXV5radnpPiG3iA==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.13", + "@peculiar/asn1-x509": "^2.3.13", + "asn1js": "^3.0.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.3.13.tgz", + "integrity": "sha512-wBNQqCyRtmqvXkGkL4DR3WxZhHy8fDiYtOjTeCd7SFE5F6GBeafw3EJ94PX/V0OJJrjQ40SkRY2IZu3ZSyBqcg==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.13", + "@peculiar/asn1-x509": "^2.3.13", + "asn1js": "^3.0.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.13.tgz", + "integrity": "sha512-3Xq3a01WkHRZL8X04Zsfg//mGaA21xlL4tlVn4v2xGT0JStiztATRkMwa5b+f/HXmY2smsiLXYK46Gwgzvfg3g==", + "dependencies": { + "asn1js": "^3.0.5", + "pvtsutils": "^1.3.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.3.13.tgz", + "integrity": "sha512-PfeLQl2skXmxX2/AFFCVaWU8U6FKW1Db43mgBhShCOFS1bVxqtvusq1hVjfuEcuSQGedrLdCSvTgabluwN/M9A==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.13", + "asn1js": "^3.0.5", + "ipaddr.js": "^2.1.0", + "pvtsutils": "^1.3.5", + "tslib": "^2.6.2" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.20.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz", @@ -844,6 +910,30 @@ "win32" ] }, + "node_modules/@simplewebauthn/server": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-10.0.1.tgz", + "integrity": "sha512-djNWcRn+H+6zvihBFJSpG3fzb0NQS9c/Mw5dYOtZ9H+oDw8qn9Htqxt4cpqRvSOAfwqP7rOvE9rwqVaoGGc3hg==", + "dependencies": { + "@hexagon/base64": "^1.1.27", + "@levischuck/tiny-cbor": "^0.2.2", + "@peculiar/asn1-android": "^2.3.10", + "@peculiar/asn1-ecc": "^2.3.8", + "@peculiar/asn1-rsa": "^2.3.8", + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/asn1-x509": "^2.3.8", + "@simplewebauthn/types": "^10.0.0", + "cross-fetch": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@simplewebauthn/types": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-10.0.0.tgz", + "integrity": "sha512-SFXke7xkgPRowY2E+8djKbdEznTVnD5R6GO7GPTthpHrokLvNKw8C3lFZypTxLI7KkCfGPfhtqB3d7OVGGa9jQ==" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1019,6 +1109,19 @@ "printable-characters": "^1.0.42" } }, + "node_modules/asn1js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", + "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", + "dependencies": { + "pvtsutils": "^1.3.2", + "pvutils": "^1.1.3", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -1170,6 +1273,14 @@ "node": ">= 0.6" } }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1456,6 +1567,14 @@ "node": ">=16.17.0" } }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "engines": { + "node": ">= 10" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1672,6 +1791,25 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-fetch-native": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", @@ -1866,6 +2004,22 @@ "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", "dev": true }, + "node_modules/pvtsutils": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.5.tgz", + "integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==", + "dependencies": { + "tslib": "^2.6.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -2189,11 +2343,15 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "node_modules/type-detect": { "version": "4.1.0", @@ -2831,6 +2989,20 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index e5e0c00..fa49c8e 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,13 @@ "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.4.5", "@cloudflare/workers-types": "^4.20240729.0", + "@simplewebauthn/types": "^10.0.0", "typescript": "^5.5.2", "vitest": "1.5.0", "wrangler": "^3.70.0" }, "dependencies": { + "@simplewebauthn/server": "^10.0.1", "hono": "^4.5.4", "zod": "^3.23.8", "zod-form-data": "^2.0.2" From f4f60a39deb6665bb1ff9f2f13ae44a7f662c38a Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Fri, 9 Aug 2024 14:49:21 -0700 Subject: [PATCH 02/14] Add RP ID and name env vars --- wrangler.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/wrangler.toml b/wrangler.toml index 4b13bc9..d076878 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -16,8 +16,9 @@ compatibility_flags = ["nodejs_compat"] # - https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables # Note: Use secrets to store sensitive data. # - https://developers.cloudflare.com/workers/configuration/secrets/ -# [vars] -# MY_VARIABLE = "production_value" +[vars] +RP_ID = "passkeys.dev" +RP_NAME = "passkeys.dev" # Bind the Workers AI model catalog. Run machine learning models, powered by serverless GPUs, on Cloudflare’s global network # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#workers-ai From 0f4d11d194f75bd303f89d1617ccd10dbe81acc9 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Fri, 9 Aug 2024 14:49:37 -0700 Subject: [PATCH 03/14] Build out registration options schema --- src/schemas.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/schemas.ts b/src/schemas.ts index bd794ca..1bcd2ec 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -10,7 +10,13 @@ export { ZodError } from 'zod'; * Schema for incoming query params to configure registration options */ export const regOptionsInputSchema = z.object({ - foo: z.string().optional(), + username: z.string(), + userVerification: z.enum(['discouraged', 'preferred', 'required']).default('preferred'), + attestation: z.enum(['none', 'direct']).default('none'), + attachment: z.enum(['cross-platform', 'platform']).optional(), + algES256: z.boolean().default(true), + algRS256: z.boolean().default(true), + discoverableCredential: z.enum(['discouraged', 'preferred', 'required']).default('required'), }); From 51a737b98bdd596b974d22012105cb9b9a5764c2 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Fri, 9 Aug 2024 14:50:49 -0700 Subject: [PATCH 04/14] Generate options from parsed input --- src/handlers/handleCreateRegOptions.ts | 55 ++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/src/handlers/handleCreateRegOptions.ts b/src/handlers/handleCreateRegOptions.ts index 76d7e37..a708681 100644 --- a/src/handlers/handleCreateRegOptions.ts +++ b/src/handlers/handleCreateRegOptions.ts @@ -1,14 +1,61 @@ import { Context } from 'hono'; +import { HTTPException } from 'hono/http-exception'; import { zfd } from 'zod-form-data'; +import { generateRegistrationOptions } from '@simplewebauthn/server'; +import { cose } from '@simplewebauthn/server/helpers'; -import { regOptionsInputSchema } from '../schemas'; +import { ZodError, regOptionsInputSchema } from '../schemas'; /** * Generate registration options */ export async function handleCreateRegOptions(context: Context): Promise { - const parsedInput = zfd.formData(regOptionsInputSchema).parse(context.req.query()); - console.log(parsedInput); - return context.text(`handleCreateRegOptions ${JSON.stringify(parsedInput)}`); + let parsedInput; + try { + parsedInput = zfd.formData(regOptionsInputSchema).parse(context.req.query()); + } catch (err) { + const _err = err as ZodError; + throw new HTTPException(400, { message: JSON.stringify(_err.errors) }); + } + + const { + algES256, + algRS256, + attestation, + discoverableCredential, + userVerification, + username, + attachment, + } = parsedInput; + + const { RP_ID, RP_NAME } = context.env; + + let supportedAlgorithmIDs = undefined; + if (algES256 || algRS256) { + supportedAlgorithmIDs = []; + + if (algES256) { + supportedAlgorithmIDs.push(cose.COSEALG.ES256); + } + + if (algRS256) { + supportedAlgorithmIDs.push(cose.COSEALG.RS256); + } + } + + const opts = await generateRegistrationOptions({ + rpID: RP_ID, + rpName: RP_NAME, + userName: username, + attestationType: attestation, + authenticatorSelection: { + authenticatorAttachment: attachment, + residentKey: discoverableCredential, + userVerification, + }, + supportedAlgorithmIDs, + }); + + return context.json(opts); } From f858b394a1e89a8248191555e0e240e35c93700f Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Fri, 9 Aug 2024 15:44:08 -0700 Subject: [PATCH 05/14] Shut wrangler up about compatibility date --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index d076878..39fb970 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -1,7 +1,7 @@ #:schema node_modules/wrangler/config-schema.json name = "demo-rp-api" main = "src/index.ts" -compatibility_date = "2024-07-29" +compatibility_date = "2024-07-25" compatibility_flags = ["nodejs_compat"] # Automatically place your workloads in an optimal location to minimize latency. From 2fdc2f18e9738f22f933dd897fdbe9e0640a6f95 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Fri, 9 Aug 2024 16:03:35 -0700 Subject: [PATCH 06/14] Update @cloudflare/vitest-pool-workers --- package-lock.json | 233 +++++++--------------------------------------- 1 file changed, 32 insertions(+), 201 deletions(-) diff --git a/package-lock.json b/package-lock.json index c8b6359..b00c9cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,18 +35,18 @@ } }, "node_modules/@cloudflare/vitest-pool-workers": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.4.17.tgz", - "integrity": "sha512-ddSKdWvYCdQ8p01NF9iIjOMDQVqexWcSFG38EWhSJeNuPiiYE0/fjV2pngjtrkRWk47CDpHg166W0XblPxPfXw==", + "version": "0.4.21", + "resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.4.21.tgz", + "integrity": "sha512-CbETvL1rdmVF04aFlMVg13a0D9pWmSTt3+ena3bLgl5EjebwGQVFk9Qjrl76OemDc9hP7PJtsb0WCmEcDYeV2A==", "dev": true, "dependencies": { "birpc": "0.2.14", "cjs-module-lexer": "^1.2.3", "devalue": "^4.3.0", "esbuild": "0.17.19", - "miniflare": "3.20240725.0", + "miniflare": "3.20240806.0", "semver": "^7.5.1", - "wrangler": "3.68.0", + "wrangler": "3.70.0", "zod": "^3.22.3" }, "peerDependencies": { @@ -55,53 +55,10 @@ "vitest": "1.3.x - 1.5.x" } }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/wrangler": { - "version": "3.68.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.68.0.tgz", - "integrity": "sha512-gsIeglkh5nOn1mHJs0bf1pOq/DvIt+umjO/5a867IYYXaN4j/ar5cRR1+F5ue3S7uEjYCLIZZjs8ESiPTSEt+Q==", - "dev": true, - "dependencies": { - "@cloudflare/kv-asset-handler": "0.3.4", - "@esbuild-plugins/node-globals-polyfill": "^0.2.3", - "@esbuild-plugins/node-modules-polyfill": "^0.2.2", - "blake3-wasm": "^2.1.5", - "chokidar": "^3.5.3", - "date-fns": "^3.6.0", - "esbuild": "0.17.19", - "miniflare": "3.20240725.0", - "nanoid": "^3.3.3", - "path-to-regexp": "^6.2.0", - "resolve": "^1.22.8", - "resolve.exports": "^2.0.2", - "selfsigned": "^2.0.1", - "source-map": "^0.6.1", - "unenv": "npm:unenv-nightly@1.10.0-1717606461.a117952", - "workerd": "1.20240725.0", - "xxhash-wasm": "^1.0.1" - }, - "bin": { - "wrangler": "bin/wrangler.js", - "wrangler2": "bin/wrangler.js" - }, - "engines": { - "node": ">=16.17.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@cloudflare/workers-types": "^4.20240725.0" - }, - "peerDependenciesMeta": { - "@cloudflare/workers-types": { - "optional": true - } - } - }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20240725.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20240725.0.tgz", - "integrity": "sha512-KpE7eycdZ9ON+tKBuTyqZh8SdFWHGrh2Ru9LcbpeFwb7O9gDQv9ceSdoV/T598qlT0a0yVKM62R6xa5ec0UOWA==", + "version": "1.20240806.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20240806.0.tgz", + "integrity": "sha512-FqcVBBCO//I39K5F+HqE/v+UkqY1UrRnS653Jv+XsNNH9TpX5fTs7VCKG4kDSnmxlAaKttyIN5sMEt7lpuNExQ==", "cpu": [ "x64" ], @@ -115,9 +72,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20240725.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20240725.0.tgz", - "integrity": "sha512-/UQlI04FdXLpPlDzzsWGz8TuKrMZKLowTo+8PkxgEiWIaBhE4DIDM5bwj3jM4Bs8yOLiL2ovQNpld5CnAKqA8g==", + "version": "1.20240806.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20240806.0.tgz", + "integrity": "sha512-8c3KvmzYp/wg+82KHSOzDetJK+pThH4MTrU1OsjmsR2cUfedm5dk5Lah9/0Ld68+6A0umFACi4W2xJHs/RoBpA==", "cpu": [ "arm64" ], @@ -131,9 +88,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20240725.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20240725.0.tgz", - "integrity": "sha512-Z5t12qYLvHz0b3ZRBBm2HQ93RiHrAnjFfdhtjMcgJypAGkiWpOCEn2xar/WqDhMfqnk0sa8aYiYAbMAlP1WN6w==", + "version": "1.20240806.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20240806.0.tgz", + "integrity": "sha512-/149Bpxw4e2p5QqnBc06g0mx+4sZYh9j0doilnt0wk/uqYkLp0DdXGMQVRB74sBLg2UD3wW8amn1w3KyFhK2tQ==", "cpu": [ "x64" ], @@ -147,9 +104,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20240725.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20240725.0.tgz", - "integrity": "sha512-j9gYXLOwOyNehLMzP7KxQ+Y6/nxcL9i6LTDJC6RChoaxLRbm0Y/9Otu+hfyzeNeRpt31ip6vqXZ1QQk6ygzI8A==", + "version": "1.20240806.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20240806.0.tgz", + "integrity": "sha512-lacDWY3S1rKL/xT6iMtTQJEKmTTKrBavPczInEuBFXElmrS6IwVjZwv8hhVm32piyNt/AuFu9BYoJALi9D85/g==", "cpu": [ "arm64" ], @@ -163,9 +120,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20240725.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20240725.0.tgz", - "integrity": "sha512-fkrJLWNN6rrPjZ0eKJx328NVMo4BsainKxAfqaPMEd6uRwjOM8uN8V4sSLsXXP8GQMAx6hAG2hU86givS4GItg==", + "version": "1.20240806.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20240806.0.tgz", + "integrity": "sha512-hC6JEfTSQK6//Lg+D54TLVn1ceTPY+fv4MXqDZIYlPP53iN+dL8Xd0utn2SG57UYdlL5FRAhm/EWHcATZg1RgA==", "cpu": [ "x64" ], @@ -1721,9 +1678,9 @@ } }, "node_modules/miniflare": { - "version": "3.20240725.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20240725.0.tgz", - "integrity": "sha512-n9NTLI8J9Xt0Cls6dRpqoIPkVFnxD9gMnU/qDkDX9diKfN16HyxpAdA5mto/hKuRpjW19TxnTMcxBo90vZXemw==", + "version": "3.20240806.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20240806.0.tgz", + "integrity": "sha512-jDsXBJOLUVpIQXHsluX3xV0piDxXolTCsxdje2Ex2LTC9PsSoBIkMwvCmnCxe9wpJJCq8rb0UMyeEn3KOF3LOw==", "dev": true, "dependencies": { "@cspotcode/source-map-support": "0.8.1", @@ -1734,7 +1691,7 @@ "glob-to-regexp": "^0.4.1", "stoppable": "^1.1.0", "undici": "^5.28.4", - "workerd": "1.20240725.0", + "workerd": "1.20240806.0", "ws": "^8.17.1", "youch": "^3.2.2", "zod": "^3.22.3" @@ -3035,9 +2992,9 @@ } }, "node_modules/workerd": { - "version": "1.20240725.0", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20240725.0.tgz", - "integrity": "sha512-VZwgejRcHsQ9FEPtc7v25ebINLAR+stL3q1hC1xByE+quskdoWpTXHkZwZ3IdSgvm9vPVbCbJw9p5mGnDByW2A==", + "version": "1.20240806.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20240806.0.tgz", + "integrity": "sha512-yyNtyzTMgVY0sgYijHBONqZFVXsOFGj2jDjS8MF/RbO2ZdGROvs4Hkc/9QnmqFWahE0STxXeJ1yW1yVotdF0UQ==", "dev": true, "hasInstallScript": true, "bin": { @@ -3047,11 +3004,11 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20240725.0", - "@cloudflare/workerd-darwin-arm64": "1.20240725.0", - "@cloudflare/workerd-linux-64": "1.20240725.0", - "@cloudflare/workerd-linux-arm64": "1.20240725.0", - "@cloudflare/workerd-windows-64": "1.20240725.0" + "@cloudflare/workerd-darwin-64": "1.20240806.0", + "@cloudflare/workerd-darwin-arm64": "1.20240806.0", + "@cloudflare/workerd-linux-64": "1.20240806.0", + "@cloudflare/workerd-linux-arm64": "1.20240806.0", + "@cloudflare/workerd-windows-64": "1.20240806.0" } }, "node_modules/wrangler": { @@ -3098,132 +3055,6 @@ } } }, - "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20240806.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20240806.0.tgz", - "integrity": "sha512-FqcVBBCO//I39K5F+HqE/v+UkqY1UrRnS653Jv+XsNNH9TpX5fTs7VCKG4kDSnmxlAaKttyIN5sMEt7lpuNExQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20240806.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20240806.0.tgz", - "integrity": "sha512-8c3KvmzYp/wg+82KHSOzDetJK+pThH4MTrU1OsjmsR2cUfedm5dk5Lah9/0Ld68+6A0umFACi4W2xJHs/RoBpA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20240806.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20240806.0.tgz", - "integrity": "sha512-/149Bpxw4e2p5QqnBc06g0mx+4sZYh9j0doilnt0wk/uqYkLp0DdXGMQVRB74sBLg2UD3wW8amn1w3KyFhK2tQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20240806.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20240806.0.tgz", - "integrity": "sha512-lacDWY3S1rKL/xT6iMtTQJEKmTTKrBavPczInEuBFXElmrS6IwVjZwv8hhVm32piyNt/AuFu9BYoJALi9D85/g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/wrangler/node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20240806.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20240806.0.tgz", - "integrity": "sha512-hC6JEfTSQK6//Lg+D54TLVn1ceTPY+fv4MXqDZIYlPP53iN+dL8Xd0utn2SG57UYdlL5FRAhm/EWHcATZg1RgA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/wrangler/node_modules/miniflare": { - "version": "3.20240806.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20240806.0.tgz", - "integrity": "sha512-jDsXBJOLUVpIQXHsluX3xV0piDxXolTCsxdje2Ex2LTC9PsSoBIkMwvCmnCxe9wpJJCq8rb0UMyeEn3KOF3LOw==", - "dev": true, - "dependencies": { - "@cspotcode/source-map-support": "0.8.1", - "acorn": "^8.8.0", - "acorn-walk": "^8.2.0", - "capnp-ts": "^0.7.0", - "exit-hook": "^2.2.1", - "glob-to-regexp": "^0.4.1", - "stoppable": "^1.1.0", - "undici": "^5.28.4", - "workerd": "1.20240806.0", - "ws": "^8.17.1", - "youch": "^3.2.2", - "zod": "^3.22.3" - }, - "bin": { - "miniflare": "bootstrap.js" - }, - "engines": { - "node": ">=16.13" - } - }, - "node_modules/wrangler/node_modules/workerd": { - "version": "1.20240806.0", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20240806.0.tgz", - "integrity": "sha512-yyNtyzTMgVY0sgYijHBONqZFVXsOFGj2jDjS8MF/RbO2ZdGROvs4Hkc/9QnmqFWahE0STxXeJ1yW1yVotdF0UQ==", - "dev": true, - "hasInstallScript": true, - "bin": { - "workerd": "bin/workerd" - }, - "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20240806.0", - "@cloudflare/workerd-darwin-arm64": "1.20240806.0", - "@cloudflare/workerd-linux-64": "1.20240806.0", - "@cloudflare/workerd-linux-arm64": "1.20240806.0", - "@cloudflare/workerd-windows-64": "1.20240806.0" - } - }, "node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", From ad7834c1f8c4af642e33ed9b63427d664d0a1f81 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Fri, 9 Aug 2024 16:05:47 -0700 Subject: [PATCH 07/14] Fix tests --- test/index.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/index.spec.ts b/test/index.spec.ts index 8329acd..f19eb08 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -24,7 +24,7 @@ describe('Routing tests', () => { // }); it('recognizes GET /registration/options', async () => { - const response = await SELF.fetch('https://example.com/registration/options', { method: 'GET' }); + const response = await SELF.fetch('https://example.com/registration/options?username=foo', { method: 'GET' }); expect(response.status).toBe(200); }); From ec20ff0c2efe178b16777d1c34c00a56f675f47e Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Fri, 9 Aug 2024 16:29:35 -0700 Subject: [PATCH 08/14] Add initial registration options tests --- test/index.spec.ts | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/index.spec.ts b/test/index.spec.ts index f19eb08..1ade911 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1,6 +1,7 @@ // test/index.spec.ts import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test'; import { describe, it, expect } from 'vitest'; +import { PublicKeyCredentialCreationOptionsJSON } from '@simplewebauthn/types'; // import worker from '../src/index'; // For now, you'll need to do something like this to get a correctly-typed @@ -48,3 +49,38 @@ describe('Routing tests', () => { expect(response.status).toBe(404); }); }); + +describe('registration options', () => { + it('should require username', async () => { + const response = await SELF.fetch('https://example.com/registration/options', { method: 'GET' }); + expect(response.status).toBe(400); + expect(await response.text()).toMatch("username"); + }); + + it('should generate basic options', async () => { + const username = 'mmiller'; + const response = await SELF.fetch(`https://example.com/registration/options?username=${username}`, { method: 'GET' }); + expect(response.status).toBe(200); + expect(response.headers.get('Content-Type')).toMatch('application/json'); + + const opts = await response.json() as PublicKeyCredentialCreationOptionsJSON; + + expect(opts.challenge).toBeTypeOf('string'); + expect(opts.rp.name).toEqual('passkeys.dev'); + expect(opts.rp.id).toEqual('passkeys.dev'); + expect(opts.user.id).toBeTypeOf('string'); + expect(opts.user.name).toEqual(username); + expect(opts.user.displayName).toEqual(''); + expect(opts.pubKeyCredParams).toEqual([ + { "alg": -7, "type": "public-key" }, + { "alg": -257, "type": "public-key" }, + ]); + expect(opts.timeout).toEqual(60000); + expect(opts.attestation).toEqual('none'); + expect(opts.excludeCredentials).toEqual([]); + expect(opts.authenticatorSelection?.residentKey).toEqual('required'); + expect(opts.authenticatorSelection?.userVerification).toEqual('preferred'); + expect(opts.authenticatorSelection?.requireResidentKey).toEqual(true); + expect(opts.extensions?.credProps).toEqual(true); + }); +}); From f46ca01a87e703f041ecb57ccb2c65fd910a5ee8 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Sat, 10 Aug 2024 10:56:23 -0700 Subject: [PATCH 09/14] Convert query param string to actual booleans --- src/schemas.ts | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/schemas.ts b/src/schemas.ts index 1bcd2ec..0b793b6 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -6,6 +6,35 @@ import { z } from 'zod'; */ export { ZodError } from 'zod'; + +/** + * Zod preprocessors + */ + +/** + * Boolean values in query params are actually strings, so coerce them to proper booleans + */ +function castToBoolean() { + return z.preprocess((value) => { + if (typeof value === 'boolean') { + return value; + } + + const _valLower = String(value).toLowerCase(); + + if (_valLower === 'true') { + return true; + } + + if (_valLower === 'false') { + return false; + } + + return value; + }, z.boolean()); +} + + /** * Schema for incoming query params to configure registration options */ @@ -14,8 +43,8 @@ export const regOptionsInputSchema = z.object({ userVerification: z.enum(['discouraged', 'preferred', 'required']).default('preferred'), attestation: z.enum(['none', 'direct']).default('none'), attachment: z.enum(['cross-platform', 'platform']).optional(), - algES256: z.boolean().default(true), - algRS256: z.boolean().default(true), + algES256: castToBoolean().default(true), + algRS256: castToBoolean().default(true), discoverableCredential: z.enum(['discouraged', 'preferred', 'required']).default('required'), }); From b76b4db037ffab9db37b0bfa2c0340d035b73364 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Sat, 10 Aug 2024 10:56:47 -0700 Subject: [PATCH 10/14] Simplify pubKeyCredParams configuration --- src/handlers/handleCreateRegOptions.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/handlers/handleCreateRegOptions.ts b/src/handlers/handleCreateRegOptions.ts index a708681..06fa3f1 100644 --- a/src/handlers/handleCreateRegOptions.ts +++ b/src/handlers/handleCreateRegOptions.ts @@ -11,6 +11,7 @@ import { ZodError, regOptionsInputSchema } from '../schemas'; * Generate registration options */ export async function handleCreateRegOptions(context: Context): Promise { + // Parse options from query params let parsedInput; try { parsedInput = zfd.formData(regOptionsInputSchema).parse(context.req.query()); @@ -31,17 +32,14 @@ export async function handleCreateRegOptions(context: Context): Promise Date: Sat, 10 Aug 2024 11:17:46 -0700 Subject: [PATCH 11/14] Add test cases --- test/index.spec.ts | 151 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 144 insertions(+), 7 deletions(-) diff --git a/test/index.spec.ts b/test/index.spec.ts index 1ade911..2e5d6da 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -25,12 +25,12 @@ describe('Routing tests', () => { // }); it('recognizes GET /registration/options', async () => { - const response = await SELF.fetch('https://example.com/registration/options?username=foo', { method: 'GET' }); + const response = await SELF.fetch('https://example.com/registration/options?username=foo'); expect(response.status).toBe(200); }); it('recognizes GET /authentication/options', async () => { - const response = await SELF.fetch('https://example.com/authentication/options', { method: 'GET' }); + const response = await SELF.fetch('https://example.com/authentication/options'); expect(response.status).toBe(200); }); @@ -50,16 +50,16 @@ describe('Routing tests', () => { }); }); -describe('registration options', () => { - it('should require username', async () => { - const response = await SELF.fetch('https://example.com/registration/options', { method: 'GET' }); +describe('Registration options', () => { + it('requires username', async () => { + const response = await SELF.fetch('https://example.com/registration/options'); expect(response.status).toBe(400); expect(await response.text()).toMatch("username"); }); - it('should generate basic options', async () => { + it('generates basic options', async () => { const username = 'mmiller'; - const response = await SELF.fetch(`https://example.com/registration/options?username=${username}`, { method: 'GET' }); + const response = await SELF.fetch(`https://example.com/registration/options?username=${username}`); expect(response.status).toBe(200); expect(response.headers.get('Content-Type')).toMatch('application/json'); @@ -83,4 +83,141 @@ describe('registration options', () => { expect(opts.authenticatorSelection?.requireResidentKey).toEqual(true); expect(opts.extensions?.credProps).toEqual(true); }); + + describe('Param: algES256', () => { + it('omits ES256 when param is false', async () => { + const response = await SELF.fetch( + 'https://example.com/registration/options?username=mmiller&algES256=false', + ); + + const opts = await response.json() as PublicKeyCredentialCreationOptionsJSON; + + expect(opts.pubKeyCredParams).toEqual([ + { "alg": -257, "type": "public-key" }, + ]) + }); + + it('errors on bad param value', async () => { + const response = await SELF.fetch( + 'https://example.com/registration/options?username=mmiller&algES256=maru', + { method: 'GET' }, + ); + expect(response.status).toBe(400); + expect(await response.text()).toMatch("algES256"); + }); + }); + + describe('Param: algRS256', () => { + it('omits RS256 when param is false', async () => { + const response = await SELF.fetch( + 'https://example.com/registration/options?username=mmiller&algRS256=false', + ); + + const opts = await response.json() as PublicKeyCredentialCreationOptionsJSON; + + expect(opts.pubKeyCredParams).toEqual([ + { "alg": -7, "type": "public-key" }, + ]); + }); + + it('errors on bad param value', async () => { + const response = await SELF.fetch( + 'https://example.com/registration/options?username=mmiller&algRS256=batsu', + ); + expect(response.status).toBe(400); + expect(await response.text()).toMatch("algRS256"); + }); + }); + + describe('Param: attestation', () => { + it.each(['none', 'direct'])('supports value "%s"', async (val) => { + const response = await SELF.fetch( + `https://example.com/registration/options?username=mmiller&attestation=${val}`, + ); + + const opts = await response.json() as PublicKeyCredentialCreationOptionsJSON; + + expect(opts.attestation).toEqual(val); + }); + + it('errors on bad param value', async () => { + const response = await SELF.fetch( + 'https://example.com/registration/options?username=mmiller&attestation=sometimes', + ); + expect(response.status).toBe(400); + expect(await response.text()).toMatch("attestation"); + }); + }); + + describe('Param: discoverableCredential', () => { + it.each([ + 'discouraged', + 'preferred', + 'required', + ])('supports value "%s"', async (val) => { + const response = await SELF.fetch( + `https://example.com/registration/options?username=mmiller&discoverableCredential=${val}`, + ); + + const opts = await response.json() as PublicKeyCredentialCreationOptionsJSON; + + expect(opts.authenticatorSelection?.residentKey).toEqual(val); + expect(opts.authenticatorSelection?.requireResidentKey).toEqual(val === 'required'); + }); + + it('errors on bad param value', async () => { + const response = await SELF.fetch( + 'https://example.com/registration/options?username=mmiller&discoverableCredential=sure', + ); + expect(response.status).toBe(400); + expect(await response.text()).toMatch("discoverableCredential"); + }); + }); + + describe('Param: userVerification', () => { + it.each([ + 'discouraged', + 'preferred', + 'required', + ])('supports value "%s"', async (val) => { + const response = await SELF.fetch( + `https://example.com/registration/options?username=mmiller&userVerification=${val}`, + ); + + const opts = await response.json() as PublicKeyCredentialCreationOptionsJSON; + + expect(opts.authenticatorSelection?.userVerification).toEqual(val); + }); + + it('errors on bad param value', async () => { + const response = await SELF.fetch( + 'https://example.com/registration/options?username=mmiller&userVerification=yesButNotCached', + ); + expect(response.status).toBe(400); + expect(await response.text()).toMatch("userVerification"); + }); + }); + + describe('Param: attachment', () => { + it.each([ + 'cross-platform', + 'platform', + ])('supports value "%s"', async (val) => { + const response = await SELF.fetch( + `https://example.com/registration/options?username=mmiller&attachment=${val}`, + ); + + const opts = await response.json() as PublicKeyCredentialCreationOptionsJSON; + + expect(opts.authenticatorSelection?.authenticatorAttachment).toEqual(val); + }); + + it('errors on bad param value', async () => { + const response = await SELF.fetch( + 'https://example.com/registration/options?username=mmiller&attachment=hybrid', + ); + expect(response.status).toBe(400); + expect(await response.text()).toMatch("attachment"); + }); + }); }); From 9b1e06c010ffe68b3c66c7577d69f7f196b6b061 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Sat, 10 Aug 2024 11:46:38 -0700 Subject: [PATCH 12/14] Change username to userName --- src/handlers/handleCreateRegOptions.ts | 4 ++-- src/schemas.ts | 2 +- test/index.spec.ts | 32 +++++++++++++------------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/handlers/handleCreateRegOptions.ts b/src/handlers/handleCreateRegOptions.ts index 06fa3f1..36690e8 100644 --- a/src/handlers/handleCreateRegOptions.ts +++ b/src/handlers/handleCreateRegOptions.ts @@ -26,7 +26,7 @@ export async function handleCreateRegOptions(context: Context): Promise { // }); it('recognizes GET /registration/options', async () => { - const response = await SELF.fetch('https://example.com/registration/options?username=foo'); + const response = await SELF.fetch('https://example.com/registration/options?userName=foo'); expect(response.status).toBe(200); }); @@ -54,12 +54,12 @@ describe('Registration options', () => { it('requires username', async () => { const response = await SELF.fetch('https://example.com/registration/options'); expect(response.status).toBe(400); - expect(await response.text()).toMatch("username"); + expect(await response.text()).toMatch("userName"); }); it('generates basic options', async () => { const username = 'mmiller'; - const response = await SELF.fetch(`https://example.com/registration/options?username=${username}`); + const response = await SELF.fetch(`https://example.com/registration/options?userName=${username}`); expect(response.status).toBe(200); expect(response.headers.get('Content-Type')).toMatch('application/json'); @@ -87,19 +87,19 @@ describe('Registration options', () => { describe('Param: algES256', () => { it('omits ES256 when param is false', async () => { const response = await SELF.fetch( - 'https://example.com/registration/options?username=mmiller&algES256=false', + 'https://example.com/registration/options?userName=mmiller&algES256=false', ); const opts = await response.json() as PublicKeyCredentialCreationOptionsJSON; expect(opts.pubKeyCredParams).toEqual([ { "alg": -257, "type": "public-key" }, - ]) + ]); }); it('errors on bad param value', async () => { const response = await SELF.fetch( - 'https://example.com/registration/options?username=mmiller&algES256=maru', + 'https://example.com/registration/options?userName=mmiller&algES256=maru', { method: 'GET' }, ); expect(response.status).toBe(400); @@ -110,7 +110,7 @@ describe('Registration options', () => { describe('Param: algRS256', () => { it('omits RS256 when param is false', async () => { const response = await SELF.fetch( - 'https://example.com/registration/options?username=mmiller&algRS256=false', + 'https://example.com/registration/options?userName=mmiller&algRS256=false', ); const opts = await response.json() as PublicKeyCredentialCreationOptionsJSON; @@ -122,7 +122,7 @@ describe('Registration options', () => { it('errors on bad param value', async () => { const response = await SELF.fetch( - 'https://example.com/registration/options?username=mmiller&algRS256=batsu', + 'https://example.com/registration/options?userName=mmiller&algRS256=batsu', ); expect(response.status).toBe(400); expect(await response.text()).toMatch("algRS256"); @@ -132,7 +132,7 @@ describe('Registration options', () => { describe('Param: attestation', () => { it.each(['none', 'direct'])('supports value "%s"', async (val) => { const response = await SELF.fetch( - `https://example.com/registration/options?username=mmiller&attestation=${val}`, + `https://example.com/registration/options?userName=mmiller&attestation=${val}`, ); const opts = await response.json() as PublicKeyCredentialCreationOptionsJSON; @@ -142,7 +142,7 @@ describe('Registration options', () => { it('errors on bad param value', async () => { const response = await SELF.fetch( - 'https://example.com/registration/options?username=mmiller&attestation=sometimes', + 'https://example.com/registration/options?userName=mmiller&attestation=sometimes', ); expect(response.status).toBe(400); expect(await response.text()).toMatch("attestation"); @@ -156,7 +156,7 @@ describe('Registration options', () => { 'required', ])('supports value "%s"', async (val) => { const response = await SELF.fetch( - `https://example.com/registration/options?username=mmiller&discoverableCredential=${val}`, + `https://example.com/registration/options?userName=mmiller&discoverableCredential=${val}`, ); const opts = await response.json() as PublicKeyCredentialCreationOptionsJSON; @@ -167,7 +167,7 @@ describe('Registration options', () => { it('errors on bad param value', async () => { const response = await SELF.fetch( - 'https://example.com/registration/options?username=mmiller&discoverableCredential=sure', + 'https://example.com/registration/options?userName=mmiller&discoverableCredential=sure', ); expect(response.status).toBe(400); expect(await response.text()).toMatch("discoverableCredential"); @@ -181,7 +181,7 @@ describe('Registration options', () => { 'required', ])('supports value "%s"', async (val) => { const response = await SELF.fetch( - `https://example.com/registration/options?username=mmiller&userVerification=${val}`, + `https://example.com/registration/options?userName=mmiller&userVerification=${val}`, ); const opts = await response.json() as PublicKeyCredentialCreationOptionsJSON; @@ -191,7 +191,7 @@ describe('Registration options', () => { it('errors on bad param value', async () => { const response = await SELF.fetch( - 'https://example.com/registration/options?username=mmiller&userVerification=yesButNotCached', + 'https://example.com/registration/options?userName=mmiller&userVerification=yesButNotCached', ); expect(response.status).toBe(400); expect(await response.text()).toMatch("userVerification"); @@ -204,7 +204,7 @@ describe('Registration options', () => { 'platform', ])('supports value "%s"', async (val) => { const response = await SELF.fetch( - `https://example.com/registration/options?username=mmiller&attachment=${val}`, + `https://example.com/registration/options?userName=mmiller&attachment=${val}`, ); const opts = await response.json() as PublicKeyCredentialCreationOptionsJSON; @@ -214,7 +214,7 @@ describe('Registration options', () => { it('errors on bad param value', async () => { const response = await SELF.fetch( - 'https://example.com/registration/options?username=mmiller&attachment=hybrid', + 'https://example.com/registration/options?userName=mmiller&attachment=hybrid', ); expect(response.status).toBe(400); expect(await response.text()).toMatch("attachment"); From f1a4509460662c7a15b691723fd5ae1fa4e1871b Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Sat, 10 Aug 2024 11:47:00 -0700 Subject: [PATCH 13/14] Support base64url userID param --- src/handlers/handleCreateRegOptions.ts | 9 ++++++- src/schemas.ts | 21 +++++++++++++++ test/index.spec.ts | 36 ++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/handlers/handleCreateRegOptions.ts b/src/handlers/handleCreateRegOptions.ts index 36690e8..2ca1c81 100644 --- a/src/handlers/handleCreateRegOptions.ts +++ b/src/handlers/handleCreateRegOptions.ts @@ -2,7 +2,7 @@ import { Context } from 'hono'; import { HTTPException } from 'hono/http-exception'; import { zfd } from 'zod-form-data'; import { generateRegistrationOptions } from '@simplewebauthn/server'; -import { cose } from '@simplewebauthn/server/helpers'; +import { cose, isoBase64URL } from '@simplewebauthn/server/helpers'; import { ZodError, regOptionsInputSchema } from '../schemas'; @@ -27,6 +27,7 @@ export async function handleCreateRegOptions(context: Context): Promise { + if (!isoBase64URL.isBase64URL(String(value))) { + throw new z.ZodError([ + { + code: z.ZodIssueCode.invalid_string, + path: ctx.path, + message: 'not a valid base64url string', + validation: 'base64', + } + ]); + } + + return value; + }, z.string().base64()); +} /** * Schema for incoming query params to configure registration options */ export const regOptionsInputSchema = z.object({ userName: z.string(), + userID: castToBase64urlString().optional(), userVerification: z.enum(['discouraged', 'preferred', 'required']).default('preferred'), attestation: z.enum(['none', 'direct']).default('none'), attachment: z.enum(['cross-platform', 'platform']).optional(), diff --git a/test/index.spec.ts b/test/index.spec.ts index a0e34bf..e88720a 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -84,6 +84,42 @@ describe('Registration options', () => { expect(opts.extensions?.credProps).toEqual(true); }); + describe('Param: userID', () => { + it('uses specified user ID', async () => { + const userID = '1234'; + const response = await SELF.fetch( + `https://example.com/registration/options?userName=mmiller&userID=${userID}`, + ); + + const opts = await response.json() as PublicKeyCredentialCreationOptionsJSON; + + expect(opts.user.id).toEqual(userID); + }); + + it('generates unique user ID when omitted', async () => { + const response1 = await SELF.fetch( + 'https://example.com/registration/options?userName=mmiller', + ); + const opts1 = await response1.json() as PublicKeyCredentialCreationOptionsJSON; + + const response2 = await SELF.fetch( + 'https://example.com/registration/options?userName=mmiller', + ); + const opts2 = await response2.json() as PublicKeyCredentialCreationOptionsJSON; + + expect(opts1.user.id).not.toEqual(opts2.user.id); + }); + + it('errors on bad param value', async () => { + const response = await SELF.fetch( + 'https://example.com/registration/options?userName=mmiller&userID=mmiller@example.com', + ); + + expect(response.status).toBe(400); + expect(await response.text()).toMatch("userID"); + }); + }); + describe('Param: algES256', () => { it('omits ES256 when param is false', async () => { const response = await SELF.fetch( From 1c95ac9eddec9987b9b7522798610a5c24c520eb Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Sat, 10 Aug 2024 11:47:04 -0700 Subject: [PATCH 14/14] Clean up tests --- test/index.spec.ts | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/test/index.spec.ts b/test/index.spec.ts index e88720a..ea27d75 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1,29 +1,8 @@ -// test/index.spec.ts -import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test'; +import { SELF } from 'cloudflare:test'; import { describe, it, expect } from 'vitest'; import { PublicKeyCredentialCreationOptionsJSON } from '@simplewebauthn/types'; -// import worker from '../src/index'; - -// For now, you'll need to do something like this to get a correctly-typed -// `Request` to pass to `worker.fetch()`. -// const IncomingRequest = Request; describe('Routing tests', () => { - // it('responds with Hello World! (unit style)', async () => { - // const request = new IncomingRequest('http://example.com'); - // // Create an empty context to pass to `worker.fetch()`. - // const ctx = createExecutionContext(); - // const response = await worker.fetch(request, env, ctx); - // // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions - // await waitOnExecutionContext(ctx); - // expect(await response.text()).toMatchInlineSnapshot(`"Hello from passkeys.dev RP demo API!"`); - // }); - - // it('responds with Hello World! (integration style)', async () => { - // const response = await SELF.fetch('https://example.com'); - // expect(await response.text()).toMatchInlineSnapshot(`"Hello from passkeys.dev RP demo API!"`); - // }); - it('recognizes GET /registration/options', async () => { const response = await SELF.fetch('https://example.com/registration/options?userName=foo'); expect(response.status).toBe(200);