From 6eb26474bf8ef522dd2ce4f1bfd4dd3d1c613514 Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Fri, 26 Jun 2026 20:05:27 +0800 Subject: [PATCH] fix(auth): resolve the OIDC-gate session hook-order-independently (D5.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The oidcAuthorizeGate before-hook resolved the subject only via getSessionFromCtx, which can return null in a global before-hook for a bearer (or non-default-cookie) request — the bearer plugin may convert Authorization -> session AFTER this hook runs, leaving gateUserId undefined so the gate was skipped (fail-open). Add an explicit token resolution (bearer, or the session cookie's token part) + a sys_session lookup, independent of plugin hook order. Co-Authored-By: Claude Opus 4.8 --- .../plugins/plugin-auth/src/auth-manager.ts | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/plugins/plugin-auth/src/auth-manager.ts b/packages/plugins/plugin-auth/src/auth-manager.ts index 651ac802bf..e3f2658b11 100644 --- a/packages/plugins/plugin-auth/src/auth-manager.ts +++ b/packages/plugins/plugin-auth/src/auth-manager.ts @@ -527,11 +527,40 @@ export class AuthManager { const clientId = ctx?.query?.client_id; if (clientId) { let gateUserId: string | undefined; + // (a) standard resolver — handles the cookie session. try { const { getSessionFromCtx } = await import('better-auth/api'); const s: any = await getSessionFromCtx(ctx as any); gateUserId = s?.user?.id ?? s?.session?.userId; - } catch { /* no session → OP redirects to login */ } + } catch { /* fall through to explicit resolution */ } + // (b) explicit token resolution — hook-order-independent. The + // bearer plugin may convert `Authorization: Bearer` to a session + // AFTER this global before-hook, so getSessionFromCtx can miss a + // bearer (or non-default cookie) request here. Resolve the token + // (bearer or the session cookie's token part) and look it up. + if (!gateUserId) { + try { + const hdr = (k: string): string => + ((ctx?.headers?.get?.(k) ?? ctx?.request?.headers?.get?.(k)) as string) || ''; + let token: string | undefined; + const bm = /^Bearer\s+(.+)$/i.exec(hdr('authorization')); + if (bm?.[1]) token = bm[1].trim(); + if (!token) { + const cm = /(?:^|;\s*)(?:__Secure-|__Host-)?better-auth\.session_token=([^;]+)/.exec(hdr('cookie')); + if (cm?.[1]) token = decodeURIComponent(cm[1]).split('.')[0]; + } + if (token) { + const sess: any = await (ctx as any).context.adapter.findOne({ + model: 'session', + where: [{ field: 'token', value: token }], + }); + const exp = sess?.expiresAt ?? sess?.expires_at; + if (sess && (!exp || new Date(exp).getTime() > Date.now())) { + gateUserId = String(sess.userId ?? sess.user_id ?? '') || undefined; + } + } + } catch { /* unresolved → fall through, OP handles auth */ } + } if (gateUserId) { const allowed = await this.config.oidcAuthorizeGate({ userId: gateUserId,