Commit 8ceace7
🔄 feat: Body-Based OpenID Refresh for Cross-Origin Admin Panels (#46)
* 🔄 feat: Body-Based OpenID Refresh for Cross-Origin Admin Panels
The cookie-based `/api/auth/refresh` controller can't be reached cross-origin
because the refresh-token cookie doesn't ship on a fetch from a different host.
Cross-origin admin panel sessions silently die at JWT expiry with no recovery.
This routes `openid` sessions through the new `POST /api/admin/oauth/refresh`
endpoint (danny-avila/LibreChat#13007) which accepts the refresh token in
the request body and returns the same shape as `/api/admin/oauth/exchange`:
`{ token, refreshToken, user, expiresAt }`. Non-openid sessions still use
the legacy cookie-based path.
The `expiresAt` field (ms epoch) is now threaded through `SessionData` and
`OAuthExchangeResponse` so the admin panel can drive proactive refresh
before the bearer expires.
`refreshAdminToken` takes a new `userId` argument that's forwarded as
`user_id` in the refresh request body for disambiguation when multiple
user docs share the same OpenID `sub`.
* feat: forward X-Tenant-Id and centralize bearer freshness in apiFetch
Two follow-ups for the BFF refresh path now that the LibreChat
backend scopes /api/admin/oauth/refresh by tenant.
apiFetch was sending the session bearer as-is. If a query landed
past JWT expiry it would fail before the 60s revalidation interval
kicked in. apiFetch now reads expiresAt, refreshes proactively when
the bearer is within 30s of expiry, persists any rotated refresh
token, and retries the original request exactly once on a 401.
The OpenID refresh request now forwards the deployment's X-Tenant-Id
header so the backend's preAuthTenantMiddleware can scope the user
lookup. Without this the backend would fall back to single-tenant
behavior and the multi-tenant duplicate (sub, iss) protection added
in danny-avila/LibreChat#13007 wouldn't activate.
Refresh logic moves to a shared src/server/utils/refresh.ts so the
verify path and apiFetch share one implementation. Concurrent
callers are deduped on the refresh token so two React Query
subscribers can't both consume a rotating token in the same BFF
process.
Adds 19 tests covering tenant header forwarded/omitted, dedupe,
proactive refresh inside/outside skew, rotation persistence, and
single-retry 401 behavior.
* fix: break refresh URL import cycle
* fix: include user, provider, and tenant in refresh dedupe key
Keying the in-flight refresh map on refreshToken alone meant two
concurrent calls that happen to share a token string but differ by
userId, tokenProvider, or tenant would coalesce, and the second
caller would receive the first caller's bearer and persist it into
the wrong session.
Build the dedupe key from tokenProvider, userId, the request's
X-Tenant-Id header, and the refresh token (joined by NUL). Adds
three regression tests covering each discriminator.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>1 parent 7b7de06 commit 8ceace7
7 files changed
Lines changed: 647 additions & 66 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
| 10 | + | |
9 | 11 | | |
10 | | - | |
11 | 12 | | |
12 | 13 | | |
13 | 14 | | |
| |||
146 | 147 | | |
147 | 148 | | |
148 | 149 | | |
| 150 | + | |
149 | 151 | | |
150 | 152 | | |
151 | 153 | | |
152 | 154 | | |
153 | 155 | | |
154 | | - | |
155 | | - | |
156 | | - | |
157 | | - | |
158 | | - | |
159 | | - | |
160 | | - | |
161 | | - | |
162 | | - | |
163 | | - | |
164 | | - | |
165 | | - | |
166 | | - | |
167 | | - | |
168 | | - | |
169 | | - | |
170 | | - | |
171 | | - | |
172 | | - | |
173 | | - | |
174 | | - | |
175 | | - | |
176 | | - | |
177 | | - | |
178 | | - | |
179 | | - | |
180 | | - | |
181 | | - | |
182 | | - | |
183 | | - | |
184 | | - | |
185 | | - | |
186 | | - | |
187 | | - | |
188 | | - | |
189 | | - | |
190 | | - | |
191 | | - | |
192 | | - | |
193 | 156 | | |
194 | 157 | | |
195 | 158 | | |
| |||
227 | 190 | | |
228 | 191 | | |
229 | 192 | | |
230 | | - | |
| 193 | + | |
231 | 194 | | |
232 | 195 | | |
233 | 196 | | |
234 | 197 | | |
| 198 | + | |
235 | 199 | | |
236 | 200 | | |
237 | 201 | | |
| |||
422 | 386 | | |
423 | 387 | | |
424 | 388 | | |
| 389 | + | |
425 | 390 | | |
426 | 391 | | |
427 | 392 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
| 2 | + | |
2 | 3 | | |
3 | | - | |
4 | | - | |
5 | | - | |
6 | | - | |
7 | | - | |
8 | | - | |
9 | | - | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | | - | |
15 | | - | |
16 | | - | |
17 | | - | |
18 | | - | |
19 | | - | |
| 4 | + | |
| 5 | + | |
20 | 6 | | |
21 | 7 | | |
22 | 8 | | |
23 | | - | |
24 | 9 | | |
25 | | - | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
26 | 18 | | |
27 | 19 | | |
28 | | - | |
29 | | - | |
30 | | - | |
| 20 | + | |
| 21 | + | |
31 | 22 | | |
32 | 23 | | |
33 | 24 | | |
34 | 25 | | |
35 | | - | |
| 26 | + | |
36 | 27 | | |
37 | 28 | | |
38 | 29 | | |
39 | | - | |
40 | 30 | | |
| 31 | + | |
41 | 32 | | |
42 | 33 | | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
43 | 45 | | |
44 | 46 | | |
45 | 47 | | |
| |||
0 commit comments