-
Notifications
You must be signed in to change notification settings - Fork 0
feat(auth): add passkey plugin #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughAdds passkey authentication support by introducing a new SQL migration that creates an Changes
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@package.json`:
- Line 41: Update the better-auth dependency in package.json to match the
`@better-auth/passkey` peer requirement: change "better-auth" from ^1.4.15 to
^1.4.17 so it aligns with "@better-auth/passkey": "^1.4.17" and avoids a
peerDependency mismatch; ensure both dependency entries (better-auth and
`@better-auth/passkey`) use the same major.minor.patch range (^1.4.17) and run
npm/yarn install to validate.
In `@src/auth/index.ts`:
- Around line 23-29: The drizzle adapter schema in src/auth/index.ts is missing
the passkey table mapping needed by `@better-auth/passkey`; update the
configuration passed to drizzleAdapter (the database: drizzleAdapter(db, {
provider: "pg", schema: { ... } }) call) to include a passkey property, e.g. add
passkey: SCHEMA.AUTH.passkey alongside user, account, session, and verification
so the adapter knows about the passkey table.
- Around line 18-21: Replace the hard-coded rpID in the passkey(...) call with a
derived hostname from the configured base URL: read env.PUBLIC_URL, parse it
(e.g., via new URL(env.PUBLIC_URL).hostname) and use that value as rpID (with a
safe fallback to the current request origin or a default if parsing fails).
Update the passkey(...) invocation in src/auth/index.ts so rpID is computed at
runtime from env.PUBLIC_URL rather than the literal "polinetwork.org", keeping
rpName as-is.
🧹 Nitpick comments (3)
drizzle/0006_uneven_lord_hawal.sql (1)
1-13: Consider makingcreated_atNOT NULL for consistency.The
created_atcolumn (line 11) is nullable, while other auth tables (auth_users,auth_sessions,auth_accounts) definecreated_atas NOT NULL. If there's no specific reason for passkeys to have a nullable creation timestamp, consider adding the NOT NULL constraint for consistency.Suggested fix
- "created_at" timestamp, + "created_at" timestamp NOT NULL,src/db/schema/auth/auth.ts (2)
85-103: Table naming inconsistency:"passkey"(singular) vs other tables using plural.Other tables use plural names (
"users","sessions","accounts","verifications"), but this table uses singular"passkey". Consider using"passkeys"for consistency across the auth schema.Suggested fix
export const passkey = createTable.auth( - "passkey", + "passkeys", {Note: This would require updating the SQL migration to use
auth_passkeysas well.
99-100: Consider makingcreatedAtnon-nullable to align with other auth tables.The passkey table has a nullable
createdAt, while all other auth tables (user,session,account,verification) use.notNull(). The absence ofupdatedAtfollows the better-auth passkey plugin schema (which does not include this field), so that's intentional. However, the nullablecreatedAtinconsistency should be addressed:Suggested fix
transports: text("transports"), - createdAt: timestamp("created_at"), + createdAt: timestamp("created_at").notNull(), aaguid: text("aaguid"),
| }, | ||
| "dependencies": { | ||
| "@azure/identity": "^4.13.0", | ||
| "@better-auth/passkey": "^1.4.17", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
@better-auth/passkey peerDependencies better-auth compatibility
💡 Result:
@better-auth/passkey is version-pinned to better-auth via peerDependencies, so you generally need to keep the versions in lockstep.
For example, @better-auth/passkey@1.4.15 declares these peer deps (must be satisfied by your app, not installed automatically):
better-auth: 1.4.15@better-auth/core: 1.4.15@better-auth/utils: 0.3.0@better-fetch/fetch: 1.1.21better-call: 1.1.8nanostores: ^1.0.1 [1]
Compatibility rule of thumb: use the same version number for better-auth, @better-auth/core, and @better-auth/passkey (e.g., all 1.4.15). If your project is on a newer better-auth (Snyk reports 1.4.17 as latest for better-auth), you’ll likely need the matching @better-auth/passkey release (or you’ll hit peer-dep conflicts). [2]
Where to check your exact version’s peer deps: the plugin’s published package.json on jsDelivr for that specific version. [1]
Sources: [1] [2]
Align better-auth version with @better-auth/passkey.
@better-auth/passkey is version-pinned to better-auth via peerDependencies and requires matching versions. Adding @better-auth/passkey ^1.4.17 while better-auth is ^1.4.15 risks a peer dependency mismatch—if better-auth resolves to 1.4.15, the passkey package will fail due to incompatible peer dependencies. Update better-auth to ^1.4.17 to ensure version alignment.
🤖 Prompt for AI Agents
In `@package.json` at line 41, Update the better-auth dependency in package.json
to match the `@better-auth/passkey` peer requirement: change "better-auth" from
^1.4.15 to ^1.4.17 so it aligns with "@better-auth/passkey": "^1.4.17" and
avoids a peerDependency mismatch; ensure both dependency entries (better-auth
and `@better-auth/passkey`) use the same major.minor.patch range (^1.4.17) and run
npm/yarn install to validate.
| passkey({ | ||
| rpID: "polinetwork.org", | ||
| rpName: "PoliNetwork APS", | ||
| }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, let's read the src/auth/index.ts file to see the full context
cat -n src/auth/index.ts | head -50Repository: PoliNetworkOrg/backend
Length of output: 1888
🏁 Script executed:
# Check if env.PUBLIC_URL is defined and used elsewhere
rg "PUBLIC_URL" -A 2 -B 2Repository: PoliNetworkOrg/backend
Length of output: 533
🏁 Script executed:
# Look for `@better-auth` package info and configuration patterns
cat -n package.json | grep -A 5 -B 5 "better-auth"Repository: PoliNetworkOrg/backend
Length of output: 782
🏁 Script executed:
# Search for any other passkey or rpID configuration in the codebase
rg "rpID|rpName" -A 2 -B 2Repository: PoliNetworkOrg/backend
Length of output: 280
🌐 Web query:
@better-auth passkey rpID configuration requirements
💡 Result:
For Better Auth’s @better-auth/passkey plugin, rpID must follow standard WebAuthn “Relying Party ID” rules:
- Set
rpIDto a domain name that matches your auth server’s origin domain (the public site/domain users are authenticating to).localhostis allowed for local development. [1] - It must be an “effective domain” (registrable domain) or a subdomain of it: you may drop labels from the left, e.g.
www.example.com→www.example.comorexample.com. [1] - It cannot be a public suffix / eTLD (e.g.,
comis invalid). [1] - Related config: set
originto the exact origin where your Better Auth server is hosted (scheme + host + optional port), with no trailing slash. [1]
Sources: Better Auth Passkey plugin docs. [1]
🌐 Web query:
WebAuthn rpID effective domain matching requirement
💡 Result:
In WebAuthn, the RP ID (rpId) must domain-match the calling page’s origin as follows:
- The RP ID is a valid domain string (host name only — no scheme, port, or path). [1]
- By default, the RP ID is the caller origin’s effective domain. [1]
- If you override it, the RP ID must be either:
- exactly equal to the origin’s effective domain, or
- a registrable domain suffix of the origin’s effective domain (i.e., you can broaden scope to a parent registrable domain, but you can’t choose a sibling or an unrelated domain). [1]
- The origin’s scheme must be
https(port is unrestricted). [1] - During authentication, the authenticator/browser ensures the ceremony uses the same
rpIdthe credential was registered under. [1][2]
Example (from the spec): for origin https://login.example.com:1337, valid RP IDs are login.example.com (default) and example.com, but not m.login.example.com and not com. [1]
Sources:
[1] W3C WebAuthn Level 2 Recommendation (RP ID / effective domain + registrable-suffix rules) (w3.org)
[2] MDN: PublicKeyCredentialRequestOptions.rpId (must match credential’s rpId; defaults to origin domain) (developer.mozilla.org)
Citations:
- 1: https://www.w3.org/TR/webauthn-2/
- 2: https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialRequestOptions?utm_source=openai
🏁 Script executed:
# Check if there's any documentation or comments about passkey/rpID in the repo
rg "passkey|rpID|rpName" -B 5 -A 5 --type ts --type jsRepository: PoliNetworkOrg/backend
Length of output: 3073
🏁 Script executed:
# Check the betterAuth configuration docs or types
rg "baseURL|trustedOrigins" src/auth/index.ts -B 3 -A 3Repository: PoliNetworkOrg/backend
Length of output: 239
🏁 Script executed:
# Look for any test files or configuration examples
find . -name "*.ts" -o -name "*.js" | xargs grep -l "passkey" 2>/dev/null | head -10Repository: PoliNetworkOrg/backend
Length of output: 114
Derive rpID from baseURL to prevent WebAuthn registration failures across environments.
Hard-coding rpID to "polinetwork.org" conflicts with the configurable baseURL (set to env.PUBLIC_URL). Per WebAuthn spec, rpID must match the origin's effective domain—if PUBLIC_URL points to localhost, a staging domain, or any other host during development/preview, the hard-coded rpID will cause passkey registration to fail. Extract the hostname from env.PUBLIC_URL to make rpID match the active origin.
Example fix
+const rpID = new URL(env.PUBLIC_URL).hostname
+
passkey({
- rpID: "polinetwork.org",
+ rpID,
rpName: "PoliNetwork APS",
}),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| passkey({ | |
| rpID: "polinetwork.org", | |
| rpName: "PoliNetwork APS", | |
| }), | |
| const rpID = new URL(env.PUBLIC_URL).hostname | |
| passkey({ | |
| rpID, | |
| rpName: "PoliNetwork APS", | |
| }), |
🤖 Prompt for AI Agents
In `@src/auth/index.ts` around lines 18 - 21, Replace the hard-coded rpID in the
passkey(...) call with a derived hostname from the configured base URL: read
env.PUBLIC_URL, parse it (e.g., via new URL(env.PUBLIC_URL).hostname) and use
that value as rpID (with a safe fallback to the current request origin or a
default if parsing fails). Update the passkey(...) invocation in
src/auth/index.ts so rpID is computed at runtime from env.PUBLIC_URL rather than
the literal "polinetwork.org", keeping rpName as-is.
| database: drizzleAdapter(db, { | ||
| provider: "pg", | ||
| schema: { | ||
| user: SCHEMA.AUTH.users, | ||
| account: SCHEMA.AUTH.accounts, | ||
| session: SCHEMA.AUTH.sessions, | ||
| verification: SCHEMA.AUTH.verifications, | ||
| user: SCHEMA.AUTH.user, | ||
| account: SCHEMA.AUTH.account, | ||
| session: SCHEMA.AUTH.session, | ||
| verification: SCHEMA.AUTH.verification, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
@better-auth passkey drizzle adapter schema configuration
💡 Result:
To use @better-auth/passkey with the Drizzle adapter, you mainly need (1) the passkey plugin enabled in your Better Auth config and (2) a Drizzle schema that includes the required passkey table, passed into drizzleAdapter.
1) Configure Better Auth + Passkey + Drizzle adapter (with schema)
// auth.ts
import { betterAuth } from "better-auth";
import { passkey } from "@better-auth/passkey";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "./drizzle";
import * as schema from "./schema"; // must export `passkey` table too
export const auth = betterAuth({
plugins: [passkey()],
database: drizzleAdapter(db, {
provider: "pg", // or "sqlite" / "mysql"
schema,
}),
});If your Drizzle tables use different names (e.g., users instead of user), you must map them in the adapter schema (or change Better Auth modelName). [1]
2) Generate/migrate the schema (recommended)
Better Auth can generate the needed Drizzle schema (including plugin tables like passkey) based on your config/plugins:
npx@better-auth/cli@latest generate(generate schema)- or
npx@better-auth/climigrate(apply migrations) [1] [2]
3) Ensure the Passkey table exists in your DB/schema
The passkey plugin requires a passkey table (with fields like publicKey, userId, credentialID, etc.). If you don’t use the CLI, you must create it yourself in Drizzle. [2]
Sources: Drizzle adapter docs (schema + mapping) [1]; Passkey plugin docs (required table + migration/generate) [2].
🏁 Script executed:
rg "SCHEMA\.AUTH\." src/ -A 2 -B 2 | head -50Repository: PoliNetworkOrg/backend
Length of output: 735
🏁 Script executed:
rg "export.*SCHEMA|const SCHEMA" src/ -B 2 -A 10 | head -80Repository: PoliNetworkOrg/backend
Length of output: 1647
🏁 Script executed:
rg "authSchema|auth.*schema" src/db/ -B 5 -A 15Repository: PoliNetworkOrg/backend
Length of output: 840
🏁 Script executed:
cat src/db/schema/auth.tsRepository: PoliNetworkOrg/backend
Length of output: 119
🏁 Script executed:
fd "auth" src/db/schema/Repository: PoliNetworkOrg/backend
Length of output: 112
🏁 Script executed:
ls -la src/db/schema/auth/ && echo "---" && wc -l src/db/schema/auth/*.tsRepository: PoliNetworkOrg/backend
Length of output: 371
🏁 Script executed:
cat src/db/schema/auth/index.ts && echo "---" && cat src/db/schema/auth/auth.tsRepository: PoliNetworkOrg/backend
Length of output: 4134
Add passkey table mapping to drizzle adapter schema.
The @better-auth/passkey plugin requires an explicit passkey table mapping in the drizzle adapter configuration. The passkey table exists in your schema definition but is missing from the adapter setup in src/auth/index.ts.
Fix required
schema: {
user: SCHEMA.AUTH.user,
account: SCHEMA.AUTH.account,
session: SCHEMA.AUTH.session,
verification: SCHEMA.AUTH.verification,
+ passkey: SCHEMA.AUTH.passkey,
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| database: drizzleAdapter(db, { | |
| provider: "pg", | |
| schema: { | |
| user: SCHEMA.AUTH.users, | |
| account: SCHEMA.AUTH.accounts, | |
| session: SCHEMA.AUTH.sessions, | |
| verification: SCHEMA.AUTH.verifications, | |
| user: SCHEMA.AUTH.user, | |
| account: SCHEMA.AUTH.account, | |
| session: SCHEMA.AUTH.session, | |
| verification: SCHEMA.AUTH.verification, | |
| database: drizzleAdapter(db, { | |
| provider: "pg", | |
| schema: { | |
| user: SCHEMA.AUTH.user, | |
| account: SCHEMA.AUTH.account, | |
| session: SCHEMA.AUTH.session, | |
| verification: SCHEMA.AUTH.verification, | |
| passkey: SCHEMA.AUTH.passkey, | |
| }, |
🤖 Prompt for AI Agents
In `@src/auth/index.ts` around lines 23 - 29, The drizzle adapter schema in
src/auth/index.ts is missing the passkey table mapping needed by
`@better-auth/passkey`; update the configuration passed to drizzleAdapter (the
database: drizzleAdapter(db, { provider: "pg", schema: { ... } }) call) to
include a passkey property, e.g. add passkey: SCHEMA.AUTH.passkey alongside
user, account, session, and verification so the adapter knows about the passkey
table.
No description provided.