From 3632b0d4d9a4924dc3891a82ffcb108d15e9ec99 Mon Sep 17 00:00:00 2001 From: Gerben Date: Tue, 27 Aug 2024 13:09:39 +0200 Subject: [PATCH 01/77] feat: add passkey specific webauthn authentication support --- package.json | 3 + pnpm-lock.yaml | 192 +++++++++++++++--- src/module.ts | 6 + src/runtime/app/composables/passkey.ts | 93 +++++++++ .../server/lib/webauthn/authenticate.ts | 79 +++++++ src/runtime/server/lib/webauthn/register.ts | 67 ++++++ 6 files changed, 414 insertions(+), 26 deletions(-) create mode 100644 src/runtime/app/composables/passkey.ts create mode 100644 src/runtime/server/lib/webauthn/authenticate.ts create mode 100644 src/runtime/server/lib/webauthn/register.ts diff --git a/package.json b/package.json index fed07992..14995fdf 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,8 @@ }, "dependencies": { "@nuxt/kit": "^3.12.4", + "@simplewebauthn/browser": "^10.0.0", + "@simplewebauthn/server": "^10.0.1", "defu": "^6.1.4", "hookable": "^5.5.3", "ofetch": "^1.3.4", @@ -49,6 +51,7 @@ "@nuxt/test-utils": "^3.14.0", "@nuxt/ui": "^2.18.4", "@nuxt/ui-pro": "^1.4.1", + "@simplewebauthn/types": "^10.0.0", "changelogen": "^0.5.5", "eslint": "^9.9.0", "nuxt": "^3.12.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fedc1a14..0ce117e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,12 @@ importers: '@nuxt/kit': specifier: ^3.12.4 version: 3.12.4(magicast@0.3.4)(rollup@3.29.4) + '@simplewebauthn/browser': + specifier: ^10.0.0 + version: 10.0.0 + '@simplewebauthn/server': + specifier: ^10.0.1 + version: 10.0.1 defu: specifier: ^6.1.4 version: 6.1.4 @@ -35,7 +41,7 @@ importers: version: 1.1.113 '@nuxt/devtools': specifier: latest - version: 1.3.9(rollup@3.29.4) + version: 1.3.9(rollup@3.29.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3)) '@nuxt/eslint-config': specifier: ^0.5.0 version: 0.5.0(eslint@9.9.0(jiti@1.21.6))(typescript@5.5.4) @@ -47,13 +53,16 @@ importers: version: 3.12.4(rollup@3.29.4) '@nuxt/test-utils': specifier: ^3.14.0 - version: 3.14.0(h3@1.12.0)(magicast@0.3.4)(nitropack@2.9.7(magicast@0.3.4))(rollup@3.29.4)(vitest@2.0.5(@types/node@22.2.0)(terser@5.31.3))(vue-router@4.4.0(vue@3.4.34(typescript@5.5.4)))(vue@3.4.34(typescript@5.5.4)) + version: 3.14.0(h3@1.12.0)(magicast@0.3.4)(nitropack@2.9.7(magicast@0.3.4))(rollup@3.29.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3))(vitest@2.0.5(@types/node@22.2.0)(terser@5.31.3))(vue-router@4.4.0(vue@3.4.34(typescript@5.5.4)))(vue@3.4.34(typescript@5.5.4)) '@nuxt/ui': specifier: ^2.18.4 - version: 2.18.4(magicast@0.3.4)(rollup@3.29.4)(vue@3.4.34(typescript@5.5.4)) + version: 2.18.4(magicast@0.3.4)(rollup@3.29.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3))(vue@3.4.34(typescript@5.5.4)) '@nuxt/ui-pro': specifier: ^1.4.1 - version: 1.4.1(magicast@0.3.4)(rollup@3.29.4)(vue@3.4.34(typescript@5.5.4)) + version: 1.4.1(magicast@0.3.4)(rollup@3.29.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3))(vue@3.4.34(typescript@5.5.4)) + '@simplewebauthn/types': + specifier: ^10.0.0 + version: 10.0.0 changelogen: specifier: ^0.5.5 version: 0.5.5(magicast@0.3.4) @@ -62,7 +71,7 @@ importers: version: 9.9.0(jiti@1.21.6) nuxt: specifier: ^3.12.4 - version: 3.12.4(@parcel/watcher@2.4.1)(@types/node@22.2.0)(eslint@9.9.0(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.4)(optionator@0.9.4)(rollup@3.29.4)(terser@5.31.3)(typescript@5.5.4)(vue-tsc@2.0.29(typescript@5.5.4)) + version: 3.12.4(@parcel/watcher@2.4.1)(@types/node@22.2.0)(eslint@9.9.0(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.4)(optionator@0.9.4)(rollup@3.29.4)(terser@5.31.3)(typescript@5.5.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3))(vue-tsc@2.0.29(typescript@5.5.4)) typescript: specifier: ^5.5.4 version: 5.5.4 @@ -80,7 +89,7 @@ importers: version: 3.12.4(@parcel/watcher@2.4.1)(@types/node@22.2.0)(eslint@9.9.0(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.4)(optionator@0.9.4)(rollup@4.19.1)(terser@5.31.3)(typescript@5.5.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3))(vue-tsc@2.0.29(typescript@5.5.4)) nuxt-auth-utils: specifier: latest - version: 0.3.2(magicast@0.3.4)(rollup@4.19.1) + version: 0.3.4(magicast@0.3.4)(rollup@4.19.1) devDependencies: '@iconify-json/gravity-ui': specifier: ^1.1.4 @@ -878,6 +887,9 @@ packages: peerDependencies: vue: ^3.2.0 + '@hexagon/base64@1.1.28': + resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} + '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} @@ -950,6 +962,9 @@ packages: '@kwsites/promise-deferred@1.1.1': resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + '@levischuck/tiny-cbor@0.2.2': + resolution: {integrity: sha512-f5CnPw997Y2GQ8FAvtuVVC19FX8mwNNC+1XJcIi16n/LTJifKO6QBgGLgN3YEmqtGMk17SKSuoWES3imJVxAVw==} + '@mapbox/node-pre-gyp@1.0.11': resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true @@ -1170,6 +1185,21 @@ packages: resolution: {integrity: sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==} engines: {node: '>= 10.0.0'} + '@peculiar/asn1-android@2.3.13': + resolution: {integrity: sha512-0VTNazDGKrLS6a3BwTDZanqq6DR/I3SbvmDMuS8Be+OYpvM6x1SRDh9AGDsHVnaCOIztOspCPc6N1m+iUv1Xxw==} + + '@peculiar/asn1-ecc@2.3.13': + resolution: {integrity: sha512-3dF2pQcrN/WJEMq+9qWLQ0gqtn1G81J4rYqFl6El6QV367b4IuhcRv+yMA84tNNyHOJn9anLXV5radnpPiG3iA==} + + '@peculiar/asn1-rsa@2.3.13': + resolution: {integrity: sha512-wBNQqCyRtmqvXkGkL4DR3WxZhHy8fDiYtOjTeCd7SFE5F6GBeafw3EJ94PX/V0OJJrjQ40SkRY2IZu3ZSyBqcg==} + + '@peculiar/asn1-schema@2.3.13': + resolution: {integrity: sha512-3Xq3a01WkHRZL8X04Zsfg//mGaA21xlL4tlVn4v2xGT0JStiztATRkMwa5b+f/HXmY2smsiLXYK46Gwgzvfg3g==} + + '@peculiar/asn1-x509@2.3.13': + resolution: {integrity: sha512-PfeLQl2skXmxX2/AFFCVaWU8U6FKW1Db43mgBhShCOFS1bVxqtvusq1hVjfuEcuSQGedrLdCSvTgabluwN/M9A==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1343,6 +1373,16 @@ packages: '@rushstack/eslint-patch@1.10.4': resolution: {integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==} + '@simplewebauthn/browser@10.0.0': + resolution: {integrity: sha512-hG0JMZD+LiLUbpQcAjS4d+t4gbprE/dLYop/CkE01ugU/9sKXflxV5s0DRjdz3uNMFecatRfb4ZLG3XvF8m5zg==} + + '@simplewebauthn/server@10.0.1': + resolution: {integrity: sha512-djNWcRn+H+6zvihBFJSpG3fzb0NQS9c/Mw5dYOtZ9H+oDw8qn9Htqxt4cpqRvSOAfwqP7rOvE9rwqVaoGGc3hg==} + engines: {node: '>=20.0.0'} + + '@simplewebauthn/types@10.0.0': + resolution: {integrity: sha512-SFXke7xkgPRowY2E+8djKbdEznTVnD5R6GO7GPTthpHrokLvNKw8C3lFZypTxLI7KkCfGPfhtqB3d7OVGGa9jQ==} + '@sindresorhus/merge-streams@2.3.0': resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} @@ -1807,6 +1847,10 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + asn1js@3.0.5: + resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==} + engines: {node: '>=12.0.0'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -2132,6 +2176,9 @@ packages: resolution: {integrity: sha512-ULYhWIonJzlScCCQrPUG5uMXzXxSixty4djud9SS37DoNxDdkeRocxzHuAo4ImRBUK+mAuU5X9TSwEDccnnuPg==} hasBin: true + cross-fetch@4.0.0: + resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -2880,6 +2927,10 @@ packages: resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==} engines: {node: '>=12.22.0'} + ipaddr.js@2.2.0: + resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} + engines: {node: '>= 10'} + iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} @@ -3403,8 +3454,8 @@ packages: engines: {node: ^16.10.0 || >=18.0.0} hasBin: true - nuxt-auth-utils@0.3.2: - resolution: {integrity: sha512-A2gRelkEQxLoosIUw6TFFsMaq06XJ6dVdUYKRSKzxfMepBUq5v7GXiw+gZ9TVAXb0YjN9obmqJOBzf2qNI77yw==} + nuxt-auth-utils@0.3.4: + resolution: {integrity: sha512-0CNREFADCyZtyAusV3n27y1jnvt2KjrZI2boaMbKPDWiyI8AcV1jESG1aBFOxyViKsF4YnXRY8QN3EJKYvxMjA==} nuxt@3.12.4: resolution: {integrity: sha512-/ddvyc2kgYYIN2UEjP8QIz48O/W3L0lZm7wChIDbOCj0vF/yLLeZHBaTb3aNvS9Hwp269nfjrm8j/mVxQK4RhA==} @@ -3849,6 +3900,13 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pvtsutils@1.3.5: + resolution: {integrity: sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==} + + pvutils@1.1.3: + resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} + engines: {node: '>=6.0.0'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -5392,6 +5450,8 @@ snapshots: '@tanstack/vue-virtual': 3.8.3(vue@3.4.34(typescript@5.5.4)) vue: 3.4.34(typescript@5.5.4) + '@hexagon/base64@1.1.28': {} + '@humanwhocodes/module-importer@1.0.1': {} '@humanwhocodes/retry@0.3.0': {} @@ -5486,6 +5546,8 @@ snapshots: '@kwsites/promise-deferred@1.1.1': {} + '@levischuck/tiny-cbor@0.2.2': {} + '@mapbox/node-pre-gyp@1.0.11': dependencies: detect-libc: 2.0.3 @@ -5526,11 +5588,12 @@ snapshots: '@nuxt/devalue@2.0.2': {} - '@nuxt/devtools-kit@1.3.9(magicast@0.3.4)(rollup@3.29.4)': + '@nuxt/devtools-kit@1.3.9(magicast@0.3.4)(rollup@3.29.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3))': dependencies: '@nuxt/kit': 3.12.4(magicast@0.3.4)(rollup@3.29.4) '@nuxt/schema': 3.12.4(rollup@3.29.4) execa: 7.2.0 + vite: 5.3.5(@types/node@22.2.0)(terser@5.31.3) transitivePeerDependencies: - magicast - rollup @@ -5560,10 +5623,10 @@ snapshots: rc9: 2.1.2 semver: 7.6.3 - '@nuxt/devtools@1.3.9(rollup@3.29.4)': + '@nuxt/devtools@1.3.9(rollup@3.29.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3))': dependencies: '@antfu/utils': 0.7.10 - '@nuxt/devtools-kit': 1.3.9(magicast@0.3.4)(rollup@3.29.4) + '@nuxt/devtools-kit': 1.3.9(magicast@0.3.4)(rollup@3.29.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3)) '@nuxt/devtools-wizard': 1.3.9 '@nuxt/kit': 3.12.4(magicast@0.3.4)(rollup@3.29.4) '@vue/devtools-core': 7.3.3(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3)) @@ -5595,7 +5658,8 @@ snapshots: simple-git: 3.25.0 sirv: 2.0.4 unimport: 3.9.1(rollup@3.29.4) - vite-plugin-inspect: 0.8.5(@nuxt/kit@3.12.4(magicast@0.3.4)(rollup@3.29.4))(rollup@3.29.4) + vite: 5.3.5(@types/node@22.2.0)(terser@5.31.3) + vite-plugin-inspect: 0.8.5(@nuxt/kit@3.12.4(magicast@0.3.4)(rollup@3.29.4))(rollup@3.29.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3)) vite-plugin-vue-inspector: 5.1.3(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3)) which: 3.0.1 ws: 8.18.0 @@ -5685,13 +5749,13 @@ snapshots: - supports-color - typescript - '@nuxt/icon@1.4.5(magicast@0.3.4)(rollup@3.29.4)(vue@3.4.34(typescript@5.5.4))': + '@nuxt/icon@1.4.5(magicast@0.3.4)(rollup@3.29.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3))(vue@3.4.34(typescript@5.5.4))': dependencies: '@iconify/collections': 1.0.444 '@iconify/types': 2.0.0 '@iconify/utils': 2.1.29 '@iconify/vue': 4.1.3-beta.1(vue@3.4.34(typescript@5.5.4)) - '@nuxt/devtools-kit': 1.3.9(magicast@0.3.4)(rollup@3.29.4) + '@nuxt/devtools-kit': 1.3.9(magicast@0.3.4)(rollup@3.29.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3)) '@nuxt/kit': 3.12.4(magicast@0.3.4)(rollup@3.29.4) consola: 3.2.3 fast-glob: 3.3.2 @@ -5862,7 +5926,7 @@ snapshots: - rollup - supports-color - '@nuxt/test-utils@3.14.0(h3@1.12.0)(magicast@0.3.4)(nitropack@2.9.7(magicast@0.3.4))(rollup@3.29.4)(vitest@2.0.5(@types/node@22.2.0)(terser@5.31.3))(vue-router@4.4.0(vue@3.4.34(typescript@5.5.4)))(vue@3.4.34(typescript@5.5.4))': + '@nuxt/test-utils@3.14.0(h3@1.12.0)(magicast@0.3.4)(nitropack@2.9.7(magicast@0.3.4))(rollup@3.29.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3))(vitest@2.0.5(@types/node@22.2.0)(terser@5.31.3))(vue-router@4.4.0(vue@3.4.34(typescript@5.5.4)))(vue@3.4.34(typescript@5.5.4))': dependencies: '@nuxt/kit': 3.12.4(magicast@0.3.4)(rollup@3.29.4) '@nuxt/schema': 3.12.4(rollup@3.29.4) @@ -5888,7 +5952,8 @@ snapshots: ufo: 1.5.4 unenv: 1.10.0 unplugin: 1.12.0 - vitest-environment-nuxt: 1.0.0(h3@1.12.0)(magicast@0.3.4)(nitropack@2.9.7(magicast@0.3.4))(rollup@3.29.4)(vitest@2.0.5(@types/node@22.2.0)(terser@5.31.3))(vue-router@4.4.0(vue@3.4.34(typescript@5.5.4)))(vue@3.4.34(typescript@5.5.4)) + vite: 5.3.5(@types/node@22.2.0)(terser@5.31.3) + vitest-environment-nuxt: 1.0.0(h3@1.12.0)(magicast@0.3.4)(nitropack@2.9.7(magicast@0.3.4))(rollup@3.29.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3))(vitest@2.0.5(@types/node@22.2.0)(terser@5.31.3))(vue-router@4.4.0(vue@3.4.34(typescript@5.5.4)))(vue@3.4.34(typescript@5.5.4)) vue: 3.4.34(typescript@5.5.4) vue-router: 4.4.0(vue@3.4.34(typescript@5.5.4)) optionalDependencies: @@ -5898,10 +5963,10 @@ snapshots: - rollup - supports-color - '@nuxt/ui-pro@1.4.1(magicast@0.3.4)(rollup@3.29.4)(vue@3.4.34(typescript@5.5.4))': + '@nuxt/ui-pro@1.4.1(magicast@0.3.4)(rollup@3.29.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3))(vue@3.4.34(typescript@5.5.4))': dependencies: '@iconify-json/vscode-icons': 1.1.37 - '@nuxt/ui': 2.18.4(magicast@0.3.4)(rollup@3.29.4)(vue@3.4.34(typescript@5.5.4)) + '@nuxt/ui': 2.18.4(magicast@0.3.4)(rollup@3.29.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3))(vue@3.4.34(typescript@5.5.4)) '@vueuse/core': 10.11.0(vue@3.4.34(typescript@5.5.4)) defu: 6.1.4 git-url-parse: 14.1.0 @@ -5932,12 +5997,12 @@ snapshots: - vite - vue - '@nuxt/ui@2.18.4(magicast@0.3.4)(rollup@3.29.4)(vue@3.4.34(typescript@5.5.4))': + '@nuxt/ui@2.18.4(magicast@0.3.4)(rollup@3.29.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3))(vue@3.4.34(typescript@5.5.4))': dependencies: '@headlessui/tailwindcss': 0.2.1(tailwindcss@3.4.7) '@headlessui/vue': 1.7.22(vue@3.4.34(typescript@5.5.4)) '@iconify-json/heroicons': 1.1.23 - '@nuxt/icon': 1.4.5(magicast@0.3.4)(rollup@3.29.4)(vue@3.4.34(typescript@5.5.4)) + '@nuxt/icon': 1.4.5(magicast@0.3.4)(rollup@3.29.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3))(vue@3.4.34(typescript@5.5.4)) '@nuxt/kit': 3.12.4(magicast@0.3.4)(rollup@3.29.4) '@nuxtjs/color-mode': 3.4.2(magicast@0.3.4)(rollup@3.29.4) '@nuxtjs/tailwindcss': 6.12.1(magicast@0.3.4)(rollup@3.29.4) @@ -6186,6 +6251,40 @@ snapshots: '@parcel/watcher-win32-ia32': 2.4.1 '@parcel/watcher-win32-x64': 2.4.1 + '@peculiar/asn1-android@2.3.13': + dependencies: + '@peculiar/asn1-schema': 2.3.13 + asn1js: 3.0.5 + tslib: 2.6.3 + + '@peculiar/asn1-ecc@2.3.13': + dependencies: + '@peculiar/asn1-schema': 2.3.13 + '@peculiar/asn1-x509': 2.3.13 + asn1js: 3.0.5 + tslib: 2.6.3 + + '@peculiar/asn1-rsa@2.3.13': + dependencies: + '@peculiar/asn1-schema': 2.3.13 + '@peculiar/asn1-x509': 2.3.13 + asn1js: 3.0.5 + tslib: 2.6.3 + + '@peculiar/asn1-schema@2.3.13': + dependencies: + asn1js: 3.0.5 + pvtsutils: 1.3.5 + tslib: 2.6.3 + + '@peculiar/asn1-x509@2.3.13': + dependencies: + '@peculiar/asn1-schema': 2.3.13 + asn1js: 3.0.5 + ipaddr.js: 2.2.0 + pvtsutils: 1.3.5 + tslib: 2.6.3 + '@pkgjs/parseargs@0.11.0': optional: true @@ -6364,6 +6463,26 @@ snapshots: '@rushstack/eslint-patch@1.10.4': {} + '@simplewebauthn/browser@10.0.0': + dependencies: + '@simplewebauthn/types': 10.0.0 + + '@simplewebauthn/server@10.0.1': + dependencies: + '@hexagon/base64': 1.1.28 + '@levischuck/tiny-cbor': 0.2.2 + '@peculiar/asn1-android': 2.3.13 + '@peculiar/asn1-ecc': 2.3.13 + '@peculiar/asn1-rsa': 2.3.13 + '@peculiar/asn1-schema': 2.3.13 + '@peculiar/asn1-x509': 2.3.13 + '@simplewebauthn/types': 10.0.0 + cross-fetch: 4.0.0 + transitivePeerDependencies: + - encoding + + '@simplewebauthn/types@10.0.0': {} + '@sindresorhus/merge-streams@2.3.0': {} '@stylistic/eslint-plugin-js@2.6.2(eslint@9.9.0(jiti@1.21.6))': @@ -6978,6 +7097,12 @@ snapshots: array-union@2.1.0: {} + asn1js@3.0.5: + dependencies: + pvtsutils: 1.3.5 + pvutils: 1.1.3 + tslib: 2.6.3 + assertion-error@2.0.1: {} ast-kit@0.12.2: @@ -7295,6 +7420,12 @@ snapshots: cronstrue@2.50.0: {} + cross-fetch@4.0.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -8209,6 +8340,8 @@ snapshots: transitivePeerDependencies: - supports-color + ipaddr.js@2.2.0: {} + iron-webcrypto@1.2.1: {} is-arrayish@0.2.1: {} @@ -8769,7 +8902,7 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - nuxt-auth-utils@0.3.2(magicast@0.3.4)(rollup@4.19.1): + nuxt-auth-utils@0.3.4(magicast@0.3.4)(rollup@4.19.1): dependencies: '@nuxt/kit': 3.12.4(magicast@0.3.4)(rollup@4.19.1) defu: 6.1.4 @@ -8783,10 +8916,10 @@ snapshots: - rollup - supports-color - nuxt@3.12.4(@parcel/watcher@2.4.1)(@types/node@22.2.0)(eslint@9.9.0(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.4)(optionator@0.9.4)(rollup@3.29.4)(terser@5.31.3)(typescript@5.5.4)(vue-tsc@2.0.29(typescript@5.5.4)): + nuxt@3.12.4(@parcel/watcher@2.4.1)(@types/node@22.2.0)(eslint@9.9.0(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.4)(optionator@0.9.4)(rollup@3.29.4)(terser@5.31.3)(typescript@5.5.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3))(vue-tsc@2.0.29(typescript@5.5.4)): dependencies: '@nuxt/devalue': 2.0.2 - '@nuxt/devtools': 1.3.9(rollup@3.29.4) + '@nuxt/devtools': 1.3.9(rollup@3.29.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3)) '@nuxt/kit': 3.12.4(magicast@0.3.4)(rollup@3.29.4) '@nuxt/schema': 3.12.4(rollup@3.29.4) '@nuxt/telemetry': 2.5.4(magicast@0.3.4)(rollup@3.29.4) @@ -9400,6 +9533,12 @@ snapshots: punycode@2.3.1: {} + pvtsutils@1.3.5: + dependencies: + tslib: 2.6.3 + + pvutils@1.1.3: {} + queue-microtask@1.2.3: {} queue-tick@1.0.1: {} @@ -10256,7 +10395,7 @@ snapshots: typescript: 5.5.4 vue-tsc: 2.0.29(typescript@5.5.4) - vite-plugin-inspect@0.8.5(@nuxt/kit@3.12.4(magicast@0.3.4)(rollup@3.29.4))(rollup@3.29.4): + vite-plugin-inspect@0.8.5(@nuxt/kit@3.12.4(magicast@0.3.4)(rollup@3.29.4))(rollup@3.29.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3)): dependencies: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.1.0(rollup@3.29.4) @@ -10267,6 +10406,7 @@ snapshots: perfect-debounce: 1.0.0 picocolors: 1.0.1 sirv: 2.0.4 + vite: 5.3.5(@types/node@22.2.0)(terser@5.31.3) optionalDependencies: '@nuxt/kit': 3.12.4(magicast@0.3.4)(rollup@3.29.4) transitivePeerDependencies: @@ -10316,9 +10456,9 @@ snapshots: fsevents: 2.3.3 terser: 5.31.3 - vitest-environment-nuxt@1.0.0(h3@1.12.0)(magicast@0.3.4)(nitropack@2.9.7(magicast@0.3.4))(rollup@3.29.4)(vitest@2.0.5(@types/node@22.2.0)(terser@5.31.3))(vue-router@4.4.0(vue@3.4.34(typescript@5.5.4)))(vue@3.4.34(typescript@5.5.4)): + vitest-environment-nuxt@1.0.0(h3@1.12.0)(magicast@0.3.4)(nitropack@2.9.7(magicast@0.3.4))(rollup@3.29.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3))(vitest@2.0.5(@types/node@22.2.0)(terser@5.31.3))(vue-router@4.4.0(vue@3.4.34(typescript@5.5.4)))(vue@3.4.34(typescript@5.5.4)): dependencies: - '@nuxt/test-utils': 3.14.0(h3@1.12.0)(magicast@0.3.4)(nitropack@2.9.7(magicast@0.3.4))(rollup@3.29.4)(vitest@2.0.5(@types/node@22.2.0)(terser@5.31.3))(vue-router@4.4.0(vue@3.4.34(typescript@5.5.4)))(vue@3.4.34(typescript@5.5.4)) + '@nuxt/test-utils': 3.14.0(h3@1.12.0)(magicast@0.3.4)(nitropack@2.9.7(magicast@0.3.4))(rollup@3.29.4)(vite@5.3.5(@types/node@22.2.0)(terser@5.31.3))(vitest@2.0.5(@types/node@22.2.0)(terser@5.31.3))(vue-router@4.4.0(vue@3.4.34(typescript@5.5.4)))(vue@3.4.34(typescript@5.5.4)) transitivePeerDependencies: - '@cucumber/cucumber' - '@jest/globals' diff --git a/src/module.ts b/src/module.ts index 2219bbfd..0e22ae04 100644 --- a/src/module.ts +++ b/src/module.ts @@ -36,6 +36,7 @@ export default defineNuxtModule({ // Server addServerPlugin(resolver.resolve('./runtime/server/plugins/oauth')) addServerImportsDir(resolver.resolve('./runtime/server/lib/oauth')) + addServerImportsDir(resolver.resolve('./runtime/server/lib/webauthn')) addServerImportsDir(resolver.resolve('./runtime/server/utils')) addServerHandler({ handler: resolver.resolve('./runtime/server/api/session.delete'), @@ -75,6 +76,11 @@ export default defineNuxtModule({ } } + // WebAuthn settings + runtimeConfig.passkey = defu(runtimeConfig.passkey, {}) + runtimeConfig.passkey.registrationOptions = defu(runtimeConfig.passkey.registrationOptions, {}) // TODO: add default values + runtimeConfig.passkey.authenticationOptions = defu(runtimeConfig.passkey.authenticationOptions, {}) // TODO: add default values + // OAuth settings runtimeConfig.oauth = defu(runtimeConfig.oauth, {}) // GitHub OAuth diff --git a/src/runtime/app/composables/passkey.ts b/src/runtime/app/composables/passkey.ts new file mode 100644 index 00000000..f2bc7fbd --- /dev/null +++ b/src/runtime/app/composables/passkey.ts @@ -0,0 +1,93 @@ +import { + browserSupportsWebAuthn, + browserSupportsWebAuthnAutofill, + platformAuthenticatorIsAvailable, + startAuthentication, + startRegistration, +} from '@simplewebauthn/browser' +import type { VerifiedAuthenticationResponse, VerifiedRegistrationResponse } from '@simplewebauthn/server' +import type { AuthenticationResponseJSON, PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialRequestOptionsJSON, RegistrationResponseJSON } from '@simplewebauthn/types' +import { computed } from 'vue' + +export function usePasskey(options: { + registrationEndpoint: string + authenticationEndpoint: string + onRegistrationError?: (error: unknown) => void + onAuthenticationError?: (error: unknown) => void +}) { + const isAvailable = computed(() => browserSupportsWebAuthn() || browserSupportsWebAuthnAutofill()) + const isPlatformAvailable = computed(() => platformAuthenticatorIsAvailable()) + + async function register(userName: string, displayName: string) { + let attestationResponse: RegistrationResponseJSON | null = null + const creationOptions = await $fetch(options.registrationEndpoint, { + method: 'POST', + body: { + userName, + displayName, + verify: false, + }, + }) + + try { + attestationResponse = await startRegistration(creationOptions) + } + catch (error) { + options.onRegistrationError?.(error) + return false + } + + if (!attestationResponse) + return false + + const verificationResponse = await $fetch(options.registrationEndpoint, { + method: 'POST', + body: { + userName, + displayName, + response: attestationResponse, + verify: true, + }, + }) + + return verificationResponse && verificationResponse.verified + } + + async function authenticate() { + let assertionResponse: AuthenticationResponseJSON | null = null + const requestOptions = await $fetch(options.authenticationEndpoint, { + method: 'POST', + body: { + verify: false, + }, + }) + + try { + assertionResponse = await startAuthentication(requestOptions) + } + catch (error) { + options.onAuthenticationError?.(error) + return false + } + + if (!assertionResponse) + return false + + const verificationResponse = await $fetch(options.authenticationEndpoint, { + method: 'POST', + body: { + response: assertionResponse, + verify: true, + }, + }) + + return verificationResponse && verificationResponse.verified + } + + return { + register, + authenticate, + isAvailable, + isPlatformAvailable, + } +} diff --git a/src/runtime/server/lib/webauthn/authenticate.ts b/src/runtime/server/lib/webauthn/authenticate.ts new file mode 100644 index 00000000..02fa7b0f --- /dev/null +++ b/src/runtime/server/lib/webauthn/authenticate.ts @@ -0,0 +1,79 @@ +import type { H3Event } from 'h3' +import { eventHandler, H3Error, createError, getRequestURL, readBody } from 'h3' +import type { GenerateAuthenticationOptionsOpts, VerifiedAuthenticationResponse } from '@simplewebauthn/server' +import { generateAuthenticationOptions, verifyAuthenticationResponse } from '@simplewebauthn/server' +import defu from 'defu' +import type { AuthenticatorTransportFuture, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types' +import { useRuntimeConfig } from '#imports' + +// FIXME: better type name? +interface AuthenticationData { + passkey: { + id: string + publicKey: Uint8Array + counter: number + transports?: AuthenticatorTransportFuture[] + } + options: PublicKeyCredentialRequestOptionsJSON +} + +interface PasskeyAuthenticationEventHandlerOptions { + storeChallenge: (event: H3Event, options: PublicKeyCredentialRequestOptionsJSON) => void | Promise + getChallenge: (event: H3Event) => AuthenticationData | Promise + onSuccces: (event: H3Event, response: VerifiedAuthenticationResponse['authenticationInfo']) => void | Promise + onError: (event: H3Event, error: H3Error) => void | Promise + config: (event: H3Event) => GenerateAuthenticationOptionsOpts | Promise +} + +export default function definePasskeyAuthenticationEventHandler({ + storeChallenge, + getChallenge, + onSuccces, + onError, + config, +}: PasskeyAuthenticationEventHandlerOptions) { + return eventHandler(async (event) => { + const url = getRequestURL(event) + const _config = defu(await config(event), useRuntimeConfig(event).passkey.authenticationOptions, { + rpID: url.hostname, + rpName: 'Nuxt Auth Utils', + }) + + const body = await readBody(event) + + try { + if (!body.verify) { + const options = await generateAuthenticationOptions(_config) + await storeChallenge(event, options) + return options + } + + const { options, passkey } = await getChallenge(event) + const verification = await verifyAuthenticationResponse({ + response: body.response, + expectedChallenge: options.challenge, + expectedOrigin: url.origin, + expectedRPID: url.hostname, + authenticator: { + credentialID: body.response.id, // TODO: Is this correct? + credentialPublicKey: passkey.publicKey, + counter: passkey.counter, + transports: passkey.transports, + }, + }) + + if (!verification.verified) + throw createError({ statusCode: 400, message: 'Failed to verify registration response' }) + + await onSuccces(event, verification.authenticationInfo) + return verification + } + catch (error) { + if (!onError) throw error + if (error instanceof H3Error) + return onError(event, error) + console.error(error) + return onError(event, createError({ statusCode: 500, message: 'Failed to authenticate passkey' })) + } + }) +} diff --git a/src/runtime/server/lib/webauthn/register.ts b/src/runtime/server/lib/webauthn/register.ts new file mode 100644 index 00000000..358ae3ed --- /dev/null +++ b/src/runtime/server/lib/webauthn/register.ts @@ -0,0 +1,67 @@ +import type { H3Event } from 'h3' +import { eventHandler, H3Error, createError, getRequestURL, readBody } from 'h3' +import type { GenerateRegistrationOptionsOpts, VerifiedRegistrationResponse } from '@simplewebauthn/server' +import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server' +import defu from 'defu' +import type { PublicKeyCredentialCreationOptionsJSON } from '@simplewebauthn/types' +import { useRuntimeConfig } from '#imports' + +interface PasskeyRegistrationEventHandlerOptions { + storeChallenge: (event: H3Event, options: PublicKeyCredentialCreationOptionsJSON) => void | Promise + getChallenge: (event: H3Event) => PublicKeyCredentialCreationOptionsJSON | Promise + onSuccces: (event: H3Event, response: VerifiedRegistrationResponse['registrationInfo'], body: any) => void | Promise // FIXME: type body + onError: (event: H3Event, error: H3Error) => void | Promise + config: (event: H3Event) => GenerateRegistrationOptionsOpts | Promise +} + +export default function definePasskeyRegistrationEventHandler({ + storeChallenge, + getChallenge, + onSuccces, + onError, + config, +}: PasskeyRegistrationEventHandlerOptions) { + return eventHandler(async (event) => { + const url = getRequestURL(event) + const _config = defu(await config(event), useRuntimeConfig(event).passkey.registrationOptions, { + rpID: url.hostname, + rpName: 'Nuxt Auth Utils', + authenticatorSelection: { + userVerification: 'preferred', + }, + }) + + const body = await readBody(event) + if (body.verify === undefined || !body.userName) + throw createError({ statusCode: 400 }) + + try { + if (!body.verify) { + const options = await generateRegistrationOptions(_config as GenerateRegistrationOptionsOpts) + await storeChallenge(event, options) + return options + } + + const options = await getChallenge(event) + const verification = await verifyRegistrationResponse({ + response: body.response, + expectedChallenge: options.challenge, + expectedOrigin: url.origin, + expectedRPID: url.hostname, + requireUserVerification: false, // TODO: make this configurable + }) + + if (!verification.verified) + throw createError({ statusCode: 400, message: 'Failed to verify registration response' }) + + await onSuccces(event, verification.registrationInfo, body) + return verification + } + catch (error) { + if (!onError) throw error + if (error instanceof H3Error) + return onError(event, error) + return onError(event, createError({ statusCode: 500, message: 'Failed to register passkey' })) + } + }) +} From f701e107e9d594dd526d9d285771b8027eea5f9c Mon Sep 17 00:00:00 2001 From: Gerben Date: Tue, 27 Aug 2024 13:10:30 +0200 Subject: [PATCH 02/77] feat: playground passkey implementation --- playground/app.vue | 62 +--------- playground/auth.d.ts | 1 + playground/components/PasskeyModal.vue | 114 ++++++++++++++++++ playground/components/PasswordModal.vue | 67 ++++++++++ playground/nuxt.config.ts | 8 ++ playground/server/api/passkey/login.post.ts | 71 +++++++++++ .../server/api/passkey/register.post.ts | 52 ++++++++ 7 files changed, 316 insertions(+), 59 deletions(-) create mode 100644 playground/components/PasskeyModal.vue create mode 100644 playground/components/PasswordModal.vue create mode 100644 playground/server/api/passkey/login.post.ts create mode 100644 playground/server/api/passkey/register.post.ts diff --git a/playground/app.vue b/playground/app.vue index 57655802..d5ff5a21 100644 --- a/playground/app.vue +++ b/playground/app.vue @@ -1,32 +1,5 @@ + + diff --git a/playground/components/PasswordModal.vue b/playground/components/PasswordModal.vue new file mode 100644 index 00000000..f8055b82 --- /dev/null +++ b/playground/components/PasswordModal.vue @@ -0,0 +1,67 @@ + + + diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index a45afb94..c0243c31 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -20,4 +20,12 @@ export default defineNuxtConfig({ // ssr: false, }, }, + nitro: { + devStorage: { + db: { + driver: 'fs', + base: './data/db', + }, + }, + }, }) diff --git a/playground/server/api/passkey/login.post.ts b/playground/server/api/passkey/login.post.ts new file mode 100644 index 00000000..fafabbe9 --- /dev/null +++ b/playground/server/api/passkey/login.post.ts @@ -0,0 +1,71 @@ +import { base64URLStringToBuffer, bufferToBase64URLString } from '@simplewebauthn/browser' +import { getRandomValues } from 'uncrypto' +import type { AuthenticatorTransportFuture, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types' +import definePasskeyAuthenticationEventHandler from '../../../../src/runtime/server/lib/webauthn/authenticate' // FIXME: autoimport + +interface PasskeyData { + id: string + publicKey: string + counter: number + transports?: AuthenticatorTransportFuture[] +} + +export default definePasskeyAuthenticationEventHandler({ + storeChallenge: async (event, options) => { + const attemptId = bufferToBase64URLString(getRandomValues(new Uint8Array(32))) + await useStorage().setItem(`passkeys:${attemptId}`, options) + setCookie(event, 'passkey-attempt-id', attemptId) + }, + getChallenge: async (event) => { + const attemptId = getCookie(event, 'passkey-attempt-id') + if (!attemptId) + throw createError({ statusCode: 400 }) + + const options = await useStorage().getItem(`passkeys:${attemptId}`) + if (!options) + throw createError({ statusCode: 400 }) + + await useStorage().removeItem(`passkeys:${attemptId}`) + + const body = await readBody(event) + const passkey = await useStorage('db').getItem(`users:${body.response.id}`) + if (!passkey) + throw createError({ statusCode: 400 }) + + console.log('user from response id', passkey) + return { + options, + passkey: { + id: passkey.id, + publicKey: new Uint8Array(base64URLStringToBuffer(passkey.publicKey)), + counter: passkey.counter, + transports: passkey.transports, + }, + } + }, + onSuccces: async (event, response) => { + console.log('Success', response) + const user = await useStorage('db').getItem(`users:${response!.credentialID}`) + if (!user) + throw createError({ statusCode: 400 }) + + user.counter = response!.newCounter + await useStorage('db').setItem(`users:${response!.credentialID}`, user) + + await setUserSession(event, { + user: { + passkey: response!.credentialID, + }, + loggedInAt: Date.now(), + }) + }, + onError: (_, error) => { + console.log('Error', error) + }, + config: async (event) => { + return { + rpID: getRequestURL(event).hostname, + rpName: 'My Passkey', + } + }, +}) diff --git a/playground/server/api/passkey/register.post.ts b/playground/server/api/passkey/register.post.ts new file mode 100644 index 00000000..3ceeb10d --- /dev/null +++ b/playground/server/api/passkey/register.post.ts @@ -0,0 +1,52 @@ +import { bufferToBase64URLString } from '@simplewebauthn/browser' +import { getRandomValues } from 'uncrypto' +import type { PublicKeyCredentialCreationOptionsJSON } from '@simplewebauthn/types' +import definePasskeyRegistrationEventHandler from '../../../../src/runtime/server/lib/webauthn/register' // FIXME: autoimport + +export default definePasskeyRegistrationEventHandler({ + storeChallenge: async (event, options) => { + const attemptId = bufferToBase64URLString(getRandomValues(new Uint8Array(32))) + await useStorage('db').setItem(`passkeys:${attemptId}`, options) + setCookie(event, 'passkey-attempt-id', attemptId) + }, + getChallenge: async (event) => { + const attemptId = getCookie(event, 'passkey-attempt-id') + if (!attemptId) + throw createError({ statusCode: 400 }) + + const options = await useStorage('db').getItem(`passkeys:${attemptId}`) + if (!options) + throw createError({ statusCode: 400 }) + + await useStorage('db').removeItem(`passkeys:${attemptId}`) + return options + }, + onSuccces: async (event, response, body) => { + const user = { + id: 1, + displayName: body.displayName, + userName: body.userName, + publicKey: bufferToBase64URLString(response!.credentialPublicKey), + counter: response!.counter, + } + await useStorage('db').setItem(`users:${response!.credentialID}`, user) + await setUserSession(event, { + user: { + passkey: response!.credentialID, + }, + loggedInAt: Date.now(), + }) + }, + onError: (_, error) => { + console.log('Error', error) + }, + config: async (event) => { + const body = await readBody(event) + return { + rpID: getRequestURL(event).hostname, + rpName: 'My Passkey', + userName: body.userName, + userDisplayName: body.displayName, + } + }, +}) From 71e04d289385a85161c03ec8f785b4fbfbd6007e Mon Sep 17 00:00:00 2001 From: Gerben Date: Tue, 27 Aug 2024 13:37:53 +0200 Subject: [PATCH 03/77] feat: initial docs --- README.md | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c488e0b4..b2099d53 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ It can also be set using environment variables: You can add your favorite provider by creating a new file in [src/runtime/server/lib/oauth/](./src/runtime/server/lib/oauth/). -### Example +#### Example Example: `~/server/routes/auth/github.get.ts` @@ -235,6 +235,73 @@ Make sure to set the callback URL in your OAuth app settings as `/a If the redirect URL mismatch in production, this means that the module cannot guess the right redirect URL. You can set the `NUXT_OAUTH__REDIRECT_URL` env variable to overwrite the default one. +### Webauthn Event Handlers + +Webauthn and passkeys require multiple requests to be completed. The webauthn event handlers handle this on a single endpoint. + +The (very simplified) steps are as follows: + +1. Generate a registration / authentication options object +2. Store the options in a persistent storage (e.g. a database or KV store (NOT AS A COOKIE!)) +3. Retrieve the options from the persistent storage after the client has created a signature +4. Verify the signature with the created options in the first step +5. Create a user (store the public key) / login the user and set the session + +In this module you are responsible for storing and retrieving the options from a persistent storage, and storing / retrieving the user and passkey. + +For this there are two special functions you need to define for this: `storeChallenge` and `getChallenge`. + +And just like with the OAuth event handlers, there is also an `onSuccess` and `onError` function. + +The `config` function is optional and can be used to overwrite the default credential creation options and credential request options. + +#### Example + +Example: `~/server/routes/auth/webauthn/register.post.ts` + +```ts +export default definePasskeyRegistrationEventHandler({ + async storeChallenge(event, options) { + // Here we store the options in a KV store and identify it using an attempt ID + const attemptId = bufferToBase64URLString(getRandomValues(new Uint8Array(32))) + await useStorage().setItem(`attempt:${attemptId}`, options) + setCookie(event, 'passkey-attempt-id', attemptId) + }, + async getChallenge(event, options) { + const attemptId = getCookie(event, 'passkey-attempt-id') + if (!attemptId) + throw createError({ statusCode: 400 }) + + const options = await useStorage().getItem(`attempt:${attemptId}`) + + // Make sure to always remove the attempt because they are single use only! + await useStorage().removeItem(`attempt:${attemptId}`) + setCookie(event, 'passkey-attempt-id', '', { maxAge: -1 }) + + if (!options) + throw createError({ statusCode: 400 }) + + return options + }, + async onSuccess(event, response, body) { + await setUserSession(event, { + user: { + passkeyId: response.credentialID, + }, + }) + }, + onError: (event, error) => { + console.error('Webauthn registration error:', error) + }, + config: async (event) => { + return { + rpName: 'My Custom Relying Party Name', + // Other webauthn options + } + }, +}) +``` + ### Extend Session We leverage hooks to let you extend the session data with your own data or log when the user clears the session. From 61287d4b26d871285ac66033b0d55bf97fa99b2c Mon Sep 17 00:00:00 2001 From: Gerben Date: Tue, 27 Aug 2024 14:09:16 +0200 Subject: [PATCH 04/77] fix: composable type and availability functions --- playground/components/PasskeyModal.vue | 2 +- src/runtime/app/composables/passkey.ts | 14 ++++++-------- src/runtime/types/index.ts | 1 + src/runtime/types/webauthn.ts | 26 ++++++++++++++++++++++++++ 4 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 src/runtime/types/webauthn.ts diff --git a/playground/components/PasskeyModal.vue b/playground/components/PasskeyModal.vue index 5448f789..955746ce 100644 --- a/playground/components/PasskeyModal.vue +++ b/playground/components/PasskeyModal.vue @@ -56,7 +56,7 @@ async function authenticate() {