authjs-corepass-provider 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +450 -0
- package/db/corepass-schema.postgres.sql +46 -0
- package/db/corepass-schema.sql +50 -0
- package/dist/chunk-72ZEQHQO.js +33 -0
- package/dist/chunk-72ZEQHQO.js.map +1 -0
- package/dist/index.d.ts +427 -0
- package/dist/index.js +1289 -0
- package/dist/index.js.map +1 -0
- package/dist/provider.d.ts +12 -0
- package/dist/provider.js +7 -0
- package/dist/provider.js.map +1 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CorePass
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
# authjs-corepass-provider
|
|
2
|
+
|
|
3
|
+
CorePass provider + server helpers for Auth.js (`@auth/core`) implementing the **pending-by-default** registration flow:
|
|
4
|
+
|
|
5
|
+
- Browser completes WebAuthn attestation (registration)
|
|
6
|
+
- Server stores a **pending registration** (no Auth.js user/account/authenticator yet)
|
|
7
|
+
- CorePass mobile app finalizes the account by calling **`POST /passkey/data`** with an **Ed448-signed** payload
|
|
8
|
+
|
|
9
|
+
## What you get
|
|
10
|
+
|
|
11
|
+
- **Provider**: `CorePass()` (wraps Auth.js WebAuthn with passkey-friendly defaults)
|
|
12
|
+
- **Server helpers**: `createCorePassServer()` exposing handlers:
|
|
13
|
+
- `startRegistration(req)`
|
|
14
|
+
- `finishRegistration(req)`
|
|
15
|
+
- `enrichRegistration(req)` (your `/passkey/data`)
|
|
16
|
+
- **DB extension schema**: `db/corepass-schema.sql`
|
|
17
|
+
|
|
18
|
+
## Flows
|
|
19
|
+
|
|
20
|
+
### Registration flow (pending-by-default)
|
|
21
|
+
|
|
22
|
+
```mermaid
|
|
23
|
+
sequenceDiagram
|
|
24
|
+
autonumber
|
|
25
|
+
actor B as Browser
|
|
26
|
+
participant S as Your backend
|
|
27
|
+
participant KV as Challenge store
|
|
28
|
+
participant DB as CorePass store
|
|
29
|
+
actor A as CorePass app
|
|
30
|
+
|
|
31
|
+
B->>S: POST /webauthn/start { email? }
|
|
32
|
+
Note over B,S: Pending TTL default is 10 minutes (pendingTtlSeconds=600)
|
|
33
|
+
S->>KV: put reg:sid {challenge,email} ttl
|
|
34
|
+
S-->>B: 200 CreationOptions + Set-Cookie corepass.sid
|
|
35
|
+
B->>B: navigator.credentials.create()
|
|
36
|
+
B->>S: POST /webauthn/finish { attestation, email? }
|
|
37
|
+
S->>KV: get+delete reg:sid
|
|
38
|
+
S->>S: verifyRegistrationResponse()
|
|
39
|
+
S->>DB: createPendingRegistration(credentialId, publicKey, counter, aaguid, email?)
|
|
40
|
+
S-->>B: 200 { pending:true, enrichToken, credentialId }
|
|
41
|
+
|
|
42
|
+
A->>S: POST /passkey/data {coreId, credentialId, timestamp, userData} + X-Signature (Ed448)
|
|
43
|
+
S->>S: validateCoreIdMainnet + timestamp window
|
|
44
|
+
S->>S: verify Ed448 signature over canonical JSON
|
|
45
|
+
S->>DB: load+delete pending by credentialId
|
|
46
|
+
S->>S: create/link Auth.js user+account+authenticator
|
|
47
|
+
S->>DB: upsert CorePass identity/profile (provided_till, flags)
|
|
48
|
+
S->>S: (optional) POST registration webhook { coreId, refId? } (registrationWebhookRetries, default 3)
|
|
49
|
+
Note over S: If registrationWebhookSecret is set, include HMAC headers:\nX-Webhook-Timestamp + X-Webhook-Signature
|
|
50
|
+
S-->>A: 200 ok
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Login flow (standard Auth.js WebAuthn authenticate)
|
|
54
|
+
|
|
55
|
+
CorePass login is normal WebAuthn: it uses the Auth.js WebAuthn callback path (`action=authenticate`), and resolves the user by stored authenticators.
|
|
56
|
+
|
|
57
|
+
```mermaid
|
|
58
|
+
sequenceDiagram
|
|
59
|
+
autonumber
|
|
60
|
+
actor B as Browser
|
|
61
|
+
participant Auth as Auth.js (@auth/core)
|
|
62
|
+
participant DB as Adapter DB
|
|
63
|
+
|
|
64
|
+
B->>Auth: GET /auth/webauthn-options?action=authenticate (provider=corepass)
|
|
65
|
+
Auth->>DB: listAuthenticatorsByUserId (optional) / challenge cookie
|
|
66
|
+
Auth-->>B: 200 RequestOptions + challenge cookie
|
|
67
|
+
B->>B: navigator.credentials.get()
|
|
68
|
+
B->>Auth: POST /auth/callback/corepass { action:"authenticate", data }
|
|
69
|
+
Auth->>DB: getAuthenticator(credentialId) + verifyAuthenticationResponse()
|
|
70
|
+
Auth->>DB: updateAuthenticatorCounter()
|
|
71
|
+
Auth-->>B: session established
|
|
72
|
+
Note over Auth: (optional) POST login webhook { coreId, refId? } (loginWebhookRetries, default 3)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Install
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install authjs-corepass-provider
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
You also need:
|
|
82
|
+
|
|
83
|
+
- `@auth/core` (peer dependency)
|
|
84
|
+
- `@simplewebauthn/browser` in your frontend
|
|
85
|
+
|
|
86
|
+
## Auth.js configuration
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { Auth } from "@auth/core"
|
|
90
|
+
import CorePass from "authjs-corepass-provider/provider"
|
|
91
|
+
|
|
92
|
+
export const auth = (req: Request) =>
|
|
93
|
+
Auth(req, {
|
|
94
|
+
providers: [CorePass()],
|
|
95
|
+
adapter: /* your Auth.js adapter */,
|
|
96
|
+
})
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## CorePass endpoints
|
|
100
|
+
|
|
101
|
+
You mount these where you want in your app (framework-specific). The handlers are plain Web API `Request -> Response`.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { createCorePassServer } from "authjs-corepass-provider"
|
|
105
|
+
|
|
106
|
+
const corepass = createCorePassServer({
|
|
107
|
+
adapter: /* Auth.js adapter (must implement WebAuthn + user methods) */,
|
|
108
|
+
// store must implement CorePassStore:
|
|
109
|
+
// - pending registrations (default flow)
|
|
110
|
+
// - coreId <-> userId identity mapping
|
|
111
|
+
// - profile metadata (o18y/o21y/kyc/provided_till)
|
|
112
|
+
//
|
|
113
|
+
// Built-ins:
|
|
114
|
+
// - d1CorePassStore(db) for Cloudflare D1
|
|
115
|
+
// - postgresCorePassStore(pg) for Postgres (node-postgres, etc)
|
|
116
|
+
// - supabaseCorePassStore(supabase) for Supabase client
|
|
117
|
+
store: /* CorePassStore implementation */,
|
|
118
|
+
challengeStore: /* CorePassChallengeStore implementation (KV/Redis/etc) */,
|
|
119
|
+
rpID: "example.com",
|
|
120
|
+
rpName: "Example",
|
|
121
|
+
expectedOrigin: "https://example.com",
|
|
122
|
+
|
|
123
|
+
// default: pending registrations are required
|
|
124
|
+
allowImmediateFinalize: false,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Optional: login webhook (call from Auth.js events.signIn)
|
|
128
|
+
// events: {
|
|
129
|
+
// async signIn({ user, account }) {
|
|
130
|
+
// if (account?.provider === "corepass" && account?.type === "webauthn" && user?.id) {
|
|
131
|
+
// await corepass.postLoginWebhook({ userId: user.id })
|
|
132
|
+
// }
|
|
133
|
+
// }
|
|
134
|
+
// }
|
|
135
|
+
//
|
|
136
|
+
// Optional: logout webhook (call from Auth.js events.signOut)
|
|
137
|
+
// events: {
|
|
138
|
+
// async signOut({ session }) {
|
|
139
|
+
// // You must be able to map the logout event to a userId.
|
|
140
|
+
// // How you obtain userId depends on your Auth.js setup/session strategy.
|
|
141
|
+
// // If you have it:
|
|
142
|
+
// // await corepass.postLogoutWebhook({ userId })
|
|
143
|
+
// }
|
|
144
|
+
// }
|
|
145
|
+
|
|
146
|
+
export async function POST(req: Request) {
|
|
147
|
+
const url = new URL(req.url)
|
|
148
|
+
if (url.pathname === "/webauthn/start") return corepass.startRegistration(req)
|
|
149
|
+
if (url.pathname === "/webauthn/finish") return corepass.finishRegistration(req)
|
|
150
|
+
if (url.pathname === "/passkey/data") return corepass.enrichRegistration(req)
|
|
151
|
+
return new Response("Not found", { status: 404 })
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### “Unified” server factory helpers (same DB client)
|
|
156
|
+
|
|
157
|
+
If you want to avoid manually wiring `store: d1CorePassStore(...)` etc, you can use the factories:
|
|
158
|
+
|
|
159
|
+
- `createCorePassServerD1({ db, ... })`
|
|
160
|
+
- `createCorePassServerPostgres({ pg, ... })`
|
|
161
|
+
- `createCorePassServerSupabase({ supabase, ... })`
|
|
162
|
+
- `createCorePassServerCloudflareD1Kv({ db, kv, ... })`
|
|
163
|
+
- `createCorePassServerPostgresRedis({ pg, redis, ... })`
|
|
164
|
+
- `createCorePassServerSupabaseUpstash({ supabase, redis, ... })`
|
|
165
|
+
- `createCorePassServerSupabaseVercelKv({ supabase, kv, ... })`
|
|
166
|
+
|
|
167
|
+
This does **not** create an Auth.js adapter for you (adapters are separate packages), but it ensures the CorePass
|
|
168
|
+
store uses the same DB client you pass in.
|
|
169
|
+
|
|
170
|
+
## `challengeStore` (what it is, and what it supports)
|
|
171
|
+
|
|
172
|
+
`challengeStore` is **not an Auth.js provider** and it is **not tied to WebAuthn/Passkey provider IDs**.
|
|
173
|
+
It’s a minimal storage interface used by this package’s custom endpoints to persist the WebAuthn challenge
|
|
174
|
+
between:
|
|
175
|
+
|
|
176
|
+
- `POST /webauthn/start` (generate challenge)
|
|
177
|
+
- `POST /webauthn/finish` (verify attestation against expected challenge)
|
|
178
|
+
|
|
179
|
+
It must support **TTL** (seconds) and **delete on use**.
|
|
180
|
+
|
|
181
|
+
### How to wire it to real systems
|
|
182
|
+
|
|
183
|
+
You create/access your KV/Redis client *in your runtime* and pass it into the helper:
|
|
184
|
+
|
|
185
|
+
- Cloudflare Workers: use an **`env` binding**
|
|
186
|
+
- Node/Next.js/etc: create a **Redis client** using URL/token from env vars
|
|
187
|
+
|
|
188
|
+
### Example: in-memory (development only)
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { memoryChallengeStore } from "authjs-corepass-provider"
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Example: Redis / Upstash
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
import { redisChallengeStore } from "authjs-corepass-provider"
|
|
198
|
+
import { Redis } from "ioredis"
|
|
199
|
+
|
|
200
|
+
const redis = new Redis(process.env.REDIS_URL!)
|
|
201
|
+
|
|
202
|
+
const challengeStore = redisChallengeStore({
|
|
203
|
+
set: (key, value, { ex }) => redis.set(key, value, "EX", ex),
|
|
204
|
+
get: (key) => redis.get(key),
|
|
205
|
+
del: (key) => redis.del(key),
|
|
206
|
+
})
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Example: Cloudflare KV
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import { kvChallengeStore } from "authjs-corepass-provider"
|
|
213
|
+
|
|
214
|
+
// wrangler.jsonc:
|
|
215
|
+
// {
|
|
216
|
+
// "kv_namespaces": [
|
|
217
|
+
// { "binding": "COREPASS_KV", "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }
|
|
218
|
+
// ]
|
|
219
|
+
// }
|
|
220
|
+
//
|
|
221
|
+
// Worker handler: env.COREPASS_KV is a KVNamespace binding.
|
|
222
|
+
//
|
|
223
|
+
// const challengeStore = kvChallengeStore(env.COREPASS_KV)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Example: Vercel KV (Redis)
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
import { vercelKvChallengeStore } from "authjs-corepass-provider"
|
|
230
|
+
import { kv } from "@vercel/kv"
|
|
231
|
+
|
|
232
|
+
// Vercel manages connection details via environment variables.
|
|
233
|
+
// See Vercel KV setup for the required env vars.
|
|
234
|
+
|
|
235
|
+
const challengeStore = vercelKvChallengeStore({
|
|
236
|
+
set: (key, value, { ex }) => kv.set(key, value, { ex }),
|
|
237
|
+
get: (key) => kv.get<string>(key),
|
|
238
|
+
del: (key) => kv.del(key),
|
|
239
|
+
})
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Example: Upstash Redis REST (`@upstash/redis`)
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
import { upstashRedisChallengeStore } from "authjs-corepass-provider"
|
|
246
|
+
import { Redis } from "@upstash/redis"
|
|
247
|
+
|
|
248
|
+
const redis = new Redis({
|
|
249
|
+
url: process.env.UPSTASH_REDIS_REST_URL!,
|
|
250
|
+
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
const challengeStore = upstashRedisChallengeStore({
|
|
254
|
+
set: (key, value, { ex }) => redis.set(key, value, { ex }),
|
|
255
|
+
get: (key) => redis.get<string>(key),
|
|
256
|
+
del: (key) => redis.del(key),
|
|
257
|
+
})
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Example: Cloudflare Durable Objects
|
|
261
|
+
|
|
262
|
+
Durable Objects are a good fit if you want low-latency ephemeral state close to your Worker.
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
import { durableObjectChallengeStore } from "authjs-corepass-provider"
|
|
266
|
+
|
|
267
|
+
// Your Durable Object must implement these routes:
|
|
268
|
+
// - POST /challenge/put { key, value, ttlSeconds }
|
|
269
|
+
// - GET /challenge/get?key=...
|
|
270
|
+
// - POST /challenge/delete { key }
|
|
271
|
+
//
|
|
272
|
+
// Then:
|
|
273
|
+
// const challengeStore = durableObjectChallengeStore(env.COREPASS_DO.get(id))
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Example: DynamoDB (AWS SDK v3)
|
|
277
|
+
|
|
278
|
+
If you already run on AWS and want a managed KV with TTL:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
import { dynamoChallengeStore } from "authjs-corepass-provider"
|
|
282
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb"
|
|
283
|
+
import { DynamoDBDocumentClient, PutCommand, GetCommand, DeleteCommand } from "@aws-sdk/lib-dynamodb"
|
|
284
|
+
|
|
285
|
+
const TableName = process.env.COREPASS_CHALLENGE_TABLE!
|
|
286
|
+
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}))
|
|
287
|
+
const nowSec = () => Math.floor(Date.now() / 1000)
|
|
288
|
+
|
|
289
|
+
// This library doesn't hard-depend on AWS SDK; you provide a small adapter:
|
|
290
|
+
const challengeStore = dynamoChallengeStore({
|
|
291
|
+
put: ({ key, value, expiresAt }) =>
|
|
292
|
+
ddb.send(new PutCommand({ TableName, Item: { pk: key, value, expiresAt } })),
|
|
293
|
+
get: async (key) => {
|
|
294
|
+
const res = await ddb.send(new GetCommand({ TableName, Key: { pk: key } }))
|
|
295
|
+
const item = res.Item as { value?: string; expiresAt?: number } | undefined
|
|
296
|
+
if (!item?.value || typeof item.expiresAt !== "number") return null
|
|
297
|
+
if (item.expiresAt < nowSec()) return null
|
|
298
|
+
return { value: item.value, expiresAt: item.expiresAt }
|
|
299
|
+
},
|
|
300
|
+
delete: (key) => ddb.send(new DeleteCommand({ TableName, Key: { pk: key } })),
|
|
301
|
+
})
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Example: SQL / D1
|
|
305
|
+
|
|
306
|
+
Use a table like:
|
|
307
|
+
|
|
308
|
+
```sql
|
|
309
|
+
CREATE TABLE IF NOT EXISTS corepass_challenges (
|
|
310
|
+
key TEXT PRIMARY KEY,
|
|
311
|
+
value TEXT NOT NULL,
|
|
312
|
+
expires_at INTEGER NOT NULL
|
|
313
|
+
);
|
|
314
|
+
CREATE INDEX IF NOT EXISTS idx_corepass_challenges_expires_at ON corepass_challenges(expires_at);
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Then implement:
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
import type { CorePassChallengeStore } from "authjs-corepass-provider"
|
|
321
|
+
|
|
322
|
+
export function sqlChallengeStore(db: {
|
|
323
|
+
exec: (sql: string, params?: unknown[]) => Promise<unknown>
|
|
324
|
+
get: (sql: string, params?: unknown[]) => Promise<{ value: string; expires_at: number } | null>
|
|
325
|
+
}): CorePassChallengeStore {
|
|
326
|
+
const nowSec = () => Math.floor(Date.now() / 1000)
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
async put(key, value, ttlSeconds) {
|
|
330
|
+
const expiresAt = nowSec() + ttlSeconds
|
|
331
|
+
await db.exec(
|
|
332
|
+
"INSERT OR REPLACE INTO corepass_challenges (key, value, expires_at) VALUES (?1, ?2, ?3)",
|
|
333
|
+
[key, value, expiresAt]
|
|
334
|
+
)
|
|
335
|
+
},
|
|
336
|
+
async get(key) {
|
|
337
|
+
const row = await db.get(
|
|
338
|
+
"SELECT value, expires_at FROM corepass_challenges WHERE key = ?1",
|
|
339
|
+
[key]
|
|
340
|
+
)
|
|
341
|
+
if (!row) return null
|
|
342
|
+
if (row.expires_at < nowSec()) {
|
|
343
|
+
await db.exec("DELETE FROM corepass_challenges WHERE key = ?1", [key])
|
|
344
|
+
return null
|
|
345
|
+
}
|
|
346
|
+
return row.value
|
|
347
|
+
},
|
|
348
|
+
async delete(key) {
|
|
349
|
+
await db.exec("DELETE FROM corepass_challenges WHERE key = ?1", [key])
|
|
350
|
+
},
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## Database
|
|
356
|
+
|
|
357
|
+
Apply your adapter’s default Auth.js schema, then apply:
|
|
358
|
+
|
|
359
|
+
- `db/corepass-schema.sql` (SQLite/D1)
|
|
360
|
+
- `db/corepass-schema.postgres.sql` (PostgreSQL/Supabase)
|
|
361
|
+
|
|
362
|
+
This adds:
|
|
363
|
+
|
|
364
|
+
- `corepass_pending_registrations`
|
|
365
|
+
- `corepass_identities` (CoreID → Auth.js `userId` mapping)
|
|
366
|
+
- `corepass_profiles` (CorePass metadata like `o18y`, `kyc`, `provided_till`)
|
|
367
|
+
|
|
368
|
+
## Options
|
|
369
|
+
|
|
370
|
+
- **`allowedAaguids`**: defaults to CorePass AAGUID `636f7265-7061-7373-6964-656e74696679`. Set to `false` to allow any authenticator.
|
|
371
|
+
- **`pubKeyCredAlgs`**: defaults to `[-257, -7, -8]` (RS256, ES256, Ed25519).
|
|
372
|
+
- **`allowImmediateFinalize`**: if enabled, `finishRegistration` may finalize immediately if `coreId` is provided in the browser payload. This is **disabled by default** because it weakens the CoreID ownership guarantee (the default flow requires the Ed448-signed `/passkey/data` request).
|
|
373
|
+
- **`emailRequired`**: defaults to `false` (email can arrive later via `/passkey/data`). If no email is ever provided, the library creates the Auth.js user with a deterministic synthetic email and updates it once a real email is received.
|
|
374
|
+
- **`requireO18y`**: defaults to `false`. If enabled, `/passkey/data` must include `userData.o18y=true` or finalization is rejected. Not enforced for immediate-finalize.
|
|
375
|
+
- **`requireO21y`**: defaults to `false`. If enabled, `/passkey/data` must include `userData.o21y=true` or finalization is rejected. Not enforced for immediate-finalize.
|
|
376
|
+
- **`requireKyc`**: defaults to `false`. If enabled, `/passkey/data` must include `userData.kyc=true` or finalization is rejected. Not enforced for immediate-finalize.
|
|
377
|
+
- **`enableRefId`**: defaults to `false`. When enabled, the server generates and stores a `refId` (UUIDv4) for the CoreID identity and can include it in webhooks. When disabled, no `refId` is generated or stored.
|
|
378
|
+
- **Registration webhook options**:
|
|
379
|
+
- **`postRegistrationWebhooks`**: defaults to `false`.
|
|
380
|
+
- **`registrationWebhookUrl`**: required if `postRegistrationWebhooks: true`.
|
|
381
|
+
- **`registrationWebhookSecret`**: optional. If set, requests are HMAC-signed (SHA-256) using `timestamp + "\\n" + body` and include `X-Webhook-Timestamp` (unix seconds) and `X-Webhook-Signature` (`sha256=<hex>`).
|
|
382
|
+
- **`registrationWebhookRetries`**: defaults to `3` (range `1-10`). Retries happen on non-2xx responses or network errors.
|
|
383
|
+
- **Login webhook options**:
|
|
384
|
+
- **`postLoginWebhooks`**: defaults to `false`.
|
|
385
|
+
- **`loginWebhookUrl`**: required if `postLoginWebhooks: true`.
|
|
386
|
+
- **`loginWebhookSecret`**: optional. Same signing format/headers as registration.
|
|
387
|
+
- **`loginWebhookRetries`**: defaults to `3` (range `1-10`). Retries happen on non-2xx responses or network errors.
|
|
388
|
+
- **Logout webhook options**:
|
|
389
|
+
- **`postLogoutWebhooks`**: defaults to `false`.
|
|
390
|
+
- **`logoutWebhookUrl`**: required if `postLogoutWebhooks: true`.
|
|
391
|
+
- **`logoutWebhookSecret`**: optional. Same signing format/headers as registration.
|
|
392
|
+
- **`logoutWebhookRetries`**: defaults to `3` (range `1-10`). Retries happen on non-2xx responses or network errors.
|
|
393
|
+
- **`pendingTtlSeconds`**: defaults to `600` (10 minutes). Pending registrations expire after this and are dropped.
|
|
394
|
+
- **`timestampWindowMs`**: defaults to `600000` (10 minutes). Enrichment `timestamp` must be within this window.
|
|
395
|
+
|
|
396
|
+
## Enrichment payload (`/passkey/data`)
|
|
397
|
+
|
|
398
|
+
The CorePass app sends:
|
|
399
|
+
|
|
400
|
+
- **Body**: `{ coreId, credentialId, timestamp, userData }`
|
|
401
|
+
- **Header**: `X-Signature` (Ed448 signature)
|
|
402
|
+
|
|
403
|
+
### Canonical payload + signature input
|
|
404
|
+
|
|
405
|
+
For signature verification, the server **does not** use the raw request body bytes. Instead it:
|
|
406
|
+
|
|
407
|
+
- **Canonicalizes JSON**: recursively sorts object keys alphabetically and serializes with `JSON.stringify(...)` (so it is **minified**, no whitespace).
|
|
408
|
+
- **Builds signature input** as:
|
|
409
|
+
|
|
410
|
+
```text
|
|
411
|
+
signatureInput = "POST\n" + signaturePath + "\n" + canonicalJsonBody
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Then it verifies `X-Signature` (Ed448) over `UTF-8(signatureInput)`.
|
|
415
|
+
|
|
416
|
+
This means the CorePass signer must sign the **same canonical JSON string** (alphabetically ordered + minified) and the same `signaturePath` (defaults to `/passkey/data`, configurable via `signaturePath`).
|
|
417
|
+
|
|
418
|
+
### Timestamp units
|
|
419
|
+
|
|
420
|
+
`timestamp` is required and must be a **Unix timestamp in microseconds**.
|
|
421
|
+
|
|
422
|
+
`userData` fields:
|
|
423
|
+
|
|
424
|
+
| Field | Type | Example | Notes |
|
|
425
|
+
| - | - | - | - |
|
|
426
|
+
| `email` | `string` | `user@example.com` | Optional. If provided later, Auth.js user email is updated. |
|
|
427
|
+
| `o18y` | `boolean (or 0/1)` | `true` | Stored in `corepass_profiles.o18y`. |
|
|
428
|
+
| `o21y` | `boolean (or 0/1)` | `false` | Stored in `corepass_profiles.o21y`. |
|
|
429
|
+
| `kyc` | `boolean (or 0/1)` | `true` | Stored in `corepass_profiles.kyc`. |
|
|
430
|
+
| `kycDoc` | `string` | `PASSPORT` | Stored in `corepass_profiles.kyc_doc`. |
|
|
431
|
+
| `dataExp` | `number` | `43829` | Minutes. Converted to `provided_till`. |
|
|
432
|
+
|
|
433
|
+
`refId` is **not part of CorePass `/passkey/data`**. If you need an external correlation id, enable `enableRefId` and deliver it via your webhooks.
|
|
434
|
+
|
|
435
|
+
### `provided_till` calculation
|
|
436
|
+
|
|
437
|
+
`provided_till` is stored as a **Unix timestamp in seconds**:
|
|
438
|
+
|
|
439
|
+
```text
|
|
440
|
+
provided_till = floor(now_sec) + dataExpMinutes * 60
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## Notes on Auth.js internals
|
|
444
|
+
|
|
445
|
+
Auth.js’ built-in WebAuthn flow normally creates the user/account/authenticator during the WebAuthn callback. CorePass intentionally delays this until enrichment, so it uses custom endpoints instead of Auth.js’ built-in “register” callback path.
|
|
446
|
+
|
|
447
|
+
## Upstream references
|
|
448
|
+
|
|
449
|
+
- Auth.js contributing guide: `https://raw.githubusercontent.com/nextauthjs/.github/main/CONTRIBUTING.md`
|
|
450
|
+
- Auth.js built-in Passkey provider: `https://raw.githubusercontent.com/nextauthjs/next-auth/main/packages/core/src/providers/passkey.ts`
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
-- CorePass extension tables for Auth.js (PostgreSQL)
|
|
2
|
+
--
|
|
3
|
+
-- Apply this *in addition to* your adapter's default schema.
|
|
4
|
+
--
|
|
5
|
+
-- Tables added:
|
|
6
|
+
-- - corepass_identities: maps CoreID to your Auth.js userId
|
|
7
|
+
-- - corepass_profiles: stores CorePass-specific user metadata
|
|
8
|
+
-- - corepass_pending_registrations: stores passkey registrations until /passkey/data enrichment finalizes them
|
|
9
|
+
|
|
10
|
+
CREATE TABLE IF NOT EXISTS corepass_identities (
|
|
11
|
+
core_id TEXT PRIMARY KEY,
|
|
12
|
+
user_id TEXT NOT NULL UNIQUE,
|
|
13
|
+
ref_id UUID,
|
|
14
|
+
created_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())::BIGINT),
|
|
15
|
+
updated_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())::BIGINT)
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE TABLE IF NOT EXISTS corepass_profiles (
|
|
19
|
+
user_id TEXT PRIMARY KEY,
|
|
20
|
+
core_id TEXT NOT NULL UNIQUE,
|
|
21
|
+
o18y BOOLEAN,
|
|
22
|
+
o21y BOOLEAN,
|
|
23
|
+
kyc BOOLEAN,
|
|
24
|
+
kyc_doc TEXT,
|
|
25
|
+
provided_till BIGINT,
|
|
26
|
+
created_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())::BIGINT),
|
|
27
|
+
updated_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())::BIGINT)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE TABLE IF NOT EXISTS corepass_pending_registrations (
|
|
31
|
+
token UUID PRIMARY KEY,
|
|
32
|
+
credential_id TEXT NOT NULL UNIQUE,
|
|
33
|
+
credential_public_key TEXT NOT NULL,
|
|
34
|
+
counter BIGINT NOT NULL DEFAULT 0,
|
|
35
|
+
credential_device_type TEXT NOT NULL,
|
|
36
|
+
credential_backed_up BOOLEAN NOT NULL DEFAULT FALSE,
|
|
37
|
+
transports TEXT,
|
|
38
|
+
email TEXT,
|
|
39
|
+
ref_id UUID,
|
|
40
|
+
aaguid UUID,
|
|
41
|
+
created_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())::BIGINT),
|
|
42
|
+
expires_at BIGINT NOT NULL
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_corepass_pending_expires_at
|
|
46
|
+
ON corepass_pending_registrations(expires_at);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
-- CorePass extension tables for Auth.js
|
|
2
|
+
--
|
|
3
|
+
-- This file intentionally does NOT include the Auth.js default tables.
|
|
4
|
+
-- Apply this *in addition to* your adapter's default schema.
|
|
5
|
+
--
|
|
6
|
+
-- Tables added:
|
|
7
|
+
-- - corepass_identities: maps CoreID to your Auth.js userId (adapter-generated or otherwise)
|
|
8
|
+
-- - corepass_profiles: stores CorePass-specific user metadata
|
|
9
|
+
-- - corepass_pending_registrations: stores passkey registrations until /passkey/data enrichment finalizes them
|
|
10
|
+
|
|
11
|
+
-- CoreID -> userId mapping
|
|
12
|
+
CREATE TABLE IF NOT EXISTS corepass_identities (
|
|
13
|
+
core_id TEXT PRIMARY KEY,
|
|
14
|
+
user_id TEXT NOT NULL UNIQUE,
|
|
15
|
+
ref_id TEXT,
|
|
16
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
|
17
|
+
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
-- CorePass user metadata (optional)
|
|
21
|
+
CREATE TABLE IF NOT EXISTS corepass_profiles (
|
|
22
|
+
user_id TEXT PRIMARY KEY,
|
|
23
|
+
core_id TEXT NOT NULL UNIQUE,
|
|
24
|
+
o18y INTEGER,
|
|
25
|
+
o21y INTEGER,
|
|
26
|
+
kyc INTEGER,
|
|
27
|
+
kyc_doc TEXT,
|
|
28
|
+
provided_till INTEGER,
|
|
29
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
|
30
|
+
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
-- Pending registrations (default CorePass flow)
|
|
34
|
+
CREATE TABLE IF NOT EXISTS corepass_pending_registrations (
|
|
35
|
+
token TEXT PRIMARY KEY,
|
|
36
|
+
credential_id TEXT NOT NULL UNIQUE, -- base64 credential id
|
|
37
|
+
credential_public_key TEXT NOT NULL, -- base64 public key
|
|
38
|
+
counter INTEGER NOT NULL DEFAULT 0,
|
|
39
|
+
credential_device_type TEXT NOT NULL,
|
|
40
|
+
credential_backed_up INTEGER NOT NULL DEFAULT 0,
|
|
41
|
+
transports TEXT,
|
|
42
|
+
email TEXT,
|
|
43
|
+
ref_id TEXT,
|
|
44
|
+
aaguid TEXT,
|
|
45
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
|
46
|
+
expires_at INTEGER NOT NULL
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_corepass_pending_expires_at
|
|
50
|
+
ON corepass_pending_registrations(expires_at);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// src/provider.ts
|
|
2
|
+
import WebAuthn, {
|
|
3
|
+
DEFAULT_WEBAUTHN_TIMEOUT
|
|
4
|
+
} from "@auth/core/providers/webauthn";
|
|
5
|
+
function CorePass(config = {}) {
|
|
6
|
+
return WebAuthn({
|
|
7
|
+
id: "corepass",
|
|
8
|
+
name: "CorePass",
|
|
9
|
+
authenticationOptions: {
|
|
10
|
+
timeout: DEFAULT_WEBAUTHN_TIMEOUT,
|
|
11
|
+
userVerification: "required"
|
|
12
|
+
},
|
|
13
|
+
registrationOptions: {
|
|
14
|
+
timeout: DEFAULT_WEBAUTHN_TIMEOUT,
|
|
15
|
+
authenticatorSelection: {
|
|
16
|
+
residentKey: "required",
|
|
17
|
+
userVerification: "required"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
verifyAuthenticationOptions: {
|
|
21
|
+
requireUserVerification: true
|
|
22
|
+
},
|
|
23
|
+
verifyRegistrationOptions: {
|
|
24
|
+
requireUserVerification: true
|
|
25
|
+
},
|
|
26
|
+
...config
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
CorePass
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=chunk-72ZEQHQO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/provider.ts"],"sourcesContent":["import WebAuthn, {\n\tDEFAULT_WEBAUTHN_TIMEOUT,\n\ttype WebAuthnConfig,\n} from \"@auth/core/providers/webauthn\"\n\n/**\n * CorePass Auth.js provider.\n *\n * This provider is a thin wrapper around Auth.js' built-in WebAuthn provider with\n * Passkey-friendly defaults. CorePass' pending-registration + enrichment flow is\n * implemented via the server helpers exported from this package (see `createCorePassServer`).\n */\nexport default function CorePass(\n\tconfig: Partial<WebAuthnConfig> = {}\n): WebAuthnConfig {\n\treturn WebAuthn({\n\t\tid: \"corepass\",\n\t\tname: \"CorePass\",\n\t\tauthenticationOptions: {\n\t\t\ttimeout: DEFAULT_WEBAUTHN_TIMEOUT,\n\t\t\tuserVerification: \"required\",\n\t\t},\n\t\tregistrationOptions: {\n\t\t\ttimeout: DEFAULT_WEBAUTHN_TIMEOUT,\n\t\t\tauthenticatorSelection: {\n\t\t\t\tresidentKey: \"required\",\n\t\t\t\tuserVerification: \"required\",\n\t\t\t},\n\t\t},\n\t\tverifyAuthenticationOptions: {\n\t\t\trequireUserVerification: true,\n\t\t},\n\t\tverifyRegistrationOptions: {\n\t\t\trequireUserVerification: true,\n\t\t},\n\t\t...config,\n\t})\n}\n"],"mappings":";AAAA,OAAO;AAAA,EACN;AAAA,OAEM;AASQ,SAAR,SACN,SAAkC,CAAC,GAClB;AACjB,SAAO,SAAS;AAAA,IACf,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,uBAAuB;AAAA,MACtB,SAAS;AAAA,MACT,kBAAkB;AAAA,IACnB;AAAA,IACA,qBAAqB;AAAA,MACpB,SAAS;AAAA,MACT,wBAAwB;AAAA,QACvB,aAAa;AAAA,QACb,kBAAkB;AAAA,MACnB;AAAA,IACD;AAAA,IACA,6BAA6B;AAAA,MAC5B,yBAAyB;AAAA,IAC1B;AAAA,IACA,2BAA2B;AAAA,MAC1B,yBAAyB;AAAA,IAC1B;AAAA,IACA,GAAG;AAAA,EACJ,CAAC;AACF;","names":[]}
|