Skip to content

Commit 9acea30

Browse files
authored
Merge pull request #142 from bio-xyz/dev
x-user-id auth
2 parents 4923ade + 2ef2708 commit 9acea30

1 file changed

Lines changed: 53 additions & 48 deletions

File tree

src/middleware/authResolver.ts

Lines changed: 53 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,11 @@
1616
* 4. Anonymous (if mode=none or auth not required)
1717
*/
1818

19-
import type {
20-
AuthContext,
21-
AuthMode,
22-
AuthResolverOptions,
23-
} from "../types/auth";
19+
import { extractBearerToken, verifyJWT } from "../services/jwt";
20+
import type { AuthContext, AuthResolverOptions } from "../types/auth";
2421
import { getAuthConfig } from "../types/auth";
25-
import { verifyJWT, extractBearerToken } from "../services/jwt";
26-
import { generateUUID } from "../utils/uuid";
27-
import { walletAddressToUUID } from "../utils/uuid";
2822
import logger from "../utils/logger";
23+
import { generateUUID, walletAddressToUUID } from "../utils/uuid";
2924

3025
/**
3126
* Constant-time string comparison to prevent timing attacks
@@ -43,6 +38,36 @@ function constantTimeCompare(a: string, b: string): boolean {
4338
return result === 0;
4439
}
4540

41+
/**
42+
* Resolve a caller-provided userId from the X-User-Id header, request body,
43+
* or fall back to generating a new UUID.
44+
*
45+
* Priority: X-User-Id header > body.userId > generated UUID
46+
*
47+
* This is only used when the caller is already trusted (valid API key or
48+
* AUTH_MODE=none), so accepting the provided userId is safe.
49+
*/
50+
function resolveProvidedUserId(request: Request, body?: any): string {
51+
// 1. Check X-User-Id header (useful for GET requests that have no body)
52+
const headerUserId = request.headers.get("X-User-Id");
53+
if (
54+
headerUserId &&
55+
typeof headerUserId === "string" &&
56+
headerUserId.length > 0
57+
) {
58+
return headerUserId;
59+
}
60+
61+
// 2. Check body.userId
62+
const bodyUserId = body?.userId;
63+
if (bodyUserId && typeof bodyUserId === "string" && bodyUserId.length > 0) {
64+
return bodyUserId;
65+
}
66+
67+
// 3. Generate a new UUID
68+
return generateUUID();
69+
}
70+
4671
/**
4772
* Check if request has valid API key (legacy support)
4873
*/
@@ -115,7 +140,7 @@ export function authResolver(options: AuthResolverOptions = {}) {
115140
required,
116141
hasX402Settlement: !!(request as any).x402Settlement,
117142
},
118-
"auth_resolver_start"
143+
"auth_resolver_start",
119144
);
120145

121146
// =====================================================
@@ -139,7 +164,7 @@ export function authResolver(options: AuthResolverOptions = {}) {
139164
wallet: x402Settlement.payer,
140165
method: "x402",
141166
},
142-
"auth_resolved_x402"
167+
"auth_resolved_x402",
143168
);
144169
}
145170

@@ -154,7 +179,8 @@ export function authResolver(options: AuthResolverOptions = {}) {
154179
const token = extractBearerToken(authHeader);
155180

156181
// Only verify if it looks like a JWT (has dots) or if we're in JWT mode
157-
const shouldVerifyJwt = token && (config.mode === "jwt" || token.includes("."));
182+
const shouldVerifyJwt =
183+
token && (config.mode === "jwt" || token.includes("."));
158184

159185
if (shouldVerifyJwt && token) {
160186
const result = await verifyJWT(token);
@@ -176,7 +202,7 @@ export function authResolver(options: AuthResolverOptions = {}) {
176202
hasEmail: !!auth.email,
177203
hasOrgId: !!auth.orgId,
178204
},
179-
"auth_resolved_jwt"
205+
"auth_resolved_jwt",
180206
);
181207
} else if (config.mode === "jwt") {
182208
// JWT provided but invalid - only reject in strict JWT mode
@@ -185,7 +211,7 @@ export function authResolver(options: AuthResolverOptions = {}) {
185211
error: result.error,
186212
path,
187213
},
188-
"auth_jwt_invalid"
214+
"auth_jwt_invalid",
189215
);
190216

