@everystack/auth 0.1.0 → 0.2.1
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/README.md +1 -7
- package/package.json +33 -22
- package/src/api.ts +42 -8
- package/src/client/index.tsx +39 -7
- package/src/cookies.ts +3 -0
- package/src/csrf.ts +57 -0
- package/src/index.ts +276 -46
- package/src/jwt/index.ts +4 -3
- package/src/passwords.ts +1 -1
- package/src/plugin.ts +264 -0
- package/src/postgres.ts +310 -59
- package/src/refresh.ts +121 -57
- package/src/schema.ts +222 -130
- package/src/sql.ts +715 -73
- package/src/testing.ts +232 -0
package/src/postgres.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Password hashing: crypt() / gen_salt('bf') via auth.hash_password()
|
|
5
5
|
* JWT signing: hmac() via auth.sign_jwt()
|
|
6
|
-
* Refresh tokens: auth.
|
|
6
|
+
* Refresh tokens: auth.issue_refresh_token() / auth.validate_refresh_token()
|
|
7
7
|
*
|
|
8
8
|
* Uses postgres.js tagged templates directly (not drizzle's sql) because
|
|
9
9
|
* drizzle expands arrays into individual params instead of PostgreSQL arrays.
|
|
@@ -15,16 +15,52 @@
|
|
|
15
15
|
import { and, eq } from 'drizzle-orm';
|
|
16
16
|
import { createJwt } from './jwt';
|
|
17
17
|
import { hmacNode } from './jwt/hmac-node';
|
|
18
|
+
import type { RefreshTokenOptions } from './refresh';
|
|
18
19
|
import type { AuthHandlers, AuthOptions, TokenPayload } from './index';
|
|
20
|
+
import { getPasswordHash, withPassword } from './index';
|
|
19
21
|
import type { OAuthProvider } from './oauth';
|
|
20
22
|
import { getRefreshFromCookies } from './cookies';
|
|
21
23
|
import { authResponse, signoutResponse } from './response';
|
|
24
|
+
import type { CookieConfig } from './response';
|
|
25
|
+
import { validateCsrfOrigin } from './csrf';
|
|
22
26
|
|
|
23
27
|
const TOKEN_EXPIRY_SECONDS = 3600; // 1 hour
|
|
24
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Dummy bcrypt hash for timing-safe user enumeration protection.
|
|
31
|
+
* When a user is not found, we still run bcrypt against this hash
|
|
32
|
+
* so response time is constant regardless of whether the email exists.
|
|
33
|
+
*/
|
|
34
|
+
const DUMMY_BCRYPT_HASH = '$2a$10$abcdefghijklmnopqrstuuABCDEFGHIJKLMNOPQRSTUVWXYZ012345';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extract client IP and user agent from a request for refresh token metadata.
|
|
38
|
+
* When repositoryId is provided, it's included for multi-tenant scoping.
|
|
39
|
+
*/
|
|
40
|
+
function extractRequestMeta(request: Request, repositoryId?: string): RefreshTokenOptions {
|
|
41
|
+
const forwarded = request.headers.get('x-forwarded-for');
|
|
42
|
+
const ipAddress = forwarded ? forwarded.split(',')[0].trim() : undefined;
|
|
43
|
+
const userAgent = request.headers.get('user-agent') ?? undefined;
|
|
44
|
+
return { repositoryId, ipAddress, userAgent };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Run CSRF origin check if cookies are configured with allowedOrigins. */
|
|
48
|
+
function csrfCheck(request: Request, cookies?: CookieConfig): Response | null {
|
|
49
|
+
if (!cookies || cookies === true) return null;
|
|
50
|
+
return validateCsrfOrigin(request, cookies.allowedOrigins);
|
|
51
|
+
}
|
|
52
|
+
|
|
25
53
|
export function createPostgresAuthHandlers(
|
|
26
54
|
db: any,
|
|
27
|
-
schema: {
|
|
55
|
+
schema: {
|
|
56
|
+
users: any;
|
|
57
|
+
profiles: any;
|
|
58
|
+
refreshTokens?: any;
|
|
59
|
+
/** Legacy OAuth tables. Use `oauth.callbacks` instead for new apps. */
|
|
60
|
+
credentials?: any;
|
|
61
|
+
/** Legacy OAuth tables. Use `oauth.callbacks` instead for new apps. */
|
|
62
|
+
identities?: any;
|
|
63
|
+
},
|
|
28
64
|
jwtSecret: Uint8Array,
|
|
29
65
|
options?: AuthOptions,
|
|
30
66
|
): AuthHandlers {
|
|
@@ -34,16 +70,25 @@ export function createPostgresAuthHandlers(
|
|
|
34
70
|
}
|
|
35
71
|
|
|
36
72
|
const { users, profiles } = schema;
|
|
73
|
+
const refreshTable = schema.refreshTokens;
|
|
37
74
|
const jwtSecretString = new TextDecoder().decode(jwtSecret);
|
|
38
75
|
|
|
39
76
|
/**
|
|
40
77
|
* Sign JWT via PostgreSQL pgcrypto hmac().
|
|
41
78
|
* Requires GUC app.jwt_secret — uses pgSql.begin() for transaction-local setting.
|
|
79
|
+
* When enrichClaims is configured and a request is provided, merges extra claims.
|
|
42
80
|
*/
|
|
43
|
-
async function signJwtPg(payload: Record<string, unknown
|
|
81
|
+
async function signJwtPg(payload: Record<string, unknown>, request?: Request): Promise<string> {
|
|
82
|
+
let claims = payload;
|
|
83
|
+
if (options?.enrichClaims && request) {
|
|
84
|
+
const extra = await options.enrichClaims(claims, request);
|
|
85
|
+
if (extra && Object.keys(extra).length > 0) {
|
|
86
|
+
claims = { ...claims, ...extra };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
44
89
|
const now = Math.floor(Date.now() / 1000);
|
|
45
90
|
const fullPayload = {
|
|
46
|
-
...
|
|
91
|
+
...claims,
|
|
47
92
|
iat: now,
|
|
48
93
|
exp: now + TOKEN_EXPIRY_SECONDS,
|
|
49
94
|
};
|
|
@@ -72,29 +117,62 @@ export function createPostgresAuthHandlers(
|
|
|
72
117
|
}
|
|
73
118
|
|
|
74
119
|
/**
|
|
75
|
-
*
|
|
120
|
+
* Issue refresh token via PostgreSQL SECURITY DEFINER function.
|
|
121
|
+
* Handles family ceiling, SHA-256 hashing, and token generation atomically.
|
|
76
122
|
*/
|
|
77
|
-
async function
|
|
78
|
-
|
|
79
|
-
|
|
123
|
+
async function issueRefreshTokenPg(
|
|
124
|
+
userId: string,
|
|
125
|
+
repositoryId?: string,
|
|
126
|
+
ipAddress?: string,
|
|
127
|
+
userAgent?: string,
|
|
128
|
+
deviceId?: string,
|
|
129
|
+
): Promise<{ token: string; familyId: string }> {
|
|
130
|
+
const repoId = repositoryId ?? null;
|
|
131
|
+
const rows = await pgSql`SELECT auth.issue_refresh_token(
|
|
132
|
+
${userId}::uuid,
|
|
133
|
+
NULL::uuid,
|
|
134
|
+
${ipAddress ?? null},
|
|
135
|
+
${userAgent ?? null},
|
|
136
|
+
${deviceId ?? null}::uuid,
|
|
137
|
+
${repoId}::uuid
|
|
138
|
+
) as result`;
|
|
139
|
+
const result = typeof rows[0].result === 'string' ? JSON.parse(rows[0].result) : rows[0].result;
|
|
140
|
+
return { token: result.token, familyId: result.familyId };
|
|
80
141
|
}
|
|
81
142
|
|
|
82
143
|
/**
|
|
83
144
|
* Validate and rotate refresh token via PostgreSQL.
|
|
84
|
-
* The SQL function handles
|
|
85
|
-
* Returns { userId, token, familyId }.
|
|
145
|
+
* The SQL function handles SELECT FOR UPDATE, reuse detection, user state check.
|
|
86
146
|
*/
|
|
87
|
-
async function validateRefreshTokenPg(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
147
|
+
async function validateRefreshTokenPg(
|
|
148
|
+
token: string,
|
|
149
|
+
deviceId?: string,
|
|
150
|
+
ipAddress?: string,
|
|
151
|
+
userAgent?: string,
|
|
152
|
+
): Promise<{ userId: string; token: string; familyId: string }> {
|
|
153
|
+
const rows = await pgSql`SELECT auth.validate_refresh_token(
|
|
154
|
+
${token},
|
|
155
|
+
${deviceId ?? null}::uuid,
|
|
156
|
+
${ipAddress ?? null},
|
|
157
|
+
${userAgent ?? null}
|
|
158
|
+
) as result`;
|
|
159
|
+
const result = typeof rows[0].result === 'string' ? JSON.parse(rows[0].result) : rows[0].result;
|
|
160
|
+
// token_reused returns as JSONB { error: 'token_reused' } instead of RAISE
|
|
161
|
+
// because PostgreSQL rolls back DELETEs when exceptions propagate.
|
|
162
|
+
if (result.error === 'token_reused') {
|
|
163
|
+
const err: any = new Error('token_reused');
|
|
164
|
+
err.code = 'P0002';
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
return { userId: result.userId, token: result.token, familyId: result.familyId };
|
|
91
168
|
}
|
|
92
169
|
|
|
93
170
|
/**
|
|
94
|
-
* Revoke all refresh tokens for a user.
|
|
171
|
+
* Revoke all refresh tokens for a user, optionally scoped by repository.
|
|
95
172
|
*/
|
|
96
|
-
async function revokeRefreshTokensPg(userId: string): Promise<void> {
|
|
97
|
-
|
|
173
|
+
async function revokeRefreshTokensPg(userId: string, repositoryId?: string): Promise<void> {
|
|
174
|
+
const repoId = repositoryId ?? null;
|
|
175
|
+
await pgSql`SELECT auth.revoke_refresh_tokens(${userId}::uuid, NULL::uuid, ${repoId}::uuid)`;
|
|
98
176
|
}
|
|
99
177
|
|
|
100
178
|
// JS-side verifier (compatible with HS256 tokens minted by SQL or JS).
|
|
@@ -111,6 +189,10 @@ export function createPostgresAuthHandlers(
|
|
|
111
189
|
},
|
|
112
190
|
|
|
113
191
|
signup: async (request: Request): Promise<Response> => {
|
|
192
|
+
const csrfError = csrfCheck(request, options?.cookies);
|
|
193
|
+
if (csrfError) return csrfError;
|
|
194
|
+
|
|
195
|
+
const enrichRequest = options?.enrichClaims ? request.clone() : undefined;
|
|
114
196
|
const { email, password, username, displayName } = await request.json();
|
|
115
197
|
|
|
116
198
|
if (!email || !password || !username || !displayName) {
|
|
@@ -126,15 +208,21 @@ export function createPostgresAuthHandlers(
|
|
|
126
208
|
.where(eq(users.email, email))
|
|
127
209
|
.limit(1);
|
|
128
210
|
if (existing.length > 0) {
|
|
211
|
+
if (options?.uniformSignupResponse) {
|
|
212
|
+
return Response.json(
|
|
213
|
+
{ message: 'Check your email to continue.' },
|
|
214
|
+
{ status: 200 },
|
|
215
|
+
);
|
|
216
|
+
}
|
|
129
217
|
return Response.json(
|
|
130
218
|
{ message: 'Email already registered' },
|
|
131
219
|
{ status: 409 },
|
|
132
220
|
);
|
|
133
221
|
}
|
|
134
222
|
|
|
135
|
-
const
|
|
223
|
+
const hash = await hashPasswordPg(password);
|
|
136
224
|
const { user, profile } = await db.transaction(async (tx: any) => {
|
|
137
|
-
const [u] = await tx.insert(users).values({ email,
|
|
225
|
+
const [u] = await tx.insert(users).values(withPassword(users, { email }, hash)).returning();
|
|
138
226
|
const [p] = await tx.insert(profiles).values({ id: u.id, username, displayName }).returning();
|
|
139
227
|
return { user: u, profile: p };
|
|
140
228
|
});
|
|
@@ -143,11 +231,19 @@ export function createPostgresAuthHandlers(
|
|
|
143
231
|
sub: user.id,
|
|
144
232
|
email: user.email,
|
|
145
233
|
role: user.role ?? 'authenticated',
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
234
|
+
}, enrichRequest);
|
|
235
|
+
|
|
236
|
+
const meta = extractRequestMeta(request, options?.repositoryId);
|
|
237
|
+
let refreshToken: string;
|
|
238
|
+
if (refreshTable) {
|
|
239
|
+
// Prefer PG function path when sql client is available
|
|
240
|
+
const result = await issueRefreshTokenPg(
|
|
241
|
+
user.id, meta.repositoryId, meta.ipAddress, meta.userAgent,
|
|
242
|
+
);
|
|
243
|
+
refreshToken = result.token;
|
|
244
|
+
} else {
|
|
245
|
+
refreshToken = await signJwtPg({ sub: user.id });
|
|
246
|
+
}
|
|
151
247
|
|
|
152
248
|
return authResponse(
|
|
153
249
|
{
|
|
@@ -167,6 +263,10 @@ export function createPostgresAuthHandlers(
|
|
|
167
263
|
},
|
|
168
264
|
|
|
169
265
|
signin: async (request: Request): Promise<Response> => {
|
|
266
|
+
const csrfError = csrfCheck(request, options?.cookies);
|
|
267
|
+
if (csrfError) return csrfError;
|
|
268
|
+
|
|
269
|
+
const enrichRequest = options?.enrichClaims ? request.clone() : undefined;
|
|
170
270
|
const { email, password } = await request.json();
|
|
171
271
|
|
|
172
272
|
if (!email || !password) {
|
|
@@ -181,14 +281,18 @@ export function createPostgresAuthHandlers(
|
|
|
181
281
|
.from(users)
|
|
182
282
|
.where(eq(users.email, email))
|
|
183
283
|
.limit(1);
|
|
284
|
+
|
|
184
285
|
if (!user) {
|
|
286
|
+
// Timing-safe: run bcrypt even when user doesn't exist
|
|
287
|
+
// so response time is constant regardless of email existence
|
|
288
|
+
await verifyPasswordPg(password, DUMMY_BCRYPT_HASH);
|
|
185
289
|
return Response.json(
|
|
186
290
|
{ message: 'Invalid email or password' },
|
|
187
291
|
{ status: 401 },
|
|
188
292
|
);
|
|
189
293
|
}
|
|
190
294
|
|
|
191
|
-
const valid = await verifyPasswordPg(password, user
|
|
295
|
+
const valid = await verifyPasswordPg(password, getPasswordHash(user)!);
|
|
192
296
|
if (!valid) {
|
|
193
297
|
return Response.json(
|
|
194
298
|
{ message: 'Invalid email or password' },
|
|
@@ -206,11 +310,18 @@ export function createPostgresAuthHandlers(
|
|
|
206
310
|
sub: user.id,
|
|
207
311
|
email: user.email,
|
|
208
312
|
role: user.role ?? 'authenticated',
|
|
209
|
-
});
|
|
313
|
+
}, enrichRequest);
|
|
210
314
|
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
315
|
+
const meta = extractRequestMeta(request, options?.repositoryId);
|
|
316
|
+
let refreshToken: string;
|
|
317
|
+
if (refreshTable) {
|
|
318
|
+
const result = await issueRefreshTokenPg(
|
|
319
|
+
user.id, meta.repositoryId, meta.ipAddress, meta.userAgent,
|
|
320
|
+
);
|
|
321
|
+
refreshToken = result.token;
|
|
322
|
+
} else {
|
|
323
|
+
refreshToken = await signJwtPg({ sub: user.id });
|
|
324
|
+
}
|
|
214
325
|
|
|
215
326
|
return authResponse(
|
|
216
327
|
{
|
|
@@ -230,6 +341,9 @@ export function createPostgresAuthHandlers(
|
|
|
230
341
|
},
|
|
231
342
|
|
|
232
343
|
refresh: async (request: Request): Promise<Response> => {
|
|
344
|
+
const csrfError = csrfCheck(request, options?.cookies);
|
|
345
|
+
if (csrfError) return csrfError;
|
|
346
|
+
|
|
233
347
|
// Read refresh token from JSON body (mobile) or cookie (web SSR)
|
|
234
348
|
let refreshToken: string | undefined;
|
|
235
349
|
try {
|
|
@@ -248,10 +362,13 @@ export function createPostgresAuthHandlers(
|
|
|
248
362
|
);
|
|
249
363
|
}
|
|
250
364
|
|
|
251
|
-
if (
|
|
252
|
-
// DB-backed rotation via
|
|
365
|
+
if (refreshTable) {
|
|
366
|
+
// DB-backed rotation via PG SECURITY DEFINER functions (preferred)
|
|
253
367
|
try {
|
|
254
|
-
const
|
|
368
|
+
const meta = extractRequestMeta(request, options?.repositoryId);
|
|
369
|
+
const result = await validateRefreshTokenPg(
|
|
370
|
+
refreshToken, undefined, meta.ipAddress, meta.userAgent,
|
|
371
|
+
);
|
|
255
372
|
|
|
256
373
|
const [user] = await db
|
|
257
374
|
.select()
|
|
@@ -259,6 +376,7 @@ export function createPostgresAuthHandlers(
|
|
|
259
376
|
.where(eq(users.id, result.userId))
|
|
260
377
|
.limit(1);
|
|
261
378
|
if (!user) {
|
|
379
|
+
await revokeRefreshTokensPg(result.userId, options?.repositoryId);
|
|
262
380
|
return Response.json({ message: 'User not found' }, { status: 401 });
|
|
263
381
|
}
|
|
264
382
|
|
|
@@ -266,18 +384,24 @@ export function createPostgresAuthHandlers(
|
|
|
266
384
|
sub: user.id,
|
|
267
385
|
email: user.email,
|
|
268
386
|
role: user.role ?? 'authenticated',
|
|
269
|
-
});
|
|
387
|
+
}, request);
|
|
270
388
|
|
|
271
389
|
return authResponse(
|
|
272
390
|
{ token, refreshToken: result.token },
|
|
273
391
|
200,
|
|
274
392
|
options?.cookies,
|
|
275
393
|
);
|
|
276
|
-
} catch {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
394
|
+
} catch (err: any) {
|
|
395
|
+
// Map PG error codes to HTTP responses
|
|
396
|
+
const pgCode = err?.code;
|
|
397
|
+
if (pgCode === 'P0001' || pgCode === 'P0002' || pgCode === 'P0003'
|
|
398
|
+
|| pgCode === 'P0004' || pgCode === 'P0005') {
|
|
399
|
+
return Response.json(
|
|
400
|
+
{ message: 'Invalid or expired refresh token' },
|
|
401
|
+
{ status: 401 },
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
throw err;
|
|
281
405
|
}
|
|
282
406
|
} else {
|
|
283
407
|
// Stateless JWT-based refresh (fallback)
|
|
@@ -301,7 +425,7 @@ export function createPostgresAuthHandlers(
|
|
|
301
425
|
sub: user.id,
|
|
302
426
|
email: user.email,
|
|
303
427
|
role: user.role ?? 'authenticated',
|
|
304
|
-
});
|
|
428
|
+
}, request);
|
|
305
429
|
const newRefreshToken = await signJwtPg({ sub: user.id });
|
|
306
430
|
|
|
307
431
|
return authResponse(
|
|
@@ -320,14 +444,21 @@ export function createPostgresAuthHandlers(
|
|
|
320
444
|
|
|
321
445
|
oauth: options?.oauth?.providers.length
|
|
322
446
|
? async (request: Request): Promise<Response> => {
|
|
323
|
-
|
|
447
|
+
const csrfError = csrfCheck(request, options?.cookies);
|
|
448
|
+
if (csrfError) return csrfError;
|
|
449
|
+
|
|
450
|
+
const enrichRequest = options?.enrichClaims ? request.clone() : undefined;
|
|
451
|
+
const oauthCallbacks = options?.oauth?.callbacks;
|
|
452
|
+
|
|
453
|
+
// Require either callbacks or legacy tables
|
|
454
|
+
if (!oauthCallbacks && (!schema.credentials || !schema.identities)) {
|
|
324
455
|
return Response.json(
|
|
325
|
-
{ message: 'OAuth not configured:
|
|
456
|
+
{ message: 'OAuth not configured: provide oauth.callbacks or credentials/identities tables' },
|
|
326
457
|
{ status: 500 },
|
|
327
458
|
);
|
|
328
459
|
}
|
|
329
460
|
|
|
330
|
-
let body: { provider?: string; token?: string };
|
|
461
|
+
let body: { provider?: string; token?: string; nonce?: string };
|
|
331
462
|
try {
|
|
332
463
|
body = await request.json();
|
|
333
464
|
} catch {
|
|
@@ -337,7 +468,7 @@ export function createPostgresAuthHandlers(
|
|
|
337
468
|
);
|
|
338
469
|
}
|
|
339
470
|
|
|
340
|
-
const { provider: providerName, token } = body;
|
|
471
|
+
const { provider: providerName, token, nonce } = body;
|
|
341
472
|
if (!providerName || !token) {
|
|
342
473
|
return Response.json(
|
|
343
474
|
{ message: 'provider and token are required' },
|
|
@@ -363,7 +494,112 @@ export function createPostgresAuthHandlers(
|
|
|
363
494
|
);
|
|
364
495
|
}
|
|
365
496
|
|
|
366
|
-
//
|
|
497
|
+
// Optional nonce validation for web OAuth flows
|
|
498
|
+
if (options?.oauth?.validateNonce) {
|
|
499
|
+
if (!nonce) {
|
|
500
|
+
return Response.json(
|
|
501
|
+
{ message: 'Nonce is required when nonce validation is configured' },
|
|
502
|
+
{ status: 400 },
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
const nonceValid = await options.oauth.validateNonce(nonce, userInfo);
|
|
506
|
+
if (!nonceValid) {
|
|
507
|
+
return Response.json(
|
|
508
|
+
{ message: 'Nonce validation failed' },
|
|
509
|
+
{ status: 401 },
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const meta = extractRequestMeta(request, options?.repositoryId);
|
|
515
|
+
|
|
516
|
+
if (oauthCallbacks) {
|
|
517
|
+
// Callback-based path (recommended) — no identities/credentials tables needed
|
|
518
|
+
const existing = await oauthCallbacks.findByProvider(providerName, userInfo.uid);
|
|
519
|
+
|
|
520
|
+
if (existing) {
|
|
521
|
+
const [existingUser] = await db
|
|
522
|
+
.select()
|
|
523
|
+
.from(users)
|
|
524
|
+
.where(eq(users.id, existing.userId))
|
|
525
|
+
.limit(1);
|
|
526
|
+
|
|
527
|
+
const [profile] = await db
|
|
528
|
+
.select()
|
|
529
|
+
.from(profiles)
|
|
530
|
+
.where(eq(profiles.id, existing.userId))
|
|
531
|
+
.limit(1);
|
|
532
|
+
|
|
533
|
+
const accessToken = await signJwtPg({
|
|
534
|
+
sub: existingUser.id,
|
|
535
|
+
email: existingUser.email,
|
|
536
|
+
role: existingUser.role ?? 'authenticated',
|
|
537
|
+
}, enrichRequest);
|
|
538
|
+
let refreshTokenStr: string;
|
|
539
|
+
if (refreshTable) {
|
|
540
|
+
const result = await issueRefreshTokenPg(
|
|
541
|
+
existingUser.id, meta.repositoryId, meta.ipAddress, meta.userAgent,
|
|
542
|
+
);
|
|
543
|
+
refreshTokenStr = result.token;
|
|
544
|
+
} else {
|
|
545
|
+
refreshTokenStr = await signJwtPg({ sub: existingUser.id });
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return authResponse(
|
|
549
|
+
{
|
|
550
|
+
token: accessToken,
|
|
551
|
+
refreshToken: refreshTokenStr,
|
|
552
|
+
user: {
|
|
553
|
+
id: existingUser.id,
|
|
554
|
+
email: existingUser.email,
|
|
555
|
+
username: profile?.username,
|
|
556
|
+
displayName: profile?.displayName,
|
|
557
|
+
avatarUrl: profile?.avatarUrl,
|
|
558
|
+
role: existingUser.role ?? 'authenticated',
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
200,
|
|
562
|
+
options?.cookies,
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// New user via callback
|
|
567
|
+
const newUser = await oauthCallbacks.createFromOAuth(userInfo, providerName);
|
|
568
|
+
|
|
569
|
+
const accessToken = await signJwtPg({
|
|
570
|
+
sub: newUser.id,
|
|
571
|
+
email: newUser.email,
|
|
572
|
+
role: newUser.role ?? 'authenticated',
|
|
573
|
+
}, enrichRequest);
|
|
574
|
+
let refreshTokenStr: string;
|
|
575
|
+
if (refreshTable) {
|
|
576
|
+
const result = await issueRefreshTokenPg(
|
|
577
|
+
newUser.id, meta.repositoryId, meta.ipAddress, meta.userAgent,
|
|
578
|
+
);
|
|
579
|
+
refreshTokenStr = result.token;
|
|
580
|
+
} else {
|
|
581
|
+
refreshTokenStr = await signJwtPg({ sub: newUser.id });
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return authResponse(
|
|
585
|
+
{
|
|
586
|
+
token: accessToken,
|
|
587
|
+
refreshToken: refreshTokenStr,
|
|
588
|
+
user: {
|
|
589
|
+
id: newUser.id,
|
|
590
|
+
email: newUser.email,
|
|
591
|
+
username: newUser.username,
|
|
592
|
+
displayName: newUser.displayName,
|
|
593
|
+
avatarUrl: newUser.avatarUrl,
|
|
594
|
+
role: newUser.role ?? 'authenticated',
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
200,
|
|
598
|
+
options?.cookies,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Legacy path: identities + credentials tables
|
|
367
603
|
const existingResults = await db
|
|
368
604
|
.select({
|
|
369
605
|
credentialId: schema.credentials.id,
|
|
@@ -396,15 +632,21 @@ export function createPostgresAuthHandlers(
|
|
|
396
632
|
sub: existingUser.id,
|
|
397
633
|
email: existingUser.email,
|
|
398
634
|
role: existingUser.role ?? 'authenticated',
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
635
|
+
}, enrichRequest);
|
|
636
|
+
let refreshTokenStr: string;
|
|
637
|
+
if (refreshTable) {
|
|
638
|
+
const result = await issueRefreshTokenPg(
|
|
639
|
+
existingUser.id, meta.repositoryId, meta.ipAddress, meta.userAgent,
|
|
640
|
+
);
|
|
641
|
+
refreshTokenStr = result.token;
|
|
642
|
+
} else {
|
|
643
|
+
refreshTokenStr = await signJwtPg({ sub: existingUser.id });
|
|
644
|
+
}
|
|
403
645
|
|
|
404
646
|
return authResponse(
|
|
405
647
|
{
|
|
406
648
|
token: accessToken,
|
|
407
|
-
refreshToken,
|
|
649
|
+
refreshToken: refreshTokenStr,
|
|
408
650
|
user: {
|
|
409
651
|
id: existingUser.id,
|
|
410
652
|
email: existingUser.email,
|
|
@@ -419,7 +661,7 @@ export function createPostgresAuthHandlers(
|
|
|
419
661
|
);
|
|
420
662
|
}
|
|
421
663
|
|
|
422
|
-
// New user — create account + identity + OAuth credential
|
|
664
|
+
// New user — create account + identity + OAuth credential (legacy path)
|
|
423
665
|
const displayName = [userInfo.firstName, userInfo.lastName]
|
|
424
666
|
.filter(Boolean)
|
|
425
667
|
.join(' ') || userInfo.email.split('@')[0];
|
|
@@ -428,7 +670,7 @@ export function createPostgresAuthHandlers(
|
|
|
428
670
|
const { user, profile } = await db.transaction(async (tx: any) => {
|
|
429
671
|
const [u] = await tx
|
|
430
672
|
.insert(users)
|
|
431
|
-
.values({ email: userInfo.email,
|
|
673
|
+
.values(withPassword(users, { email: userInfo.email }, null))
|
|
432
674
|
.returning();
|
|
433
675
|
|
|
434
676
|
const [ident] = await tx
|
|
@@ -467,15 +709,21 @@ export function createPostgresAuthHandlers(
|
|
|
467
709
|
sub: user.id,
|
|
468
710
|
email: user.email,
|
|
469
711
|
role: user.role ?? 'authenticated',
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
712
|
+
}, enrichRequest);
|
|
713
|
+
let refreshTokenStr: string;
|
|
714
|
+
if (refreshTable) {
|
|
715
|
+
const result = await issueRefreshTokenPg(
|
|
716
|
+
user.id, meta.repositoryId, meta.ipAddress, meta.userAgent,
|
|
717
|
+
);
|
|
718
|
+
refreshTokenStr = result.token;
|
|
719
|
+
} else {
|
|
720
|
+
refreshTokenStr = await signJwtPg({ sub: user.id });
|
|
721
|
+
}
|
|
474
722
|
|
|
475
723
|
return authResponse(
|
|
476
724
|
{
|
|
477
725
|
token: accessToken,
|
|
478
|
-
refreshToken,
|
|
726
|
+
refreshToken: refreshTokenStr,
|
|
479
727
|
user: {
|
|
480
728
|
id: user.id,
|
|
481
729
|
email: user.email,
|
|
@@ -491,13 +739,16 @@ export function createPostgresAuthHandlers(
|
|
|
491
739
|
}
|
|
492
740
|
: undefined,
|
|
493
741
|
|
|
494
|
-
signout: async (
|
|
742
|
+
signout: async (request: Request): Promise<Response> => {
|
|
743
|
+
const csrfError = csrfCheck(request, options?.cookies);
|
|
744
|
+
if (csrfError) return csrfError;
|
|
745
|
+
|
|
495
746
|
return signoutResponse(options?.cookies);
|
|
496
747
|
},
|
|
497
748
|
|
|
498
749
|
revokeRefreshTokens: async (userId: string): Promise<void> => {
|
|
499
|
-
if (
|
|
500
|
-
await revokeRefreshTokensPg(userId);
|
|
750
|
+
if (refreshTable) {
|
|
751
|
+
await revokeRefreshTokensPg(userId, options?.repositoryId);
|
|
501
752
|
}
|
|
502
753
|
},
|
|
503
754
|
};
|