@korajs/auth 1.0.0-beta.12 → 1.0.0-beta.14

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.
Files changed (101) hide show
  1. package/README.md +52 -47
  2. package/dist/{create-org-session-RsDj9cl4.d.cts → create-org-session-ChFdulEM.d.cts} +211 -17
  3. package/dist/{create-org-session-RsDj9cl4.d.ts → create-org-session-ChFdulEM.d.ts} +211 -17
  4. package/dist/index.cjs +645 -150
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +27 -9
  7. package/dist/index.d.ts +27 -9
  8. package/dist/index.js +644 -150
  9. package/dist/index.js.map +1 -1
  10. package/dist/{operation-encryptor-DRmKNWpF.d.cts → operation-encryptor-DDdlb9bm.d.cts} +16 -0
  11. package/dist/{operation-encryptor-DRmKNWpF.d.ts → operation-encryptor-DDdlb9bm.d.ts} +16 -0
  12. package/dist/react.d.cts +2 -2
  13. package/dist/react.d.ts +2 -2
  14. package/dist/server.cjs +2880 -1667
  15. package/dist/server.cjs.map +1 -1
  16. package/dist/server.d.cts +810 -169
  17. package/dist/server.d.ts +810 -169
  18. package/dist/server.js +2848 -1646
  19. package/dist/server.js.map +1 -1
  20. package/dist/svelte.cjs +2 -2
  21. package/dist/svelte.cjs.map +1 -1
  22. package/dist/svelte.d.cts +2 -2
  23. package/dist/svelte.d.ts +2 -2
  24. package/dist/svelte.js +2 -2
  25. package/dist/svelte.js.map +1 -1
  26. package/dist/vue.d.cts +1 -1
  27. package/dist/vue.d.ts +1 -1
  28. package/package.json +7 -7
  29. package/src/admin/admin-api.ts +327 -0
  30. package/src/admin/audit-log.ts +324 -0
  31. package/src/admin/webhooks.ts +576 -0
  32. package/src/bindings/create-auth-session.ts +184 -0
  33. package/src/bindings/create-org-session.ts +130 -0
  34. package/src/client/auth-client.ts +1592 -0
  35. package/src/client/auth-sync.ts +213 -0
  36. package/src/client/device-session.ts +104 -0
  37. package/src/client/org-client.ts +399 -0
  38. package/src/client/quickstart.ts +108 -0
  39. package/src/client/storage.ts +94 -0
  40. package/src/device/device-identity.ts +330 -0
  41. package/src/device/device-store.ts +379 -0
  42. package/src/encryption/auto-lock.ts +170 -0
  43. package/src/encryption/database-encryption.ts +265 -0
  44. package/src/encryption/key-derivation.ts +149 -0
  45. package/src/encryption/operation-encryptor.ts +361 -0
  46. package/src/index.ts +132 -0
  47. package/src/mfa/totp.ts +826 -0
  48. package/src/org/org-routes.ts +758 -0
  49. package/src/org/org-store.ts +490 -0
  50. package/src/org/org-types.ts +230 -0
  51. package/src/passkey/passkey-client.ts +597 -0
  52. package/src/passkey/passkey-server.ts +779 -0
  53. package/src/postgres/ensure-schema.ts +65 -0
  54. package/src/provider/adapter.ts +246 -0
  55. package/src/provider/built-in/auth-routes.ts +1313 -0
  56. package/src/provider/built-in/email-verification.ts +303 -0
  57. package/src/provider/built-in/password-hash.ts +118 -0
  58. package/src/provider/built-in/password-reset.ts +416 -0
  59. package/src/provider/built-in/postgres-user-store.ts +365 -0
  60. package/src/provider/built-in/quickstart-server.ts +760 -0
  61. package/src/provider/built-in/sqlite-user-store.ts +335 -0
  62. package/src/provider/built-in/sync-scopes.ts +85 -0
  63. package/src/provider/built-in/user-store.ts +465 -0
  64. package/src/provider/external/clerk-adapter.ts +157 -0
  65. package/src/provider/external/external-jwt-provider.ts +491 -0
  66. package/src/provider/external/supabase-adapter.ts +163 -0
  67. package/src/provider/oauth/linked-identity-store.ts +108 -0
  68. package/src/provider/oauth/oauth-flow.ts +550 -0
  69. package/src/provider/oauth/oauth-types.ts +184 -0
  70. package/src/provider/oauth/postgres-oauth-store.ts +296 -0
  71. package/src/provider/oauth/sqlite-oauth-store.ts +285 -0
  72. package/src/rbac/rbac-engine.ts +323 -0
  73. package/src/rbac/rbac-types.ts +210 -0
  74. package/src/rbac/scope-resolver.ts +140 -0
  75. package/src/react/AuthProvider.tsx +97 -0
  76. package/src/react/OrgProvider.tsx +41 -0
  77. package/src/react/auth-context.ts +26 -0
  78. package/src/react/hooks.ts +110 -0
  79. package/src/react/org-hooks.ts +214 -0
  80. package/src/react.ts +26 -0
  81. package/src/server.ts +338 -0
  82. package/src/session/session.ts +401 -0
  83. package/src/svelte/auth-context.ts +50 -0
  84. package/src/svelte/org-context.ts +32 -0
  85. package/src/svelte/org-hooks.ts +201 -0
  86. package/src/svelte/use-auth.ts +115 -0
  87. package/src/svelte.ts +25 -0
  88. package/src/tokens/encrypted-token-store.ts +360 -0
  89. package/src/tokens/jwt.ts +236 -0
  90. package/src/tokens/postgres-token-revocation-store.ts +140 -0
  91. package/src/tokens/sqlite-token-revocation-store.ts +121 -0
  92. package/src/tokens/token-manager.ts +821 -0
  93. package/src/tokens/token-store.ts +192 -0
  94. package/src/types.ts +394 -0
  95. package/src/vue/auth-context.ts +10 -0
  96. package/src/vue/auth-provider-types.ts +5 -0
  97. package/src/vue/auth-provider.ts +76 -0
  98. package/src/vue/org-hooks.ts +193 -0
  99. package/src/vue/org-provider.ts +49 -0
  100. package/src/vue/use-auth.ts +139 -0
  101. package/src/vue.ts +10 -0
