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,