@fluixi/oauth2 1.0.0-alpha.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/LICENSE +21 -0
- package/dist/api-guard.cjs +185 -0
- package/dist/api-guard.d.ts +94 -0
- package/dist/api-guard.d.ts.map +1 -0
- package/dist/api-guard.js +127 -0
- package/dist/api-guard.mjs +141 -0
- package/dist/browser-FDUOEUM2.mjs +3544 -0
- package/dist/browser-ONMHX3Z3.mjs +3545 -0
- package/dist/browser.cjs +735 -0
- package/dist/browser.d.ts +3 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +448 -0
- package/dist/browser.mjs +704 -0
- package/dist/callback.cjs +125 -0
- package/dist/callback.d.ts +113 -0
- package/dist/callback.d.ts.map +1 -0
- package/dist/callback.js +183 -0
- package/dist/callback.mjs +106 -0
- package/dist/chunk-25WE6UWI.mjs +10 -0
- package/dist/chunk-4NSOGPLM.mjs +15 -0
- package/dist/chunk-7P6ASYW6.mjs +9 -0
- package/dist/chunk-OGSFNTKA.mjs +13 -0
- package/dist/client-auth.cjs +115 -0
- package/dist/client-auth.d.ts +43 -0
- package/dist/client-auth.d.ts.map +1 -0
- package/dist/client-auth.js +112 -0
- package/dist/client-auth.mjs +84 -0
- package/dist/discovery.cjs +86 -0
- package/dist/discovery.d.ts +15 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +51 -0
- package/dist/discovery.mjs +63 -0
- package/dist/dpop-LVPWWADA.mjs +80 -0
- package/dist/dpop-NS7NKOWM.mjs +79 -0
- package/dist/dpop-store.cjs +152 -0
- package/dist/dpop-store.d.ts +37 -0
- package/dist/dpop-store.d.ts.map +1 -0
- package/dist/dpop-store.js +119 -0
- package/dist/dpop-store.mjs +121 -0
- package/dist/dpop.cjs +117 -0
- package/dist/dpop.d.ts +72 -0
- package/dist/dpop.d.ts.map +1 -0
- package/dist/dpop.js +114 -0
- package/dist/dpop.mjs +86 -0
- package/dist/env-config.cjs +366 -0
- package/dist/env-config.d.ts +103 -0
- package/dist/env-config.d.ts.map +1 -0
- package/dist/env-config.js +184 -0
- package/dist/env-config.mjs +343 -0
- package/dist/env-credentials.cjs +82 -0
- package/dist/env-credentials.d.ts +68 -0
- package/dist/env-credentials.d.ts.map +1 -0
- package/dist/env-credentials.js +76 -0
- package/dist/env-credentials.mjs +51 -0
- package/dist/index.cjs +940 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +101 -0
- package/dist/index.mjs +909 -0
- package/dist/pkce.cjs +87 -0
- package/dist/pkce.d.ts +58 -0
- package/dist/pkce.d.ts.map +1 -0
- package/dist/pkce.js +112 -0
- package/dist/pkce.mjs +66 -0
- package/dist/presets.cjs +240 -0
- package/dist/presets.d.ts +140 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +182 -0
- package/dist/presets.mjs +217 -0
- package/dist/return-to.cjs +46 -0
- package/dist/return-to.d.ts +49 -0
- package/dist/return-to.d.ts.map +1 -0
- package/dist/return-to.js +71 -0
- package/dist/return-to.mjs +25 -0
- package/dist/roles.cjs +56 -0
- package/dist/roles.d.ts +35 -0
- package/dist/roles.d.ts.map +1 -0
- package/dist/roles.js +37 -0
- package/dist/roles.mjs +35 -0
- package/dist/server-client.cjs +134 -0
- package/dist/server-client.d.ts +12 -0
- package/dist/server-client.d.ts.map +1 -0
- package/dist/server-client.js +107 -0
- package/dist/server-client.mjs +111 -0
- package/dist/server.cjs +470 -0
- package/dist/server.d.ts +89 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +360 -0
- package/dist/server.mjs +441 -0
- package/dist/service-client.cjs +244 -0
- package/dist/service-client.d.ts +89 -0
- package/dist/service-client.d.ts.map +1 -0
- package/dist/service-client.js +112 -0
- package/dist/service-client.mjs +213 -0
- package/dist/service.cjs +903 -0
- package/dist/service.d.ts +96 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +129 -0
- package/dist/service.mjs +874 -0
- package/dist/tokens.cjs +29 -0
- package/dist/tokens.d.ts +13 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +21 -0
- package/dist/tokens.mjs +8 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/dist/types.cjs +33 -0
- package/dist/types.d.ts +239 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +23 -0
- package/dist/types.mjs +12 -0
- package/dist/verify.cjs +724 -0
- package/dist/verify.d.ts +205 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +371 -0
- package/dist/verify.mjs +590 -0
- package/package.json +121 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@fluixi/oauth2/server`: the routes behind `tokens: 'server'`.
|
|
3
|
+
*
|
|
4
|
+
* The browser never sees a token here. The authorization code is exchanged on this side,
|
|
5
|
+
* the tokens are sealed into an httpOnly cookie, and the page gets a cookie it cannot
|
|
6
|
+
* read. That is the difference the mode exists for: a script on the page has nothing to
|
|
7
|
+
* steal.
|
|
8
|
+
*
|
|
9
|
+
* Mount it as middleware in a @fluixi/start app:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* // src/middleware.ts
|
|
13
|
+
* import { defineMiddleware } from '@fluixi/start';
|
|
14
|
+
* import { oauthRoutes } from '@fluixi/oauth2/server';
|
|
15
|
+
*
|
|
16
|
+
* const auth = oauthRoutes({
|
|
17
|
+
* issuer: process.env.OIDC_ISSUER!,
|
|
18
|
+
* clientId: process.env.OIDC_CLIENT_ID!,
|
|
19
|
+
* clientSecret: process.env.OIDC_CLIENT_SECRET!,
|
|
20
|
+
* redirectUri: 'http://localhost:3000/auth/callback',
|
|
21
|
+
* cookieSecret: process.env.SESSION_SECRET!,
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* export default defineMiddleware([auth, …]);
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* The cookie itself is `@fluixi/session/server`, which knows nothing about OAuth: it
|
|
28
|
+
* seals a value and reads it back. What this file adds is the flow that decides what
|
|
29
|
+
* goes in it.
|
|
30
|
+
*/
|
|
31
|
+
import { createSessionCookie } from '@fluixi/session/server';
|
|
32
|
+
import { safeReturnTo } from './return-to.js';
|
|
33
|
+
import { applyClientAuth, } from './client-auth.js';
|
|
34
|
+
import { discover } from './discovery.js';
|
|
35
|
+
import { createPkcePair, createStateValue, expiryFrom, timingSafeEqual } from './pkce.js';
|
|
36
|
+
import { OAuthError } from './types.js';
|
|
37
|
+
/**
|
|
38
|
+
* The middleware.
|
|
39
|
+
*
|
|
40
|
+
* Handles `<basePath>/login`, `/callback`, `/refresh`, `/me` and `/logout`, and passes
|
|
41
|
+
* anything else along untouched.
|
|
42
|
+
*/
|
|
43
|
+
export function oauthRoutes(config) {
|
|
44
|
+
const base = (config.basePath ?? '/auth').replace(/\/$/, '');
|
|
45
|
+
const scopes = config.scopes ?? ['openid', 'profile', 'email'];
|
|
46
|
+
const cookie = createSessionCookie({
|
|
47
|
+
secret: config.cookieSecret,
|
|
48
|
+
name: config.cookieName,
|
|
49
|
+
secure: config.secureCookie,
|
|
50
|
+
});
|
|
51
|
+
let endpoints = config.endpoints ?? null;
|
|
52
|
+
const resolve = async () => {
|
|
53
|
+
if (endpoints)
|
|
54
|
+
return endpoints;
|
|
55
|
+
if (!config.issuer) {
|
|
56
|
+
throw new OAuthError('Give oauthRoutes an issuer or endpoints.', 'missing_issuer');
|
|
57
|
+
}
|
|
58
|
+
endpoints = await discover(config.issuer);
|
|
59
|
+
return endpoints;
|
|
60
|
+
};
|
|
61
|
+
const json = (body, status = 200, headers = {}) => new Response(JSON.stringify(body), {
|
|
62
|
+
status,
|
|
63
|
+
headers: { 'content-type': 'application/json', ...headers },
|
|
64
|
+
});
|
|
65
|
+
const clientAuth = {
|
|
66
|
+
clientId: config.clientId,
|
|
67
|
+
// `client_secret_post` by default here, which is what this half has always sent, so
|
|
68
|
+
// an existing deployment keeps working.
|
|
69
|
+
method: config.tokenAuthMethod ?? (config.clientSecret ? 'body' : 'none'),
|
|
70
|
+
clientSecret: config.clientSecret,
|
|
71
|
+
privateKey: config.privateKey,
|
|
72
|
+
keyId: config.keyId,
|
|
73
|
+
assertionAlgorithm: config.assertionAlgorithm,
|
|
74
|
+
};
|
|
75
|
+
async function tokenRequest(body) {
|
|
76
|
+
const { token } = await resolve();
|
|
77
|
+
const headers = {
|
|
78
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
79
|
+
};
|
|
80
|
+
const form = new URLSearchParams(body);
|
|
81
|
+
await applyClientAuth(clientAuth, token, headers, form);
|
|
82
|
+
const response = await fetch(token, { method: 'POST', headers, body: form.toString() });
|
|
83
|
+
const payload = (await response.json().catch(() => ({})));
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new OAuthError(String(payload.error ?? 'token_request_failed'), String(payload.error ?? 'token_request_failed'));
|
|
86
|
+
}
|
|
87
|
+
return payload;
|
|
88
|
+
}
|
|
89
|
+
return async function middleware(request, next) {
|
|
90
|
+
const url = new URL(request.url);
|
|
91
|
+
if (!url.pathname.startsWith(`${base}/`))
|
|
92
|
+
return next();
|
|
93
|
+
const route = url.pathname.slice(base.length);
|
|
94
|
+
const session = async () => {
|
|
95
|
+
return cookie.read(request);
|
|
96
|
+
};
|
|
97
|
+
if (route === '/login') {
|
|
98
|
+
const { authorization } = await resolve();
|
|
99
|
+
const { verifier, challenge } = await createPkcePair();
|
|
100
|
+
const state = createStateValue();
|
|
101
|
+
const target = new URL(authorization);
|
|
102
|
+
target.searchParams.set('response_type', 'code');
|
|
103
|
+
target.searchParams.set('client_id', config.clientId);
|
|
104
|
+
target.searchParams.set('redirect_uri', config.redirectUri);
|
|
105
|
+
target.searchParams.set('scope', scopes.join(' '));
|
|
106
|
+
target.searchParams.set('state', state);
|
|
107
|
+
target.searchParams.set('code_challenge', challenge);
|
|
108
|
+
target.searchParams.set('code_challenge_method', 'S256');
|
|
109
|
+
// The verifier goes in the cookie, which is the only thing that survives the trip
|
|
110
|
+
// and is not readable by the page.
|
|
111
|
+
const pendingCookie = await cookie.write({
|
|
112
|
+
accessToken: '',
|
|
113
|
+
pending: {
|
|
114
|
+
verifier,
|
|
115
|
+
state,
|
|
116
|
+
// Checked here rather than only at the callback, so a refused destination
|
|
117
|
+
// never reaches the cookie and cannot be read back out of it later.
|
|
118
|
+
returnTo: safeReturnTo(url.searchParams.get('returnTo'), {
|
|
119
|
+
fallback: '',
|
|
120
|
+
origin: url.origin,
|
|
121
|
+
allowedOrigins: config.allowedReturnOrigins,
|
|
122
|
+
}) || undefined,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
// Ten minutes: long enough to sign in, short enough that an abandoned attempt
|
|
126
|
+
// does not sit in the browser.
|
|
127
|
+
{ maxAge: 600 });
|
|
128
|
+
return new Response(null, {
|
|
129
|
+
status: 302,
|
|
130
|
+
headers: {
|
|
131
|
+
location: target.toString(),
|
|
132
|
+
'set-cookie': pendingCookie,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (route === '/callback') {
|
|
137
|
+
const current = await session();
|
|
138
|
+
const pending = current?.pending;
|
|
139
|
+
const error = url.searchParams.get('error');
|
|
140
|
+
if (error)
|
|
141
|
+
return json({ error }, 400);
|
|
142
|
+
const code = url.searchParams.get('code');
|
|
143
|
+
const state = url.searchParams.get('state');
|
|
144
|
+
if (!code)
|
|
145
|
+
return json({ error: 'no_code' }, 400);
|
|
146
|
+
if (!pending || !state || !timingSafeEqual(pending.state, state)) {
|
|
147
|
+
return json({ error: 'state_mismatch' }, 400);
|
|
148
|
+
}
|
|
149
|
+
const payload = await tokenRequest({
|
|
150
|
+
grant_type: 'authorization_code',
|
|
151
|
+
code,
|
|
152
|
+
redirect_uri: config.redirectUri,
|
|
153
|
+
code_verifier: pending.verifier,
|
|
154
|
+
});
|
|
155
|
+
const signedIn = await cookie.write({
|
|
156
|
+
accessToken: String(payload.access_token ?? ''),
|
|
157
|
+
refreshToken: typeof payload.refresh_token === 'string' ? payload.refresh_token : undefined,
|
|
158
|
+
idToken: typeof payload.id_token === 'string' ? payload.id_token : undefined,
|
|
159
|
+
expiresAt: expiryFrom(payload.expires_in),
|
|
160
|
+
});
|
|
161
|
+
return new Response(null, {
|
|
162
|
+
status: 302,
|
|
163
|
+
headers: {
|
|
164
|
+
// Checked again on the way out. The cookie cannot be forged, but a value that
|
|
165
|
+
// was acceptable when the flow started should still be one when it finishes,
|
|
166
|
+
// and this is the line that actually performs the redirect.
|
|
167
|
+
location: safeReturnTo(pending.returnTo ?? config.defaultReturnTo, {
|
|
168
|
+
fallback: '/',
|
|
169
|
+
origin: url.origin,
|
|
170
|
+
allowedOrigins: config.allowedReturnOrigins,
|
|
171
|
+
}),
|
|
172
|
+
'set-cookie': signedIn,
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (route === '/refresh') {
|
|
177
|
+
const current = await session();
|
|
178
|
+
if (!current?.refreshToken)
|
|
179
|
+
return json({ error: 'no_session' }, 401);
|
|
180
|
+
const payload = await tokenRequest({
|
|
181
|
+
grant_type: 'refresh_token',
|
|
182
|
+
refresh_token: current.refreshToken,
|
|
183
|
+
});
|
|
184
|
+
const refreshed = await cookie.write({
|
|
185
|
+
accessToken: String(payload.access_token ?? ''),
|
|
186
|
+
// A provider that rotates refresh tokens sends a new one; one that does not
|
|
187
|
+
// expects the old one to keep working.
|
|
188
|
+
refreshToken: typeof payload.refresh_token === 'string'
|
|
189
|
+
? payload.refresh_token
|
|
190
|
+
: current.refreshToken,
|
|
191
|
+
idToken: current.idToken,
|
|
192
|
+
expiresAt: expiryFrom(payload.expires_in),
|
|
193
|
+
});
|
|
194
|
+
return json({ ok: true }, 200, { 'set-cookie': refreshed });
|
|
195
|
+
}
|
|
196
|
+
if (route === '/me') {
|
|
197
|
+
const current = await session();
|
|
198
|
+
if (!current?.accessToken)
|
|
199
|
+
return json({ error: 'no_session' }, 401);
|
|
200
|
+
const { userinfo } = await resolve();
|
|
201
|
+
if (!userinfo)
|
|
202
|
+
return json({ error: 'no_userinfo' }, 501);
|
|
203
|
+
const response = await fetch(userinfo, {
|
|
204
|
+
headers: { authorization: `Bearer ${current.accessToken}` },
|
|
205
|
+
});
|
|
206
|
+
if (!response.ok)
|
|
207
|
+
return json({ error: 'userinfo_failed' }, response.status);
|
|
208
|
+
const claims = (await response.json());
|
|
209
|
+
return json(config.mapUser ? config.mapUser(claims, current.accessToken) : claims);
|
|
210
|
+
}
|
|
211
|
+
if (route === '/revoke') {
|
|
212
|
+
const current = await session();
|
|
213
|
+
const hint = url.searchParams.get('hint') === 'access_token' ? 'access_token' : 'refresh_token';
|
|
214
|
+
const target = hint === 'access_token' ? current?.accessToken : current?.refreshToken;
|
|
215
|
+
if (!target)
|
|
216
|
+
return json({ error: 'no_session' }, 401);
|
|
217
|
+
const { revocation } = await resolve().catch(() => ({ revocation: undefined }));
|
|
218
|
+
// A provider with no revocation endpoint cannot do this, and saying so beats a
|
|
219
|
+
// silent success that leaves the token live.
|
|
220
|
+
if (!revocation)
|
|
221
|
+
return json({ error: 'no_revocation_endpoint' }, 501);
|
|
222
|
+
const headers = {
|
|
223
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
224
|
+
};
|
|
225
|
+
const form = new URLSearchParams({ token: target, token_type_hint: hint });
|
|
226
|
+
await applyClientAuth(clientAuth, revocation, headers, form);
|
|
227
|
+
// RFC 7009 answers 200 whether or not the provider knew the token, so there is
|
|
228
|
+
// nothing to branch on.
|
|
229
|
+
await fetch(revocation, { method: 'POST', headers, body: form.toString() }).catch(() => undefined);
|
|
230
|
+
return json({ ok: true, revoked: hint });
|
|
231
|
+
}
|
|
232
|
+
if (route === '/logout') {
|
|
233
|
+
const current = await session();
|
|
234
|
+
const { endSession } = await resolve().catch(() => ({ endSession: undefined }));
|
|
235
|
+
// Retire the refresh token before dropping the cookie, or a copy stays usable for
|
|
236
|
+
// its whole lifetime while the browser believes it signed out.
|
|
237
|
+
const { revocation } = await resolve().catch(() => ({ revocation: undefined }));
|
|
238
|
+
if (revocation && current?.refreshToken) {
|
|
239
|
+
const headers = {
|
|
240
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
241
|
+
};
|
|
242
|
+
const form = new URLSearchParams({
|
|
243
|
+
token: current.refreshToken,
|
|
244
|
+
token_type_hint: 'refresh_token',
|
|
245
|
+
});
|
|
246
|
+
await applyClientAuth(clientAuth, revocation, headers, form);
|
|
247
|
+
await fetch(revocation, { method: 'POST', headers, body: form.toString() }).catch(() => undefined);
|
|
248
|
+
}
|
|
249
|
+
// Max-Age=0 is what removes it. Clearing the value alone leaves a cookie behind.
|
|
250
|
+
const cleared = cookie.clear();
|
|
251
|
+
if (endSession && current?.idToken) {
|
|
252
|
+
const target = new URL(endSession);
|
|
253
|
+
target.searchParams.set('id_token_hint', current.idToken);
|
|
254
|
+
if (config.defaultReturnTo) {
|
|
255
|
+
target.searchParams.set('post_logout_redirect_uri', config.defaultReturnTo);
|
|
256
|
+
}
|
|
257
|
+
return new Response(null, {
|
|
258
|
+
status: 302,
|
|
259
|
+
headers: { location: target.toString(), 'set-cookie': cleared },
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return json({ ok: true }, 200, { 'set-cookie': cleared });
|
|
263
|
+
}
|
|
264
|
+
return next();
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Put the signed-in user on the request, so a server render knows who is looking.
|
|
269
|
+
*
|
|
270
|
+
* This is the piece that makes `tokens: 'server'` able to render authenticated pages. The
|
|
271
|
+
* cookie is the credential and the server can read it, so the user is resolved before
|
|
272
|
+
* the render and written where `@fluixi/auth`'s hooks already look: request locals,
|
|
273
|
+
* under the key they read. `useUser()` then answers during SSR instead of returning null
|
|
274
|
+
* and filling in after hydration.
|
|
275
|
+
*
|
|
276
|
+
* Dependency injection does not solve this and is not meant to. DI decides which
|
|
277
|
+
* instance a component gets; locals carry what the middleware learned about this
|
|
278
|
+
* request. They are different questions, and the user is the second one.
|
|
279
|
+
*
|
|
280
|
+
* There is no browser-mode equivalent, and that is not an omission. In that mode the
|
|
281
|
+
* token lives in the browser and is never sent on a document request, so the server has
|
|
282
|
+
* no credential to resolve and cannot know who is signed in. An app built that way
|
|
283
|
+
* renders the signed-out shell and fills it in on the client.
|
|
284
|
+
*
|
|
285
|
+
* ```ts
|
|
286
|
+
* export default defineMiddleware([auth, sessionLocals(config), ...]);
|
|
287
|
+
* ```
|
|
288
|
+
*
|
|
289
|
+
* Mount it after `oauthRoutes`, so a request to `/auth/login` is answered rather than
|
|
290
|
+
* having its session resolved first.
|
|
291
|
+
*/
|
|
292
|
+
export function sessionLocals(config) {
|
|
293
|
+
const cookie = createSessionCookie({
|
|
294
|
+
secret: config.cookieSecret,
|
|
295
|
+
name: config.cookieName,
|
|
296
|
+
secure: config.secureCookie,
|
|
297
|
+
});
|
|
298
|
+
const ttl = config.userCacheMs ?? 30_000;
|
|
299
|
+
/**
|
|
300
|
+
* Keyed by the access token, which this process already holds in the cookie it just
|
|
301
|
+
* opened, so nothing new is exposed by keeping it here.
|
|
302
|
+
*
|
|
303
|
+
* Deliberately not a verifier. The provider still decides who this is; the cache only
|
|
304
|
+
* says how recently it was asked.
|
|
305
|
+
*/
|
|
306
|
+
const cache = new Map();
|
|
307
|
+
/** Bounded, so a long-running server does not hold every token it has ever seen. */
|
|
308
|
+
const remember = (token, user) => {
|
|
309
|
+
if (ttl <= 0)
|
|
310
|
+
return;
|
|
311
|
+
if (cache.size > 500) {
|
|
312
|
+
const now = Date.now();
|
|
313
|
+
for (const [key, entry] of cache)
|
|
314
|
+
if (entry.until <= now)
|
|
315
|
+
cache.delete(key);
|
|
316
|
+
// Still full of live entries: drop the oldest rather than grow without limit.
|
|
317
|
+
if (cache.size > 500)
|
|
318
|
+
cache.delete(cache.keys().next().value);
|
|
319
|
+
}
|
|
320
|
+
cache.set(token, { user, until: Date.now() + ttl });
|
|
321
|
+
};
|
|
322
|
+
return async function middleware(request, next) {
|
|
323
|
+
const { getRequestLocals } = await import('@fluixi/server');
|
|
324
|
+
const locals = getRequestLocals(request);
|
|
325
|
+
const session = await cookie.read(request);
|
|
326
|
+
// Written either way. A route that reads locals should find "nobody" rather than
|
|
327
|
+
// nothing, so the absent case and the signed-out case look the same to it.
|
|
328
|
+
if (!session?.accessToken) {
|
|
329
|
+
locals.auth = { user: null, session: null };
|
|
330
|
+
return next();
|
|
331
|
+
}
|
|
332
|
+
const cached = cache.get(session.accessToken);
|
|
333
|
+
if (cached && cached.until > Date.now()) {
|
|
334
|
+
locals.auth = { user: cached.user, session: { expiresAt: session.expiresAt } };
|
|
335
|
+
return next();
|
|
336
|
+
}
|
|
337
|
+
let user = null;
|
|
338
|
+
try {
|
|
339
|
+
const endpoints = config.endpoints ?? (config.issuer ? await discover(config.issuer) : null);
|
|
340
|
+
if (endpoints?.userinfo) {
|
|
341
|
+
const response = await fetch(endpoints.userinfo, {
|
|
342
|
+
headers: { authorization: `Bearer ${session.accessToken}` },
|
|
343
|
+
});
|
|
344
|
+
if (response.ok) {
|
|
345
|
+
const claims = (await response.json());
|
|
346
|
+
user = config.mapUser ? config.mapUser(claims, session.accessToken) : claims;
|
|
347
|
+
// Only a successful answer. Caching a refusal would keep someone signed out
|
|
348
|
+
// after the provider came back.
|
|
349
|
+
remember(session.accessToken, user);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
// A provider that is unreachable should not take the page down with it. The render
|
|
355
|
+
// proceeds as signed out, and the client retries on its own.
|
|
356
|
+
}
|
|
357
|
+
locals.auth = { user, session: user ? { expiresAt: session.expiresAt } : null };
|
|
358
|
+
return next();
|
|
359
|
+
};
|
|
360
|
+
}
|