@@ -0,0 +1,303 @@
1
+ import { KoraError } from '@korajs/core'
2
+ import type { UserStore } from './user-store'
3
+
4
+ // ============================================================================
5
+ // Types
6
+ // ============================================================================
7
+
8
+ /**
9
+ * A pending email verification token.
10
+ */
11
+ export interface EmailVerificationToken {
12
+ /** Cryptographically random single-use token */
13
+ token: string
14
+ /** User ID the token was generated for */
15
+ userId: string
16
+ /** Email being verified */
17
+ email: string
18
+ /** When the token was created (ms since epoch) */
19
+ createdAt: number
20
+ /** When the token expires (ms since epoch) */
21
+ expiresAt: number
22
+ /** Whether the token has been consumed */
23
+ consumed: boolean
24
+ }
25
+
26
+ /**
27
+ * Persistence interface for email verification tokens.
28
+ */
29
+ export interface EmailVerificationStore {
30
+ /** Store a verification token. */
31
+ store(token: EmailVerificationToken): Promise<void>
32
+
33
+ /** Look up a token. Returns null if not found. */
34
+ get(token: string): Promise<EmailVerificationToken | null>
35
+
36
+ /** Mark a token as consumed. */
37
+ consume(token: string): Promise<void>
38
+
39
+ /** Count active (non-consumed, non-expired) tokens for a user. */
40
+ countActiveForUser(userId: string): Promise<number>
41
+
42
+ /** Remove expired tokens. */
43
+ cleanExpired(): Promise<number>
44
+ }
45
+
46
+ /**
47
+ * Configuration for the email verification flow.
48
+ */
49
+ export interface EmailVerificationConfig {
50
+ /** User store for looking up users */
51
+ userStore: UserStore
52
+ /** Store for verification tokens. Defaults to InMemoryEmailVerificationStore. */
53
+ verificationStore?: EmailVerificationStore
54
+ /** Token TTL in milliseconds. Defaults to 24 hours. */
55
+ tokenTtlMs?: number
56
+ /** Max verification requests per user per TTL window. Defaults to 3. */
57
+ maxRequestsPerUser?: number
58
+ /**
59
+ * Callback invoked when a verification email should be sent.
60
+ * The developer must implement email sending.
61
+ * If not provided, the token is returned in the route response (development mode).
62
+ */
63
+ onVerificationRequired?: (email: string, token: string, expiresAt: number) => void | Promise<void>
64
+ }
65
+
66
+ // ============================================================================
67
+ // Errors
68
+ // ============================================================================
69
+
70
+ export class EmailVerificationError extends KoraError {
71
+ constructor(message: string, code: string, context?: Record<string, unknown>) {
72
+ super(message, code, context)
73
+ this.name = 'EmailVerificationError'
74
+ }
75
+ }
76
+
77
+ export class VerificationTokenExpiredError extends EmailVerificationError {
78
+ constructor() {
79
+ super('Email verification token has expired.', 'VERIFICATION_TOKEN_EXPIRED')
80
+ }
81
+ }
82
+
83
+ export class VerificationTokenNotFoundError extends EmailVerificationError {
84
+ constructor() {
85
+ super('Email verification token not found or already used.', 'VERIFICATION_TOKEN_NOT_FOUND')
86
+ }
87
+ }
88
+
89
+ // ============================================================================
90
+ // InMemoryEmailVerificationStore
91
+ // ============================================================================
92
+
93
+ export class InMemoryEmailVerificationStore implements EmailVerificationStore {
94
+ private tokens = new Map<string, EmailVerificationToken>()
95
+
96
+ async store(token: EmailVerificationToken): Promise<void> {
97
+ this.tokens.set(token.token, token)
98
+ }
99
+
100
+ async get(token: string): Promise<EmailVerificationToken | null> {
101
+ return this.tokens.get(token) ?? null
102
+ }
103
+
104
+ async consume(token: string): Promise<void> {
105
+ const entry = this.tokens.get(token)
106
+ if (entry) {
107
+ this.tokens.set(token, { ...entry, consumed: true })
108
+ }
109
+ }
110
+
111
+ async countActiveForUser(userId: string): Promise<number> {
112
+ const now = Date.now()
113
+ let count = 0
114
+ for (const token of this.tokens.values()) {
115
+ if (token.userId === userId && !token.consumed && now < token.expiresAt) {
116
+ count++
117
+ }
118
+ }
119
+ return count
120
+ }
121
+
122
+ async cleanExpired(): Promise<number> {
123
+ const now = Date.now()
124
+ let count = 0
125
+ for (const [key, token] of this.tokens) {
126
+ if (now > token.expiresAt) {
127
+ this.tokens.delete(key)
128
+ count++
129
+ }
130
+ }
131
+ return count
132
+ }
133
+ }
134
+
135
+ // ============================================================================
136
+ // EmailVerificationManager
137
+ // ============================================================================
138
+
139
+ /** Default TTL: 24 hours */
140
+ const DEFAULT_TOKEN_TTL_MS = 24 * 60 * 60 * 1000
141
+
142
+ /** Default max requests per user per TTL window */
143
+ const DEFAULT_MAX_REQUESTS = 3
144
+
145
+ /**
146
+ * Manages the email verification flow.
147
+ *
148
+ * @example
149
+ * ```typescript
150
+ * const verifier = new EmailVerificationManager({
151
+ * userStore,
152
+ * onVerificationRequired: async (email, token, expiresAt) => {
153
+ * await sendEmail(email, `Verify: https://app.com/verify?token=${token}`)
154
+ * },
155
+ * })
156
+ *
157
+ * // Send verification email
158
+ * await verifier.sendVerification('user-1', 'user@example.com')
159
+ *
160
+ * // Verify email
161
+ * await verifier.verifyEmail(token)
162
+ * ```
163
+ */
164
+ export class EmailVerificationManager {
165
+ private readonly userStore: UserStore
166
+ private readonly verificationStore: EmailVerificationStore
167
+ private readonly tokenTtlMs: number
168
+ private readonly maxRequestsPerUser: number
169
+ private readonly onVerificationRequired?: (
170
+ email: string,
171
+ token: string,
172
+ expiresAt: number,
173
+ ) => void | Promise<void>
174
+
175
+ constructor(config: EmailVerificationConfig) {
176
+ this.userStore = config.userStore
177
+ this.verificationStore = config.verificationStore ?? new InMemoryEmailVerificationStore()
178
+ this.tokenTtlMs = config.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS
179
+ this.maxRequestsPerUser = config.maxRequestsPerUser ?? DEFAULT_MAX_REQUESTS
180
+ this.onVerificationRequired = config.onVerificationRequired
181
+ }
182
+
183
+ /**
184
+ * Send a verification email for a user.
185
+ * Rate-limited to prevent abuse.
186
+ */
187
+ async sendVerification(
188
+ userId: string,
189
+ email: string,
190
+ ): Promise<{
191
+ status: number
192
+ body: { data: { message: string; token?: string } } | { error: string }
193
+ }> {
194
+ const normalizedEmail = email.toLowerCase().trim()
195
+
196
+ // Rate limit
197
+ const activeCount = await this.verificationStore.countActiveForUser(userId)
198
+ if (activeCount >= this.maxRequestsPerUser) {
199
+ return {
200
+ status: 429,
201
+ body: { error: 'Too many verification requests. Please try again later.' },
202
+ }
203
+ }
204
+
205
+ // Generate token
206
+ const token = generateSecureToken()
207
+ const now = Date.now()
208
+ const verificationToken: EmailVerificationToken = {
209
+ token,
210
+ userId,
211
+ email: normalizedEmail,
212
+ createdAt: now,
213
+ expiresAt: now + this.tokenTtlMs,
214
+ consumed: false,
215
+ }
216
+
217
+ await this.verificationStore.store(verificationToken)
218
+
219
+ // Invoke callback
220
+ if (this.onVerificationRequired) {
221
+ try {
222
+ await this.onVerificationRequired(normalizedEmail, token, verificationToken.expiresAt)
223
+ } catch {
224
+ // Don't fail if callback errors
225
+ }
226
+ }
227
+
228
+ // In development mode (no callback), return the token
229
+ const responseData: { message: string; token?: string } = {
230
+ message: 'Verification email sent.',
231
+ }
232
+ if (!this.onVerificationRequired) {
233
+ responseData.token = token
234
+ }
235
+
236
+ return { status: 200, body: { data: responseData } }
237
+ }
238
+
239
+ /**
240
+ * Verify an email using a verification token.
241
+ */
242
+ async verifyEmail(token: string): Promise<{
243
+ status: number
244
+ body: { data: { message: string; userId: string; email: string } } | { error: string }
245
+ }> {
246
+ const verificationToken = await this.verificationStore.get(token)
247
+ if (!verificationToken || verificationToken.consumed) {
248
+ return { status: 404, body: { error: 'Verification token not found or already used.' } }
249
+ }
250
+
251
+ if (Date.now() > verificationToken.expiresAt) {
252
+ await this.verificationStore.consume(token)
253
+ return { status: 410, body: { error: 'Verification token has expired.' } }
254
+ }
255
+
256
+ // Consume token
257
+ await this.verificationStore.consume(token)
258
+
259
+ // Mark user's email as verified
260
+ await this.userStore.setEmailVerified(verificationToken.userId, true)
261
+
262
+ return {
263
+ status: 200,
264
+ body: {
265
+ data: {
266
+ message: 'Email verified successfully.',
267
+ userId: verificationToken.userId,
268
+ email: verificationToken.email,
269
+ },
270
+ },
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Resend verification email for a user.
276
+ * Delegates to sendVerification with rate limiting.
277
+ */
278
+ async resendVerification(userId: string): Promise<{
279
+ status: number
280
+ body: { data: { message: string; token?: string } } | { error: string }
281
+ }> {
282
+ const user = await this.userStore.findById(userId)
283
+ if (!user) {
284
+ return { status: 404, body: { error: 'User not found.' } }
285
+ }
286
+
287
+ return this.sendVerification(userId, user.email)
288
+ }
289
+ }
290
+
291
+ // ============================================================================
292
+ // Helpers
293
+ // ============================================================================
294
+
295
+ function generateSecureToken(): string {
296
+ const bytes = new Uint8Array(32)
297
+ globalThis.crypto.getRandomValues(bytes)
298
+ let binary = ''
299
+ for (let i = 0; i < bytes.length; i++) {
300
+ binary += String.fromCharCode(bytes[i] as number)
301
+ }
302
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
303
+ }
@@ -0,0 +1,118 @@
1
+ import { pbkdf2, randomBytes, timingSafeEqual } from 'node:crypto'
2
+
3
+ /** Number of PBKDF2 iterations. 600,000 per OWASP 2023 recommendations for SHA-512. */
4
+ const PBKDF2_ITERATIONS = 600_000
5
+
6
+ /** Digest algorithm used by PBKDF2. */
7
+ const PBKDF2_DIGEST = 'sha512'
8
+
9
+ /** Length of the derived key in bytes. */
10
+ const KEY_LENGTH = 64
11
+
12
+ /** Length of the random salt in bytes. */
13
+ const SALT_LENGTH = 32
14
+
15
+ interface HashResult {
16
+ /** Hex-encoded PBKDF2 derived key. */
17
+ hash: string
18
+ /** Hex-encoded random salt used during hashing. */
19
+ salt: string
20
+ }
21
+
22
+ /**
23
+ * Hashes a password using PBKDF2 with a cryptographically random salt.
24
+ *
25
+ * Uses SHA-512 with 600,000 iterations and a 32-byte random salt,
26
+ * producing a 64-byte derived key. Both the hash and salt are returned
27
+ * as hex-encoded strings for storage.
28
+ *
29
+ * @param password - The plaintext password to hash
30
+ * @returns The hex-encoded hash and salt
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * const { hash, salt } = await hashPassword('my-secret-password')
35
+ * // Store hash and salt in the database
36
+ * ```
37
+ */
38
+ export async function hashPassword(password: string): Promise<HashResult> {
39
+ const salt = randomBytes(SALT_LENGTH)
40
+
41
+ const derivedKey = await pbkdf2Async(password, salt, PBKDF2_ITERATIONS, KEY_LENGTH, PBKDF2_DIGEST)
42
+
43
+ return {
44
+ hash: derivedKey.toString('hex'),
45
+ salt: salt.toString('hex'),
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Verifies a plaintext password against a stored hash and salt using
51
+ * timing-safe comparison to prevent timing attacks.
52
+ *
53
+ * Re-derives the key from the password and salt using the same PBKDF2
54
+ * parameters, then compares the result against the stored hash using
55
+ * `crypto.timingSafeEqual` to avoid leaking information through
56
+ * response timing.
57
+ *
58
+ * @param password - The plaintext password to verify
59
+ * @param hash - The hex-encoded hash to compare against
60
+ * @param salt - The hex-encoded salt that was used to produce the hash
61
+ * @returns `true` if the password matches, `false` otherwise
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * const isValid = await verifyPassword('my-secret-password', storedHash, storedSalt)
66
+ * if (isValid) {
67
+ * // Grant access
68
+ * }
69
+ * ```
70
+ */
71
+ export async function verifyPassword(
72
+ password: string,
73
+ hash: string,
74
+ salt: string,
75
+ ): Promise<boolean> {
76
+ const saltBuffer = Buffer.from(salt, 'hex')
77
+
78
+ const derivedKey = await pbkdf2Async(
79
+ password,
80
+ saltBuffer,
81
+ PBKDF2_ITERATIONS,
82
+ KEY_LENGTH,
83
+ PBKDF2_DIGEST,
84
+ )
85
+
86
+ const hashBuffer = Buffer.from(hash, 'hex')
87
+
88
+ // Both buffers must be the same length for timingSafeEqual.
89
+ // If the stored hash has an unexpected length, reject rather than throw.
90
+ if (derivedKey.length !== hashBuffer.length) {
91
+ return false
92
+ }
93
+
94
+ return timingSafeEqual(derivedKey, hashBuffer)
95
+ }
96
+
97
+ /**
98
+ * Promisified wrapper around Node.js `crypto.pbkdf2`.
99
+ * The callback-based API delegates hashing to libuv's thread pool,
100
+ * avoiding blocking the event loop during the 600,000 iterations.
101
+ */
102
+ function pbkdf2Async(
103
+ password: string,
104
+ salt: Buffer,
105
+ iterations: number,
106
+ keyLength: number,
107
+ digest: string,
108
+ ): Promise<Buffer> {
109
+ return new Promise((resolve, reject) => {
110
+ pbkdf2(password, salt, iterations, keyLength, digest, (err, derivedKey) => {
111
+ if (err) {
112
+ reject(err)
113
+ } else {
114
+ resolve(derivedKey)
115
+ }
116
+ })
117
+ })
118
+ }