├── .changeset ├── README.md └── config.json ├── .gitignore ├── CHANGELOG.md ├── License ├── README.md ├── biome.json ├── bun.lockb ├── package.json ├── src ├── index.ts ├── large-blob-passkey-account.ts ├── passkey-connector.ts ├── passkey-provider.ts ├── passkey.ts ├── passkey.types.ts └── utils │ ├── encoding.ts │ └── webauthn-zod.ts ├── tsconfig.json └── vitest.config.js /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | 15 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 16 | 17 | # Runtime data 18 | 19 | pids 20 | _.pid 21 | _.seed 22 | \*.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | 30 | coverage 31 | \*.lcov 32 | 33 | # nyc test coverage 34 | 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | 43 | bower_components 44 | 45 | # node-waf configuration 46 | 47 | .lock-wscript 48 | 49 | # Compiled binary addons (https://nodejs.org/api/addons.html) 50 | 51 | build/Release 52 | 53 | # Dependency directories 54 | 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Snowpack dependency directory (https://snowpack.dev/) 59 | 60 | web_modules/ 61 | 62 | # TypeScript cache 63 | 64 | \*.tsbuildinfo 65 | 66 | # Optional npm cache directory 67 | 68 | .npm 69 | 70 | # Optional eslint cache 71 | 72 | .eslintcache 73 | 74 | # Optional stylelint cache 75 | 76 | .stylelintcache 77 | 78 | # Microbundle cache 79 | 80 | .rpt2_cache/ 81 | .rts2_cache_cjs/ 82 | .rts2_cache_es/ 83 | .rts2_cache_umd/ 84 | 85 | # Optional REPL history 86 | 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | 91 | \*.tgz 92 | 93 | # Yarn Integrity file 94 | 95 | .yarn-integrity 96 | 97 | # dotenv environment variable files 98 | 99 | .env 100 | .env.development.local 101 | .env.test.local 102 | .env.production.local 103 | .env.local 104 | 105 | # parcel-bundler cache (https://parceljs.org/) 106 | 107 | .cache 108 | .parcel-cache 109 | 110 | # Next.js build output 111 | 112 | .next 113 | out 114 | 115 | # Nuxt.js build / generate output 116 | 117 | .nuxt 118 | dist 119 | 120 | # Gatsby files 121 | 122 | .cache/ 123 | 124 | # Comment in the public line in if your project uses Gatsby and not Next.js 125 | 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | 128 | # public 129 | 130 | # vuepress build output 131 | 132 | .vuepress/dist 133 | 134 | # vuepress v2.x temp and cache directory 135 | 136 | .temp 137 | .cache 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.\* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @forum/passkeys 2 | 3 | ## 0.1.2 4 | 5 | ### Patch Changes 6 | 7 | - add some webauthn zod schemas 8 | 9 | ## 0.1.1 10 | 11 | ### Patch Changes 12 | 13 | - initial working version w/o exmaple 14 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Forum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Passkeys Accounts 2 | 3 | A collection of utilities to enable passkey accounts in [viem](https://viem.sh) & [wagmi](https://wagmi.sh) 4 | 5 | ## Features 6 | 7 | - Create & Import EOA into a passkey wallet & use it to interact with directly with [Ethereum](https://ethereum.org/) 8 | - Bring your own passkey library, such as [SimpleWebAuthn](https://github.com/MasterKale/SimpleWebAuthn) or [React Native Passkeys](https://github.com/peterferguson/react-native-passkeys) 9 | - (Mostly) Unopinionated about how you store and handle the private key 10 | 11 | ... This is a work in progress so if you find any issues please let us know. 12 | 13 | 14 | ## Installation 15 | 16 | Install wagmi, viem & @forum/passkeys 17 | 18 | ```bash 19 | pnpm install wagmi viem @forum/passkeys 20 | ``` 21 | 22 | 23 | ## Quick Start 24 | 25 | This is one possible way to use this library to get started with largeBlob passkey accounts in wagmi. 26 | 27 | It splits the setup process into three steps: 28 | 29 | 1. First define a class to represent your site's passkey. This can wrap the `navigator.credential` api itself or some library like `SimpleWebAuthn` or `react-native-passkeys`. 30 | 31 | This should handle the calls to your server to verify that the calls are legit. [See SimpleWebAuthn's server docs](`https://simplewebauthn.dev/docs/packages/server#2-verify-registration-response`) for an example of how to handle the verification. 32 | 33 | ```tsx 34 | 35 | import { Passkey as AbstractPasskey } from '@forum/passkeys' 36 | 37 | export class Passkey extends AbstractPasskey { 38 | 39 | // - init your relaying party parameters 40 | // ... 41 | 42 | async create(options): Promise { 43 | const { challenge } = await getChallengeFromServer() 44 | 45 | const passkeyResult = await await navigator.credential.create({ 46 | ...options, 47 | challenge 48 | }) 49 | 50 | if (!passkeyResult) throw new Error('Failed to create passkey') 51 | 52 | const verified = await getVerifiedPasskeyResult(passkeyResult) 53 | 54 | if (!verified) throw new Error('Failed to verify challenge') 55 | 56 | return passkeyResult 57 | } 58 | 59 | async get(options): Promise { 60 | const { challenge } = await getChallengeFromServer() 61 | 62 | const passkeyResult = await navigator.credential.get({ 63 | ...options, 64 | rpId: hostname, 65 | challenge 66 | }) 67 | 68 | const verified = await getVerifiedPasskeyResult(passkeyResult) 69 | 70 | if (!verified) throw new Error('Failed to verify challenge') 71 | 72 | return passkeyResult 73 | } 74 | } 75 | ``` 76 | 77 | 2. Define a custom hook to create the account 78 | ```tsx 79 | import { useAccount, useConfig, useConnect, useDisconnect } from 'wagmi' 80 | import { generatePrivateKey, privateKeyToAddress } from 'viem/accounts' 81 | import { PasskeyConnector } from '@forum/passkeys' 82 | 83 | export const useCreateAccount() { 84 | const config = useConfig() 85 | 86 | const createAccount = async ( 87 | username: string, 88 | privateKey = generatePrivateKey() 89 | ) => { 90 | const passkey = new ForumPasskey() 91 | const address = privateKeyToAddress(privateKey) 92 | 93 | // - generate the initial passkey for the new user & check that they are 94 | // - using a device/browser that supports `largeBlob` webauthn extension 95 | const credential = await passkey.create({ 96 | user: { 97 | id: base64URLFromString(address), 98 | name: username, 99 | displayName: username, 100 | }, 101 | extensions: { largeBlob: { support: 'required' } }, 102 | }) 103 | 104 | if (!credential?.clientExtensionResults?.largeBlob?.supported) 105 | throw new Error('LargeBlob not supported') 106 | 107 | // - optional: if you have access to a secure store (e.g. keychain access) 108 | // - you can store the pk at this point 109 | await storeInYourOwnSecureStoreForPrivateKeys({ 110 | credentialId: credential.id, 111 | privateKey 112 | }) 113 | 114 | // - init the viem passkey account 115 | const largeBlobAccount = new LargeBlobPasskeyAccount({ 116 | passkey: new ForumPasskey(), 117 | privateKey 118 | }) 119 | 120 | // - init the wagmi passkey connector 121 | const connector = new PasskeyConnector({ 122 | account: largeBlobAccount.toAccount(), 123 | config, 124 | }) 125 | 126 | connect({ connector }) 127 | 128 | // - you could choose to delay the following (storing the large blob) 129 | // - until the users first tx but for the example we do it here 130 | const write = await passkey.get({ 131 | extensions: { largeBlob: { write: privateKey } }, 132 | allowCredentials: [{ type: 'public-key', id: credential.id }], 133 | }) 134 | 135 | if(!write?.clientExtensionResults.largeBlob?.written) 136 | throw new Error('failed to store large blob') 137 | 138 | return credential 139 | } 140 | 141 | return { createAccount } 142 | } 143 | ``` 144 | 145 | 3. Integrate the hook into a normal wagmi connect flow 146 | ```tsx 147 | import { useAccount, useConfig, useConnect, useDisconnect } from 'wagmi' 148 | import { generatePrivateKey, privateKeyToAddress } from 'viem/accounts' 149 | import { PasskeyConnector } from '@forum/passkeys' 150 | import { useCreateAccount } from './use-create-account.ts' 151 | 152 | function Profile() { 153 | const { address } = useAccount() 154 | const { connect } = useConnect() 155 | const { disconnect } = useDisconnect() 156 | const { createAccount } = useCreateAccount() 157 | 158 | if (address) { 159 | return ( 160 |
161 | Connected to { address } 162 |