191217
// In JWT mode, invalid JWT = reject (don't fall through)
@@ -207,14 +233,8 @@ export function authResolver(options: AuthResolverOptions = {}) {
207233
// =====================================================
208234
// Only used if JWT mode is not active or no JWT provided
209235
if (!auth && isValidApiKey(request)) {
210-
// API key is valid - trust the caller's userId from body
211-
const providedUserId = body?.userId;
212-
const userId =
213-
providedUserId &&
214-
typeof providedUserId === "string" &&
215-
providedUserId.length > 0
216-
? providedUserId
217-
: generateUUID();
236+
// API key is valid - trust the caller's userId from header/body
237+
const userId = resolveProvidedUserId(request, body);
218238

219239
auth = {
220240
userId,
@@ -226,23 +246,16 @@ export function authResolver(options: AuthResolverOptions = {}) {
226246
{
227247
userId,
228248
method: "api_key",
229-
userIdSource: providedUserId ? "body" : "generated",
230249
},
231-
"auth_resolved_api_key"
250+
"auth_resolved_api_key",
232251
);
233252
}
234253

235254
// =====================================================
236255
// Priority 4: None Mode (Development)
237256
// =====================================================
238257
if (!auth && config.mode === "none") {
239-
const providedUserId = body?.userId;
240-
const userId =
241-
providedUserId &&
242-
typeof providedUserId === "string" &&
243-
providedUserId.length > 0
244-
? providedUserId
245-
: generateUUID();
258+
const userId = resolveProvidedUserId(request, body);
246259

247260
auth = {
248261
userId,
@@ -254,9 +267,8 @@ export function authResolver(options: AuthResolverOptions = {}) {
254267
{
255268
userId,
256269
method: "anonymous",
257-
userIdSource: providedUserId ? "body" : "generated",
258270
},
259-
"auth_resolved_anonymous"
271+
"auth_resolved_anonymous",
260272
);
261273
}
262274

@@ -270,7 +282,7 @@ export function authResolver(options: AuthResolverOptions = {}) {
270282
path,
271283
mode: config.mode,
272284
},
273-
"auth_required_but_missing"
285+
"auth_required_but_missing",
274286
);
275287

276288
set.status = 401;
@@ -302,7 +314,7 @@ export function authResolver(options: AuthResolverOptions = {}) {
302314
userId: auth.userId,
303315
method: "anonymous",
304316
},
305-
"auth_resolved_anonymous_fallback"
317+
"auth_resolved_anonymous_fallback",
306318
);
307319
}
308320

@@ -316,7 +328,7 @@ export function authResolver(options: AuthResolverOptions = {}) {
316328
verified: auth.verified,
317329
path,
318330
},
319-
"auth_resolver_complete"
331+
"auth_resolver_complete",
320332
);
321333
};
322334
}
@@ -341,7 +353,10 @@ export function authBeforeHandle(options: { optional?: boolean } = {}) {
341353
* @param body - Optional parsed request body (for userId extraction in dev mode)
342354
* @returns AuthContext with userId if authenticated
343355
*/
344-
export async function resolveAuth(request: Request, body?: any): Promise<{
356+
export async function resolveAuth(
357+
request: Request,
358+
body?: any,
359+
): Promise<{
345360
authenticated: boolean;
346361
userId?: string;
347362
method?: string;
@@ -373,21 +388,11 @@ export async function resolveAuth(request: Request, body?: any): Promise<{
373388
}
374389
}
375390

376-
// Helper to get userId from body (same logic as authResolver middleware)
377-
const getUserIdFromBody = () => {
378-
const providedUserId = body?.userId;
379-
return providedUserId &&
380-
typeof providedUserId === "string" &&
381-
providedUserId.length > 0
382-
? providedUserId
383-
: generateUUID();
384-
};
385-
386391
// Check API key (legacy)
387392
if (isValidApiKey(request)) {
388393
return {
389394
authenticated: true,
390-
userId: getUserIdFromBody(),
395+
userId: resolveProvidedUserId(request, body),
391396
method: "api_key",
392397
};
393398
}
@@ -396,7 +401,7 @@ export async function resolveAuth(request: Request, body?: any): Promise<{
396401
if (config.mode === "none") {
397402
return {
398403
authenticated: true,
399-
userId: getUserIdFromBody(),
404+
userId: resolveProvidedUserId(request, body),
400405
method: "anonymous",
401406
};
402407
}

0 commit comments

Comments
 (0)