diff --git a/node/authenticate-with-passkey/.gitignore b/node/authenticate-with-passkey/.gitignore
new file mode 100644
index 00000000..6a7d6d8e
--- /dev/null
+++ b/node/authenticate-with-passkey/.gitignore
@@ -0,0 +1,130 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+.cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
\ No newline at end of file
diff --git a/node/authenticate-with-passkey/.prettierrc.json b/node/authenticate-with-passkey/.prettierrc.json
new file mode 100644
index 00000000..0a725205
--- /dev/null
+++ b/node/authenticate-with-passkey/.prettierrc.json
@@ -0,0 +1,6 @@
+{
+ "trailingComma": "es5",
+ "tabWidth": 2,
+ "semi": true,
+ "singleQuote": true
+}
diff --git a/node/authenticate-with-passkey/README.md b/node/authenticate-with-passkey/README.md
new file mode 100644
index 00000000..3a6a63c7
--- /dev/null
+++ b/node/authenticate-with-passkey/README.md
@@ -0,0 +1,47 @@
+# 📬 Node.js Authenticate with Passkey
+
+Sign-in with passkey into Appwrite Account.
+
+## 🧰 Usage
+
+Read tutorial [article on Dev.to](https://dev.to/meldiron/biometric-authentication-with-passkeys-3e1) to learn more.
+
+## ⚙️ Configuration
+
+| Setting | Value |
+| ----------------- | --------------- |
+| Runtime | Node (18.0) |
+| Entrypoint | `src/main.js` |
+| Build Commands | `npm install && npm run setup` |
+| Permissions | `any` |
+| Timeout (Seconds) | 15 |
+
+## 🔒 Environment Variables
+
+### ALLOWED_HOSTNAME
+
+Hostname (like `myapp.com`, without protocol or port) that is allowed to use passkey authentication.
+
+| Question | Answer |
+| ------------ | ------------------------------ |
+| Required | No |
+| Sample Value | `passkeydemo.appwrite.global` |
+
+### APPWRITE_API_KEY
+
+API Key to talk to Appwrite backend APIs.
+
+| Question | Answer |
+| ------------- | -------------------------------------------------------------------------------------------------- |
+| Required | Yes |
+| Sample Value | `d1efb...aec35` |
+| Documentation | [Appwrite: Getting Started for Server](https://appwrite.io/docs/advanced/platform/api-keys) |
+
+### APPWRITE_ENDPOINT
+
+The URL endpoint of the Appwrite server. If not provided, it defaults to the Appwrite Cloud server: `https://cloud.appwrite.io/v1`.
+
+| Question | Answer |
+| ------------ | ------------------------------ |
+| Required | No |
+| Sample Value | `https://cloud.appwrite.io/v1` |
\ No newline at end of file
diff --git a/node/authenticate-with-passkey/package-lock.json b/node/authenticate-with-passkey/package-lock.json
new file mode 100644
index 00000000..8b4632cd
--- /dev/null
+++ b/node/authenticate-with-passkey/package-lock.json
@@ -0,0 +1,430 @@
+{
+ "name": "authenticate-with-passkey",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "authenticate-with-passkey",
+ "version": "1.0.0",
+ "dependencies": {
+ "@simplewebauthn/server": "^9.0.2",
+ "node-appwrite": "npm:@appwrite.io/console@0.6.0-rc.8"
+ },
+ "devDependencies": {
+ "prettier": "^3.0.0"
+ }
+ },
+ "node_modules/@cbor-extract/cbor-extract-darwin-arm64": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz",
+ "integrity": "sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@cbor-extract/cbor-extract-darwin-x64": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz",
+ "integrity": "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@cbor-extract/cbor-extract-linux-arm": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz",
+ "integrity": "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==",
+ "cpu": [
+ "arm"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@cbor-extract/cbor-extract-linux-arm64": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz",
+ "integrity": "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@cbor-extract/cbor-extract-linux-x64": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz",
+ "integrity": "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@cbor-extract/cbor-extract-win32-x64": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz",
+ "integrity": "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "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/@peculiar/asn1-android": {
+ "version": "2.3.10",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.3.10.tgz",
+ "integrity": "sha512-z9Rx9cFJv7UUablZISe7uksNbFJCq13hO0yEAOoIpAymALTLlvUOSLnGiQS7okPaM5dP42oTLhezH6XDXRXjGw==",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.3.8",
+ "asn1js": "^3.0.5",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@peculiar/asn1-ecc": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.8.tgz",
+ "integrity": "sha512-Ah/Q15y3A/CtxbPibiLM/LKcMbnLTdUdLHUgdpB5f60sSvGkXzxJCu5ezGTFHogZXWNX3KSmYqilCrfdmBc6pQ==",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.3.8",
+ "@peculiar/asn1-x509": "^2.3.8",
+ "asn1js": "^3.0.5",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@peculiar/asn1-rsa": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.3.8.tgz",
+ "integrity": "sha512-ES/RVEHu8VMYXgrg3gjb1m/XG0KJWnV4qyZZ7mAg7rrF3VTmRbLxO8mk+uy0Hme7geSMebp+Wvi2U6RLLEs12Q==",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.3.8",
+ "@peculiar/asn1-x509": "^2.3.8",
+ "asn1js": "^3.0.5",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@peculiar/asn1-schema": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.8.tgz",
+ "integrity": "sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==",
+ "dependencies": {
+ "asn1js": "^3.0.5",
+ "pvtsutils": "^1.3.5",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@peculiar/asn1-x509": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.3.8.tgz",
+ "integrity": "sha512-voKxGfDU1c6r9mKiN5ZUsZWh3Dy1BABvTM3cimf0tztNwyMJPhiXY94eRTgsMQe6ViLfT6EoXxkWVzcm3mFAFw==",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.3.8",
+ "asn1js": "^3.0.5",
+ "ipaddr.js": "^2.1.0",
+ "pvtsutils": "^1.3.5",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@simplewebauthn/server": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-9.0.2.tgz",
+ "integrity": "sha512-aaWA+qVOU4byk5IDb/l+M1+7dmrAJhTb4ISJHucpsgRQcMMEes76tbGIqO2JQuA7N50tc/OBrnGKBjoKYG1kSw==",
+ "dependencies": {
+ "@hexagon/base64": "^1.1.27",
+ "@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": "^9.0.1",
+ "cbor-x": "^1.5.2",
+ "cross-fetch": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.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=="
+ },
+ "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/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ },
+ "node_modules/cbor-extract": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.2.0.tgz",
+ "integrity": "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==",
+ "hasInstallScript": true,
+ "optional": true,
+ "dependencies": {
+ "node-gyp-build-optional-packages": "5.1.1"
+ },
+ "bin": {
+ "download-cbor-prebuilds": "bin/download-prebuilds.js"
+ },
+ "optionalDependencies": {
+ "@cbor-extract/cbor-extract-darwin-arm64": "2.2.0",
+ "@cbor-extract/cbor-extract-darwin-x64": "2.2.0",
+ "@cbor-extract/cbor-extract-linux-arm": "2.2.0",
+ "@cbor-extract/cbor-extract-linux-arm64": "2.2.0",
+ "@cbor-extract/cbor-extract-linux-x64": "2.2.0",
+ "@cbor-extract/cbor-extract-win32-x64": "2.2.0"
+ }
+ },
+ "node_modules/cbor-x": {
+ "version": "1.5.8",
+ "resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.5.8.tgz",
+ "integrity": "sha512-gc3bHBsvG6GClCY6c0/iip+ghlqizkVp+TtaL927lwvP4VP9xBdi1HmqPR5uj/Mj/0TOlngMkIYa25wKg+VNrQ==",
+ "optionalDependencies": {
+ "cbor-extract": "^2.2.0"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "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/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
+ "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
+ "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.6",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 0.12"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz",
+ "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/isomorphic-form-data": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isomorphic-form-data/-/isomorphic-form-data-2.0.0.tgz",
+ "integrity": "sha512-TYgVnXWeESVmQSg4GLVbalmQ+B4NPi/H4eWxqALKj63KsUrcu301YDjBqaOw3h+cbak7Na4Xyps3BiptHtxTfg==",
+ "dependencies": {
+ "form-data": "^2.3.2"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-appwrite": {
+ "name": "@appwrite.io/console",
+ "version": "0.6.0-rc.8",
+ "resolved": "https://registry.npmjs.org/@appwrite.io/console/-/console-0.6.0-rc.8.tgz",
+ "integrity": "sha512-VVOAk5PhfD017U/vVdfzecbhJEXx2w243hoZum0KgnhJAxWDYjkrFSvO5GxTZNsrC0pYvtogGKrYYBxQ3QpfqQ==",
+ "dependencies": {
+ "cross-fetch": "3.1.5",
+ "isomorphic-form-data": "2.0.0"
+ }
+ },
+ "node_modules/node-appwrite/node_modules/cross-fetch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
+ "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
+ "dependencies": {
+ "node-fetch": "2.6.7"
+ }
+ },
+ "node_modules/node-appwrite/node_modules/node-fetch": {
+ "version": "2.6.7",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
+ "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
+ "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": {
+ "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-gyp-build-optional-packages": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz",
+ "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==",
+ "optional": true,
+ "dependencies": {
+ "detect-libc": "^2.0.1"
+ },
+ "bin": {
+ "node-gyp-build-optional-packages": "bin.js",
+ "node-gyp-build-optional-packages-optional": "optional.js",
+ "node-gyp-build-optional-packages-test": "build-test.js"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
+ "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
+ "dev": true,
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "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/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.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
+ },
+ "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"
+ }
+ }
+ }
+}
diff --git a/node/authenticate-with-passkey/package.json b/node/authenticate-with-passkey/package.json
new file mode 100644
index 00000000..d21f856e
--- /dev/null
+++ b/node/authenticate-with-passkey/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "authenticate-with-passkey",
+ "version": "1.0.0",
+ "description": "",
+ "main": "src/main.js",
+ "type": "module",
+ "scripts": {
+ "format": "prettier --write ."
+ },
+ "dependencies": {
+ "@simplewebauthn/server": "^9.0.2",
+ "node-appwrite": "^12.0.0"
+ },
+ "devDependencies": {
+ "prettier": "^3.0.0"
+ }
+}
diff --git a/node/authenticate-with-passkey/src/appwrite.js b/node/authenticate-with-passkey/src/appwrite.js
new file mode 100644
index 00000000..f8ef8b01
--- /dev/null
+++ b/node/authenticate-with-passkey/src/appwrite.js
@@ -0,0 +1,64 @@
+import { Client, Users, ID, Databases, Query } from 'node-appwrite';
+
+class AppwriteService {
+ constructor() {
+ const client = new Client();
+ client
+ .setEndpoint(
+ process.env.APPWRITE_ENDPOINT ?? 'https://cloud.appwrite.io/v1'
+ )
+ .setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID)
+ .setKey(process.env.APPWRITE_API_KEY);
+
+ this.users = new Users(client);
+ this.databases = new Databases(client);
+ }
+
+ async prepareUser(email) {
+ const response = await this.users.list([ Query.equal('email', email), Query.limit(1) ]);
+ let user = response.users[0] ?? null;
+
+ if(!user) {
+ user = await this.users.create(ID.unique(), email);
+ }
+
+ return user;
+ }
+
+ async createSessionToken(userId) {
+ return await this.users.createToken(userId, 64, 60);
+ }
+
+ async createChallenge(userId, token) {
+ return await this.databases.createDocument('main', 'challenges', ID.unique(), {
+ userId: userId,
+ token
+ });
+ }
+
+ async getChallenge(challengeId) {
+ return await this.databases.getDocument('main', 'challenges', challengeId);
+ }
+
+ async deleteChallenge(challengeId) {
+ return await this.databases.deleteDocument('main', 'challenges', challengeId);
+ }
+
+ async createCredentials(userId, credentials) {
+ return await this.databases.createDocument('main', 'credentials', ID.unique(), {
+ userId,
+ credentials: JSON.stringify(credentials)
+ });
+ }
+
+ async getCredential(userId) {
+ const documents = (await this.databases.listDocuments('main', 'credentials', [
+ Query.equal('userId', userId),
+ Query.limit(1)
+ ])).documents;
+
+ return documents[0];
+ }
+}
+
+export default AppwriteService;
diff --git a/node/authenticate-with-passkey/src/main.js b/node/authenticate-with-passkey/src/main.js
new file mode 100644
index 00000000..23ecf161
--- /dev/null
+++ b/node/authenticate-with-passkey/src/main.js
@@ -0,0 +1,217 @@
+import {
+ getStaticFile,
+ throwIfMissing
+} from './utils.js';
+import AppwriteService from './appwrite.js';
+import * as SimpleWebAuthnServer from '@simplewebauthn/server';
+import * as SimpleWebAuthnServerHelpers from '@simplewebauthn/server/helpers';
+
+export default async ({ req, res, log }) => {
+ throwIfMissing(process.env, [
+ 'APPWRITE_API_KEY',
+ 'ALLOWED_HOSTNAME'
+ ]);
+
+ const corsHeaders = {
+ 'Access-Control-Allow-Origin': 'https://' + process.env.ALLOWED_HOSTNAME,
+ 'Access-Control-Allow-Methods': 'GET, POST, PATCH, PUT, DELETE, OPTIONS',
+ 'Access-Control-Allow-Headers': 'Origin, Content-Type'
+ };
+
+ const appwrite = new AppwriteService();
+
+ if (req.method === 'OPTIONS') {
+ return res.send('', 200, corsHeaders);
+ }
+
+ if (req.path === '/' && req.method === 'GET') {
+ return res.send(getStaticFile('index.html'), 200, {
+ 'Content-Type': 'text/html; charset=utf-8',
+ ...corsHeaders
+ });
+ }
+
+ if (req.path === '/v1/challenges' && req.method === 'POST') {
+ try {
+ throwIfMissing(req.body, ['email']);
+ } catch (error) {
+ log(error);
+ return res.send('Please provide email.', 400, corsHeaders);
+ }
+
+ const user = await appwrite.prepareUser(req.body.email);
+
+ const credential = await appwrite.getCredential(user.$id);
+ if (credential) {
+ return res.send('You already have passkey. Please sign in.', 400, corsHeaders);
+ }
+
+ const options = await SimpleWebAuthnServer.generateRegistrationOptions({
+ rpName: 'Passkeys Demo (Appwrite)',
+ rpID: process.env.ALLOWED_HOSTNAME,
+ userID: user.$id,
+ userName: req.body.email,
+ userDisplayName: req.body.email,
+ attestationType: 'none',
+ authenticatorSelection: {
+ residentKey: 'preferred',
+ userVerification: 'preferred',
+ authenticatorAttachment: 'platform',
+ },
+ });
+
+ const challenge = await appwrite.createChallenge(user.$id, options.challenge);
+
+ return res.json({
+ challengeId: challenge.$id,
+ options
+ }, 200, corsHeaders);
+ }
+
+ if (req.path === '/v1/challenges' && req.method === 'PUT') {
+ try {
+ throwIfMissing(req.body, ['challengeId', 'registration']);
+ } catch (error) {
+ log(error);
+ return res.send('Please provide challengeId and registration.', 400, corsHeaders);
+ }
+
+ const { challengeId, registration } = req.body;
+
+ let challenge;
+
+ try {
+ challenge = await appwrite.getChallenge(challengeId);
+ } catch (error) {
+ log(error);
+ return res.send('Challenge not found. Please start over.', 400, corsHeaders);
+ }
+
+ let verification;
+ try {
+ verification = await SimpleWebAuthnServer.verifyRegistrationResponse({
+ response: registration,
+ expectedChallenge: challenge.token,
+ expectedOrigin: 'https://' + process.env.ALLOWED_HOSTNAME,
+ expectedRPID: process.env.ALLOWED_HOSTNAME
+ });
+ } catch (error) {
+ log(error);
+ return res.send('Could not finish registration process.', 400, corsHeaders);
+ }
+
+ const { verified, registrationInfo } = verification;
+
+ if (!verified) {
+ log(verification);
+ return res.send('Incorrect passkey.', 500, corsHeaders);
+ }
+
+ await appwrite.createCredentials(challenge.userId, {
+ credentialID: SimpleWebAuthnServerHelpers.isoUint8Array.toHex(registrationInfo.credentialID),
+ credentialPublicKey: SimpleWebAuthnServerHelpers.isoUint8Array.toHex(registrationInfo.credentialPublicKey),
+ counter: registrationInfo.counter,
+ credentialDeviceType: registrationInfo.credentialDeviceType,
+ credentialBackedUp: registrationInfo.credentialBackedUp,
+ transports: registration.response.transports
+ });
+ await appwrite.deleteChallenge(challenge.$id);
+
+ return res.send('OK', 200, corsHeaders);
+ }
+
+ if (req.path === '/v1/tokens' && req.method === 'POST') {
+ try {
+ throwIfMissing(req.body, ['email']);
+ } catch (error) {
+ log(error);
+ return res.send('Please provide email.', 400, corsHeaders);
+ }
+
+ const user = await appwrite.prepareUser(req.body.email);
+
+ const credential = await appwrite.getCredential(user.$id);
+ if(!credential) {
+ return res.send('You do not have passkey yet. Please sign up.', 400, corsHeaders);
+ }
+
+ const authenticator = JSON.parse(credential.credentials);
+
+ const options = await SimpleWebAuthnServer.generateAuthenticationOptions({
+ rpID: process.env.ALLOWED_HOSTNAME,
+ userVerification: 'preferred',
+ allowCredentials: [{
+ id: SimpleWebAuthnServerHelpers.isoUint8Array.fromHex(authenticator.credentialID),
+ type: 'public-key',
+ transports: authenticator.transports
+ }]
+ });
+
+ const challenge = await appwrite.createChallenge(user.$id, options.challenge);
+
+ return res.json({
+ challengeId: challenge.$id,
+ options
+ }, 200, corsHeaders);
+ }
+
+ if (req.path === '/v1/tokens' && req.method === 'PUT') {
+ try {
+ throwIfMissing(req.body, ['challengeId', 'authentication']);
+ } catch (error) {
+ log(error);
+ return res.send('Please provide challengeId and authentication.', 400, corsHeaders);
+ }
+
+ const { challengeId, authentication } = req.body;
+
+ let challenge;
+ try {
+ challenge = await appwrite.getChallenge(challengeId);
+ } catch (error) {
+ log(error);
+ return res.send('Challenge not found. Please start over.', 400, corsHeaders);
+ }
+
+ const credential = await appwrite.getCredential(challenge.userId);
+ if(!credential) {
+ return res.send('You do not have passkey yet. Please sign up.', 400, corsHeaders);
+ }
+
+ let verification;
+ try {
+ const authenticator = JSON.parse(credential.credentials);
+ authenticator.credentialID = SimpleWebAuthnServerHelpers.isoUint8Array.fromHex(authenticator.credentialID);
+ authenticator.credentialPublicKey = SimpleWebAuthnServerHelpers.isoUint8Array.fromHex(authenticator.credentialPublicKey);
+
+ verification = await SimpleWebAuthnServer.verifyAuthenticationResponse({
+ response: authentication,
+ expectedChallenge: challenge.token,
+ expectedOrigin: 'https://' + process.env.ALLOWED_HOSTNAME,
+ expectedRPID: process.env.ALLOWED_HOSTNAME,
+ authenticator
+ });
+ } catch (error) {
+ log(error);
+ log(error.message);
+ log(error.stack);
+ return res.send('Could not finish authentication process.', 400, corsHeaders);
+ }
+
+ const { verified } = verification;
+
+ if (!verified) {
+ log(verification);
+ return res.send('Incorrect passkey.', 400, corsHeaders);
+ }
+
+ const token = await appwrite.createSessionToken(challenge.userId);
+
+ return res.json({
+ secret: token.secret,
+ userId: challenge.userId
+ }, 200, corsHeaders);
+ }
+
+ return res.send('Not found.', 404, corsHeaders);
+};
diff --git a/node/authenticate-with-passkey/src/utils.js b/node/authenticate-with-passkey/src/utils.js
new file mode 100644
index 00000000..11c3437b
--- /dev/null
+++ b/node/authenticate-with-passkey/src/utils.js
@@ -0,0 +1,35 @@
+import path from 'path';
+import { fileURLToPath } from 'url';
+import fs from 'fs';
+import crypto from 'crypto';
+
+/**
+ * Throws an error if any of the keys are missing from the object
+ * @param {*} obj
+ * @param {string[]} keys
+ * @throws {Error}
+ */
+export function throwIfMissing(obj, keys) {
+ const missing = [];
+ for (let key of keys) {
+ if (!(key in obj) || !obj[key]) {
+ missing.push(key);
+ }
+ }
+ if (missing.length > 0) {
+ throw new Error(`Missing required fields: ${missing.join(', ')}`);
+ }
+}
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const staticFolder = path.join(__dirname, '../static');
+
+/**
+ * Returns the contents of a file in the static folder
+ * @param {string} fileName
+ * @returns {string} Contents of static/{fileName}
+ */
+export function getStaticFile(fileName) {
+ return fs.readFileSync(path.join(staticFolder, fileName)).toString();
+}
diff --git a/node/authenticate-with-passkey/static/index.html b/node/authenticate-with-passkey/static/index.html
new file mode 100644
index 00000000..fa264667
--- /dev/null
+++ b/node/authenticate-with-passkey/static/index.html
@@ -0,0 +1,360 @@
+
+
+
+
+
+
+
+ Demo | Authenticate with passkey
+
+
+
+
+
+
+
+
+
+
+
+
+ Passkey authentication
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Biometric authentication
+
+
+ Passkeys provide authentication method that does not require users to remember something. Instead, a
+ combination of owning something (a device with private key) and being something (fingerprint, face, voice)
+ is used to authenticate.
+
+
+
+
+
+
+
+ Sign in
+ Sign up
+ My Account
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Please wait
+
Loading ...
+
+
+
+
+
+
+
+
+
Sorry
+
You are not logged in yet.
+
+
+
+
+
+
+
+
+
+
+
Welcome!
+
Signed in as
+
+
+
+
+ Your user ID is
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ .
+ .
+
+
+
\ No newline at end of file