diff --git a/.agents/plans/swift-pink-sky-simplewebauthn-v13-upgrade.md b/.agents/plans/swift-pink-sky-simplewebauthn-v13-upgrade.md new file mode 100644 index 000000000..8ca225c13 --- /dev/null +++ b/.agents/plans/swift-pink-sky-simplewebauthn-v13-upgrade.md @@ -0,0 +1,186 @@ +--- +date: 2026-01-14 +title: Simplewebauthn V13 Upgrade +--- + +## Overview + +Upgrade SimpleWebAuthn packages from v9.x to v13.x to address the deprecation of `@simplewebauthn/types` and take advantage of new features and improvements. + +## Current State + +The codebase currently uses: +- `@simplewebauthn/browser@9.x` +- `@simplewebauthn/server@9.x` +- `@simplewebauthn/types@9.x` + +## Breaking Changes Summary (v9 → v13) + +### v10.0.0 Breaking Changes +1. **Minimum Node version raised to Node v20** +2. **`generateRegistrationOptions()` now expects `Base64URLString` for `excludeCredentials` IDs** (no more `type: 'public-key'` needed) +3. **`generateAuthenticationOptions()` now expects `Base64URLString` for `allowCredentials` IDs** +4. **`credentialID` returned from verification methods is now `Base64URLString`** instead of `Uint8Array` +5. **`AuthenticatorDevice.credentialID` is now `Base64URLString`** +6. **`rpID` is now required when calling `generateAuthenticationOptions()`** +7. **`generateRegistrationOptions()` will generate random user IDs** if not provided +8. **`user.id` is treated as base64url string in `startRegistration()`** +9. **`userHandle` is treated as base64url string in `startAuthentication()`** + +### v11.0.0 Breaking Changes +1. **Positional arguments in `startRegistration()` and `startAuthentication()` replaced by object** + - Before: `startRegistration(options)` + - After: `startRegistration({ optionsJSON: options })` + - Before: `startAuthentication(options)` + - After: `startAuthentication({ optionsJSON: options })` +2. **`AuthenticatorDevice` type renamed to `WebAuthnCredential`** + - `credentialID` → `credential.id` + - `credentialPublicKey` → `credential.publicKey` +3. **`verifyRegistrationResponse()` returns `registrationInfo.credential` instead of individual properties** + - `credentialID` → `credential.id` + - `credentialPublicKey` → `credential.publicKey` + - `counter` → `credential.counter` + - `transports` are now in `credential.transports` +4. **`verifyAuthenticationResponse()` uses `credential` argument instead of `authenticator`** + +### v13.0.0 Breaking Changes +1. **`@simplewebauthn/types` package is retired** + - Types are now exported from `@simplewebauthn/browser` and `@simplewebauthn/server` + - Import types from `@simplewebauthn/server` instead + +## Files to Update + +### Package Changes +1. Remove `@simplewebauthn/types` dependency +2. Update `@simplewebauthn/browser` to `^13.2.2` +3. Update `@simplewebauthn/server` to `^13.2.2` + +### Server-side Files + +#### 1. `packages/lib/server-only/auth/create-passkey-registration-options.ts` +- Change import from `@simplewebauthn/types` to `@simplewebauthn/server` +- Remove `type: 'public-key'` from `excludeCredentials` items +- Update `userID` to use `isoUint8Array.fromUTF8String()` for proper encoding + +#### 2. `packages/lib/server-only/auth/create-passkey-authentication-options.ts` +- Change import from `@simplewebauthn/types` to `@simplewebauthn/server` +- Remove `type: 'public-key'` from `allowCredentials` items + +#### 3. `packages/lib/server-only/auth/create-passkey-signin-options.ts` +- No changes needed (already using correct options) + +#### 4. `packages/lib/server-only/auth/create-passkey.ts` +- Change import from `@simplewebauthn/types` to `@simplewebauthn/server` +- Update to use new `registrationInfo.credential` structure: + - `credentialID` → `credential.id` + - `credentialPublicKey` → `credential.publicKey` + - `counter` → `credential.counter` +- Note: `credential.id` is now a `Base64URLString`, so `Buffer.from(credentialID)` needs updating + +#### 5. `packages/lib/server-only/document/is-recipient-authorized.ts` +- Update `verifyAuthenticationResponse()` to use `credential` instead of `authenticator`: + - Change `authenticator: { credentialID, credentialPublicKey, counter }` to `credential: { id, publicKey, counter }` +- Since `credential.id` is now base64url string, convert stored `credentialId` buffer to base64url + +#### 6. `packages/auth/server/routes/passkey.ts` +- Update `verifyAuthenticationResponse()` to use `credential` instead of `authenticator` +- Same changes as `is-recipient-authorized.ts` + +#### 7. `packages/trpc/server/auth-router/create-passkey.ts` +- Change import from `@simplewebauthn/types` to `@simplewebauthn/server` + +### Browser-side Files + +#### 8. `apps/remix/app/components/dialogs/passkey-create-dialog.tsx` +- Update `startRegistration()` call: + - Before: `startRegistration(passkeyRegistrationOptions)` + - After: `startRegistration({ optionsJSON: passkeyRegistrationOptions })` + +#### 9. `apps/remix/app/components/forms/signin.tsx` +- Update `startAuthentication()` call: + - Before: `startAuthentication(options)` + - After: `startAuthentication({ optionsJSON: options })` + +#### 10. `apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx` +- Update `startAuthentication()` call: + - Before: `startAuthentication(options)` + - After: `startAuthentication({ optionsJSON: options })` + +### Database/Schema Considerations + +The database stores `credentialId` as `Bytes`. The new API returns `credential.id` as `Base64URLString`. We need to: +1. When **storing** a new passkey: Convert from `Base64URLString` to `Buffer` +2. When **passing to verification**: Convert from `Buffer` to `Base64URLString` + +Use `isoBase64URL` helper from `@simplewebauthn/server/helpers` for these conversions. + +## Implementation Steps + +### Step 1: Update package.json dependencies +```bash +npm uninstall @simplewebauthn/types +npm install @simplewebauthn/browser@^13.2.2 @simplewebauthn/server@^13.2.2 +``` + +### Step 2: Update type imports +Replace all `@simplewebauthn/types` imports with `@simplewebauthn/server` + +### Step 3: Update browser-side API calls +- `startRegistration(options)` → `startRegistration({ optionsJSON: options })` +- `startAuthentication(options)` → `startAuthentication({ optionsJSON: options })` + +### Step 4: Update server-side registration +- Update `excludeCredentials` format (remove `type: 'public-key'`) +- Update `userID` encoding if needed +- Update `verifyRegistrationResponse()` result handling for new `credential` structure + +### Step 5: Update server-side authentication +- Update `allowCredentials` format (remove `type: 'public-key'`) +- Update `verifyAuthenticationResponse()` to use `credential` instead of `authenticator` +- Handle `Base64URLString` for `credential.id` + +### Step 6: Update credential storage/retrieval +- When storing: Convert `Base64URLString` to `Buffer` +- When reading: Convert `Buffer` to `Base64URLString` + +### Step 7: Test passkey flows +1. Test passkey creation +2. Test passkey sign-in +3. Test passkey authentication for document signing +4. Test passkey deletion + +## Code Examples + +### Converting stored Buffer to Base64URLString for verification +```typescript +import { isoBase64URL } from '@simplewebauthn/server/helpers'; + +// When reading from database (Buffer) and passing to verification +const credential = { + id: isoBase64URL.fromBuffer(passkey.credentialId), + publicKey: new Uint8Array(passkey.credentialPublicKey), + counter: Number(passkey.counter), + transports: passkey.transports, +}; +``` + +### Converting Base64URLString to Buffer for storage +```typescript +import { isoBase64URL } from '@simplewebauthn/server/helpers'; + +// When storing from registration response +const credentialIdBuffer = Buffer.from( + isoBase64URL.toBuffer(registrationInfo.credential.id) +); +``` + +## Risks and Mitigations + +1. **Database compatibility**: The `credentialId` is stored as `Bytes` in the database. The new API uses `Base64URLString`. We need proper conversion functions. + - **Mitigation**: Use `isoBase64URL.fromBuffer()` and `isoBase64URL.toBuffer()` for conversions + +2. **Existing passkeys**: Existing passkeys should continue to work as long as conversion is done correctly. + - **Mitigation**: Test with existing passkeys after upgrade + +3. **Browser compatibility**: v10+ requires newer browser APIs. + - **Mitigation**: `browserSupportsWebAuthn()` already handles this check \ No newline at end of file diff --git a/apps/remix/app/components/dialogs/passkey-create-dialog.tsx b/apps/remix/app/components/dialogs/passkey-create-dialog.tsx index 77bd54b57..541d2d4c7 100644 --- a/apps/remix/app/components/dialogs/passkey-create-dialog.tsx +++ b/apps/remix/app/components/dialogs/passkey-create-dialog.tsx @@ -73,7 +73,9 @@ export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCre try { const passkeyRegistrationOptions = await createPasskeyRegistrationOptions(); - const registrationResult = await startRegistration(passkeyRegistrationOptions); + const registrationResult = await startRegistration({ + optionsJSON: passkeyRegistrationOptions, + }); await createPasskey({ passkeyName, diff --git a/apps/remix/app/components/forms/signin.tsx b/apps/remix/app/components/forms/signin.tsx index 625b92af5..6338512fe 100644 --- a/apps/remix/app/components/forms/signin.tsx +++ b/apps/remix/app/components/forms/signin.tsx @@ -171,7 +171,7 @@ export const SignInForm = ({ const { options, sessionId } = await createPasskeySigninOptions(); - const credential = await startAuthentication(options); + const credential = await startAuthentication({ optionsJSON: options }); await authClient.passkey.signIn({ credential: JSON.stringify(credential), diff --git a/apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx b/apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx index 930738c74..9cdb2e28a 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx @@ -90,7 +90,7 @@ export const DocumentSigningAuthPasskey = ({ preferredPasskeyId: passkeyId, }); - const authenticationResponse = await startAuthentication(options); + const authenticationResponse = await startAuthentication({ optionsJSON: options }); await onReauthFormSubmit({ type: DocumentAuth.PASSKEY, @@ -146,7 +146,7 @@ export const DocumentSigningAuthPasskey = ({ if (passkeyData.isInitialLoading || (passkeyData.isError && passkeyData.passkeys.length === 0)) { return (
- +
); } diff --git a/apps/remix/package.json b/apps/remix/package.json index 1308dfc46..23be27c2d 100644 --- a/apps/remix/package.json +++ b/apps/remix/package.json @@ -38,8 +38,8 @@ "@oslojs/encoding": "^1.1.0", "@react-router/node": "^7.12.0", "@react-router/serve": "^7.12.0", - "@simplewebauthn/browser": "^9.0.1", - "@simplewebauthn/server": "^9.0.3", + "@simplewebauthn/browser": "^13.2.2", + "@simplewebauthn/server": "^13.2.2", "@tanstack/react-query": "5.90.10", "autoprefixer": "^10.4.22", "colord": "^2.9.3", @@ -88,7 +88,6 @@ "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-typescript": "^12.3.0", - "@simplewebauthn/types": "^9.0.1", "@types/content-disposition": "^0.5.9", "@types/formidable": "^3.4.6", "@types/luxon": "^3.7.1", diff --git a/package-lock.json b/package-lock.json index 6e9d47501..d898bd9ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -135,8 +135,8 @@ "@oslojs/encoding": "^1.1.0", "@react-router/node": "^7.12.0", "@react-router/serve": "^7.12.0", - "@simplewebauthn/browser": "^9.0.1", - "@simplewebauthn/server": "^9.0.3", + "@simplewebauthn/browser": "^13.2.2", + "@simplewebauthn/server": "^13.2.2", "@tanstack/react-query": "5.90.10", "autoprefixer": "^10.4.22", "colord": "^2.9.3", @@ -185,7 +185,6 @@ "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-typescript": "^12.3.0", - "@simplewebauthn/types": "^9.0.1", "@types/content-disposition": "^0.5.9", "@types/formidable": "^3.4.6", "@types/luxon": "^3.7.1", @@ -11944,6 +11943,31 @@ "tslib": "^2.8.1" } }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.0.tgz", + "integrity": "sha512-2uZqP+ggSncESeUF/9Su8rWqGclEfEiz1SyU02WX5fUONFfkjzS2Z/F1Li0ofSmf4JqYXIOdCAZqIXAIBAT1OA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509-attr": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.0.tgz", + "integrity": "sha512-BeWIu5VpTIhfRysfEp73SGbwjjoLL/JWXhJ/9mo4vXnz3tRGm+NGm3KNcRzQ9VMVqwYS2RHlolz21svzRXIHPQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, "node_modules/@peculiar/asn1-ecc": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.0.tgz", @@ -11956,6 +11980,48 @@ "tslib": "^2.8.1" } }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.0.tgz", + "integrity": "sha512-rtUvtf+tyKGgokHHmZzeUojRZJYPxoD/jaN1+VAB4kKR7tXrnDCA/RAWXAIhMJJC+7W27IIRGe9djvxKgsldCQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-pkcs8": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.0.tgz", + "integrity": "sha512-KyQ4D8G/NrS7Fw3XCJrngxmjwO/3htnA0lL9gDICvEQ+GJ+EPFqldcJQTwPIdvx98Tua+WjkdKHSC0/Km7T+lA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.0.tgz", + "integrity": "sha512-b78OQ6OciW0aqZxdzliXGYHASeCvvw5caqidbpQRYW2mBtXIX2WhofNXTEe7NyxTb0P6J62kAAWLwn0HuMF1Fw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-pfx": "^2.6.0", + "@peculiar/asn1-pkcs8": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509-attr": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, "node_modules/@peculiar/asn1-rsa": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.0.tgz", @@ -11991,6 +12057,40 @@ "tslib": "^2.8.1" } }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.0.tgz", + "integrity": "sha512-MuIAXFX3/dc8gmoZBkwJWxUWOSvG4MMDntXhrOZpJVMkYX+MYc/rUAU2uJOved9iJEoiUx7//3D8oG83a78UJA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@pinojs/redact": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", @@ -15177,18 +15277,15 @@ "license": "MIT" }, "node_modules/@simplewebauthn/browser": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-9.0.1.tgz", - "integrity": "sha512-wD2WpbkaEP4170s13/HUxPcAV5y4ZXaKo1TfNklS5zDefPinIgXOpgz1kpEvobAsaLPa2KeH7AKKX/od1mrBJw==", - "license": "MIT", - "dependencies": { - "@simplewebauthn/types": "^9.0.1" - } + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.2.2.tgz", + "integrity": "sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA==", + "license": "MIT" }, "node_modules/@simplewebauthn/server": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-9.0.3.tgz", - "integrity": "sha512-FMZieoBosrVLFxCnxPFD9Enhd1U7D8nidVDT4MsHc6l4fdVcjoeHjDueeXCloO1k5O/fZg1fsSXXPKbY2XTzDA==", + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.2.2.tgz", + "integrity": "sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA==", "license": "MIT", "dependencies": { "@hexagon/base64": "^1.1.27", @@ -15198,20 +15295,12 @@ "@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8", - "@simplewebauthn/types": "^9.0.1", - "cross-fetch": "^4.0.0" + "@peculiar/x509": "^1.13.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=20.0.0" } }, - "node_modules/@simplewebauthn/types": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-9.0.1.tgz", - "integrity": "sha512-tGSRP1QvsAvsJmnOlRQyw/mvK9gnPtjEc5fg2+m8n+QUa+D7rvrKkOYyfpy42GTs90X3RDOnqJgfHt+qO67/+w==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "MIT" - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -32199,6 +32288,12 @@ "@babel/runtime": "^7.9.2" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -35224,6 +35319,24 @@ "fsevents": "~2.3.3" } }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/turbo": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/turbo/-/turbo-1.13.4.tgz", @@ -37005,6 +37118,7 @@ "@hono/standard-validator": "^0.2.0", "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0", + "@simplewebauthn/server": "^13.2.2", "arctic": "^3.7.0", "hono": "4.10.6", "luxon": "^3.7.2", @@ -37359,6 +37473,7 @@ "@node-rs/bcrypt": "^1.10.7", "@pdf-lib/fontkit": "^1.1.1", "@scure/base": "^1.2.6", + "@simplewebauthn/server": "^13.2.2", "@sindresorhus/slugify": "^3.0.0", "@team-plain/typescript-sdk": "^5.11.0", "@vvo/tzdb": "^6.196.0", @@ -37461,6 +37576,7 @@ "dependencies": { "@documenso/lib": "*", "@documenso/prisma": "*", + "@simplewebauthn/server": "^13.2.2", "@tanstack/react-query": "5.90.10", "@trpc/client": "11.8.1", "@trpc/react-query": "11.8.1", diff --git a/package.json b/package.json index 3c38f26bc..7dff318d6 100644 --- a/package.json +++ b/package.json @@ -103,4 +103,4 @@ "typescript": "5.6.2", "zod": "^3.25.76" } -} +} \ No newline at end of file diff --git a/packages/auth/package.json b/packages/auth/package.json index 7566d6eb8..0c3dda9a1 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -15,6 +15,7 @@ "@hono/standard-validator": "^0.2.0", "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0", + "@simplewebauthn/server": "^13.2.2", "arctic": "^3.7.0", "hono": "4.10.6", "luxon": "^3.7.2", @@ -22,4 +23,4 @@ "ts-pattern": "^5.9.0", "zod": "^3.25.76" } -} \ No newline at end of file +} diff --git a/packages/auth/server/routes/passkey.ts b/packages/auth/server/routes/passkey.ts index 0d0b43854..1bb3e16de 100644 --- a/packages/auth/server/routes/passkey.ts +++ b/packages/auth/server/routes/passkey.ts @@ -1,6 +1,7 @@ import { sValidator } from '@hono/standard-validator'; import { UserSecurityAuditLogType } from '@prisma/client'; import { verifyAuthenticationResponse } from '@simplewebauthn/server'; +import { isoBase64URL } from '@simplewebauthn/server/helpers'; import { Hono } from 'hono'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; @@ -80,9 +81,9 @@ export const passkeyRoute = new Hono() expectedChallenge: challengeToken.token, expectedOrigin: origin, expectedRPID: rpId, - authenticator: { - credentialID: new Uint8Array(Array.from(passkey.credentialId)), - credentialPublicKey: new Uint8Array(passkey.credentialPublicKey), + credential: { + id: isoBase64URL.fromBuffer(passkey.credentialId), + publicKey: new Uint8Array(passkey.credentialPublicKey), counter: Number(passkey.counter), }, }).catch(() => null); diff --git a/packages/lib/package.json b/packages/lib/package.json index b48a77fb7..f324f258e 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -34,6 +34,7 @@ "@node-rs/bcrypt": "^1.10.7", "@pdf-lib/fontkit": "^1.1.1", "@scure/base": "^1.2.6", + "@simplewebauthn/server": "^13.2.2", "@sindresorhus/slugify": "^3.0.0", "@team-plain/typescript-sdk": "^5.11.0", "@vvo/tzdb": "^6.196.0", diff --git a/packages/lib/server-only/auth/create-passkey-authentication-options.ts b/packages/lib/server-only/auth/create-passkey-authentication-options.ts index 2c35f46d7..3efe84041 100644 --- a/packages/lib/server-only/auth/create-passkey-authentication-options.ts +++ b/packages/lib/server-only/auth/create-passkey-authentication-options.ts @@ -1,6 +1,7 @@ import type { Passkey } from '@prisma/client'; import { generateAuthenticationOptions } from '@simplewebauthn/server'; -import type { AuthenticatorTransportFuture } from '@simplewebauthn/types'; +import type { AuthenticatorTransportFuture } from '@simplewebauthn/server'; +import { isoBase64URL } from '@simplewebauthn/server/helpers'; import { DateTime } from 'luxon'; import { prisma } from '@documenso/prisma'; @@ -53,8 +54,7 @@ export const createPasskeyAuthenticationOptions = async ({ allowCredentials: preferredPasskey ? [ { - id: preferredPasskey.credentialId, - type: 'public-key', + id: isoBase64URL.fromBuffer(preferredPasskey.credentialId), // eslint-disable-next-line @typescript-eslint/consistent-type-assertions transports: preferredPasskey.transports as AuthenticatorTransportFuture[], }, diff --git a/packages/lib/server-only/auth/create-passkey-registration-options.ts b/packages/lib/server-only/auth/create-passkey-registration-options.ts index 8f2b3d53a..8c8895ed4 100644 --- a/packages/lib/server-only/auth/create-passkey-registration-options.ts +++ b/packages/lib/server-only/auth/create-passkey-registration-options.ts @@ -1,5 +1,6 @@ import { generateRegistrationOptions } from '@simplewebauthn/server'; -import type { AuthenticatorTransportFuture } from '@simplewebauthn/types'; +import type { AuthenticatorTransportFuture } from '@simplewebauthn/server'; +import { isoBase64URL } from '@simplewebauthn/server/helpers'; import { DateTime } from 'luxon'; import { prisma } from '@documenso/prisma'; @@ -32,14 +33,13 @@ export const createPasskeyRegistrationOptions = async ({ const options = await generateRegistrationOptions({ rpName, rpID, - userID: userId.toString(), + userID: Buffer.from(userId.toString()), userName: user.email, userDisplayName: user.name ?? undefined, timeout: PASSKEY_TIMEOUT, attestationType: 'none', excludeCredentials: passkeys.map((passkey) => ({ - id: passkey.credentialId, - type: 'public-key', + id: isoBase64URL.fromBuffer(passkey.credentialId), // eslint-disable-next-line @typescript-eslint/consistent-type-assertions transports: passkey.transports as AuthenticatorTransportFuture[], })), diff --git a/packages/lib/server-only/auth/create-passkey.ts b/packages/lib/server-only/auth/create-passkey.ts index 5aa6a138c..7ac53d1d8 100644 --- a/packages/lib/server-only/auth/create-passkey.ts +++ b/packages/lib/server-only/auth/create-passkey.ts @@ -1,6 +1,7 @@ import { UserSecurityAuditLogType } from '@prisma/client'; import { verifyRegistrationResponse } from '@simplewebauthn/server'; -import type { RegistrationResponseJSON } from '@simplewebauthn/types'; +import type { RegistrationResponseJSON } from '@simplewebauthn/server'; +import { isoBase64URL } from '@simplewebauthn/server/helpers'; import { prisma } from '@documenso/prisma'; @@ -83,20 +84,19 @@ export const createPasskey = async ({ }); } - const { credentialPublicKey, credentialID, counter, credentialDeviceType, credentialBackedUp } = - verification.registrationInfo; + const { credentialDeviceType, credentialBackedUp, credential } = verification.registrationInfo; await prisma.$transaction(async (tx) => { await tx.passkey.create({ data: { userId, name: passkeyName, - credentialId: Buffer.from(credentialID), - credentialPublicKey: Buffer.from(credentialPublicKey), - counter, + credentialId: Buffer.from(isoBase64URL.toBuffer(credential.id)), + credentialPublicKey: Buffer.from(credential.publicKey), + counter: credential.counter, credentialDeviceType, credentialBackedUp, - transports: verificationResponse.response.transports, + transports: credential.transports, }, }); diff --git a/packages/lib/server-only/document/is-recipient-authorized.ts b/packages/lib/server-only/document/is-recipient-authorized.ts index d19880372..affdcc0d5 100644 --- a/packages/lib/server-only/document/is-recipient-authorized.ts +++ b/packages/lib/server-only/document/is-recipient-authorized.ts @@ -1,5 +1,6 @@ import type { Envelope, Recipient } from '@prisma/client'; import { verifyAuthenticationResponse } from '@simplewebauthn/server'; +import { isoBase64URL } from '@simplewebauthn/server/helpers'; import { match } from 'ts-pattern'; import { prisma } from '@documenso/prisma'; @@ -252,9 +253,9 @@ const verifyPasskey = async ({ expectedChallenge: verificationToken.token, expectedOrigin: origin, expectedRPID: rpId, - authenticator: { - credentialID: new Uint8Array(Array.from(passkey.credentialId)), - credentialPublicKey: new Uint8Array(passkey.credentialPublicKey), + credential: { + id: isoBase64URL.fromBuffer(passkey.credentialId), + publicKey: new Uint8Array(passkey.credentialPublicKey), counter: Number(passkey.counter), }, }).catch(() => null); // May want to log this for insights. diff --git a/packages/trpc/package.json b/packages/trpc/package.json index fadc8d658..139ee58f2 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -12,6 +12,7 @@ "dependencies": { "@documenso/lib": "*", "@documenso/prisma": "*", + "@simplewebauthn/server": "^13.2.2", "@tanstack/react-query": "5.90.10", "@trpc/client": "11.8.1", "@trpc/react-query": "11.8.1", diff --git a/packages/trpc/server/auth-router/create-passkey.ts b/packages/trpc/server/auth-router/create-passkey.ts index 42b97b45b..0eaa369ea 100644 --- a/packages/trpc/server/auth-router/create-passkey.ts +++ b/packages/trpc/server/auth-router/create-passkey.ts @@ -1,4 +1,4 @@ -import type { RegistrationResponseJSON } from '@simplewebauthn/types'; +import type { RegistrationResponseJSON } from '@simplewebauthn/server'; import { createPasskey } from '@documenso/lib/server-only/auth/create-passkey';