@jcoder-stack/abp-react 0.1.0
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/README.md +86 -0
- package/dist/application-configuration-DhOZRqtz.d.ts +170 -0
- package/dist/auth.d.ts +56 -0
- package/dist/auth.js +58 -0
- package/dist/chunk-HWT6PBBS.js +155 -0
- package/dist/chunk-OK6PUP2E.js +21 -0
- package/dist/chunk-QQFH53SC.js +700 -0
- package/dist/chunk-UDMHSDXZ.js +161 -0
- package/dist/chunk-XXJLSSG3.js +116 -0
- package/dist/core.d.ts +65 -0
- package/dist/core.js +24 -0
- package/dist/i18n.d.ts +18 -0
- package/dist/i18n.js +12 -0
- package/dist/is-granted-C0-1wvoW.d.ts +10 -0
- package/dist/logger-BSnS65IC.d.ts +60 -0
- package/dist/logger.d.ts +26 -0
- package/dist/logger.js +26 -0
- package/dist/oidc-7fu5kKVF.d.ts +202 -0
- package/dist/permissions.d.ts +18 -0
- package/dist/permissions.js +8 -0
- package/dist/proxy.d.ts +201 -0
- package/dist/proxy.js +535 -0
- package/dist/react.d.ts +125 -0
- package/dist/react.js +236 -0
- package/dist/router.d.ts +39 -0
- package/dist/router.js +41 -0
- package/dist/translator-B3hyoZmK.d.ts +40 -0
- package/dist/types-Bj0MpXtI.d.ts +97 -0
- package/package.json +116 -0
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
// src/auth/base64url.ts
|
|
2
|
+
function bytesToBase64Url(bytes) {
|
|
3
|
+
let bin = "";
|
|
4
|
+
for (const byte of bytes) bin += String.fromCharCode(byte);
|
|
5
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
6
|
+
}
|
|
7
|
+
function base64UrlToBytes(value) {
|
|
8
|
+
const padded = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
9
|
+
const bin = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
|
|
10
|
+
const bytes = new Uint8Array(bin.length);
|
|
11
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
12
|
+
return bytes;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/auth/codec.ts
|
|
16
|
+
var IV_LENGTH = 12;
|
|
17
|
+
var VERSION = 1;
|
|
18
|
+
async function deriveKey(secret, usage) {
|
|
19
|
+
const ikm = await crypto.subtle.importKey(
|
|
20
|
+
"raw",
|
|
21
|
+
new TextEncoder().encode(secret),
|
|
22
|
+
"HKDF",
|
|
23
|
+
false,
|
|
24
|
+
["deriveKey"]
|
|
25
|
+
);
|
|
26
|
+
return crypto.subtle.deriveKey(
|
|
27
|
+
{
|
|
28
|
+
name: "HKDF",
|
|
29
|
+
hash: "SHA-256",
|
|
30
|
+
salt: new TextEncoder().encode("jc-abp-auth-codec-v1"),
|
|
31
|
+
info: new TextEncoder().encode(usage)
|
|
32
|
+
},
|
|
33
|
+
ikm,
|
|
34
|
+
{ name: "AES-GCM", length: 256 },
|
|
35
|
+
false,
|
|
36
|
+
["encrypt", "decrypt"]
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
function createCodec(secret, schema, opts) {
|
|
40
|
+
if (secret.length < 32) throw new Error("codec secret must be at least 32 characters");
|
|
41
|
+
const keyPromise = deriveKey(secret, opts.usage);
|
|
42
|
+
return {
|
|
43
|
+
async seal(data) {
|
|
44
|
+
const key = await keyPromise;
|
|
45
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
|
46
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(data));
|
|
47
|
+
const ciphertext = new Uint8Array(
|
|
48
|
+
await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plaintext)
|
|
49
|
+
);
|
|
50
|
+
const combined = new Uint8Array(1 + iv.length + ciphertext.length);
|
|
51
|
+
combined[0] = VERSION;
|
|
52
|
+
combined.set(iv, 1);
|
|
53
|
+
combined.set(ciphertext, 1 + iv.length);
|
|
54
|
+
return bytesToBase64Url(combined);
|
|
55
|
+
},
|
|
56
|
+
async open(token) {
|
|
57
|
+
try {
|
|
58
|
+
const key = await keyPromise;
|
|
59
|
+
const combined = base64UrlToBytes(token);
|
|
60
|
+
if (combined.length <= 1 + IV_LENGTH || combined[0] !== VERSION) return null;
|
|
61
|
+
const iv = Uint8Array.from(combined.slice(1, 1 + IV_LENGTH));
|
|
62
|
+
const ciphertext = Uint8Array.from(combined.slice(1 + IV_LENGTH));
|
|
63
|
+
const plaintext = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);
|
|
64
|
+
const parsed = schema.safeParse(JSON.parse(new TextDecoder().decode(plaintext)));
|
|
65
|
+
if (parsed.success) return parsed.data;
|
|
66
|
+
opts.onError?.(new Error("codec payload failed schema validation"));
|
|
67
|
+
return null;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
opts.onError?.(error);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/auth/cookies.ts
|
|
77
|
+
var COOKIE_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
78
|
+
function serializeCookie(name, value, opts = {}) {
|
|
79
|
+
if (!COOKIE_NAME_PATTERN.test(name)) {
|
|
80
|
+
throw new Error(`invalid cookie name: ${JSON.stringify(name)}`);
|
|
81
|
+
}
|
|
82
|
+
const sameSite = opts.sameSite ?? "Lax";
|
|
83
|
+
if (sameSite === "None" && opts.secure === false) {
|
|
84
|
+
throw new Error("SameSite=None requires Secure");
|
|
85
|
+
}
|
|
86
|
+
const parts = [`${name}=${encodeURIComponent(value)}`, `Path=${opts.path ?? "/"}`];
|
|
87
|
+
if (opts.httpOnly !== false) parts.push("HttpOnly");
|
|
88
|
+
if (opts.secure !== false) parts.push("Secure");
|
|
89
|
+
parts.push(`SameSite=${sameSite}`);
|
|
90
|
+
if (opts.maxAge !== void 0) parts.push(`Max-Age=${opts.maxAge}`);
|
|
91
|
+
return parts.join("; ");
|
|
92
|
+
}
|
|
93
|
+
function clearCookie(name, opts = {}) {
|
|
94
|
+
return serializeCookie(name, "", { ...opts, maxAge: 0 });
|
|
95
|
+
}
|
|
96
|
+
function parseCookieHeader(header) {
|
|
97
|
+
const out = {};
|
|
98
|
+
if (!header) return out;
|
|
99
|
+
for (const part of header.split(/;\s*/)) {
|
|
100
|
+
const eq = part.indexOf("=");
|
|
101
|
+
if (eq <= 0) continue;
|
|
102
|
+
const raw = part.slice(eq + 1);
|
|
103
|
+
try {
|
|
104
|
+
out[part.slice(0, eq).trim()] = decodeURIComponent(raw);
|
|
105
|
+
} catch {
|
|
106
|
+
out[part.slice(0, eq).trim()] = raw;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
var COOKIE_CHUNK_SIZE = 3600;
|
|
112
|
+
function splitByEncodedLength(value, limit) {
|
|
113
|
+
const chunks = [];
|
|
114
|
+
let chunk = "";
|
|
115
|
+
let encodedLength = 0;
|
|
116
|
+
for (const char of value) {
|
|
117
|
+
const charLength = encodeURIComponent(char).length;
|
|
118
|
+
if (chunk !== "" && encodedLength + charLength > limit) {
|
|
119
|
+
chunks.push(chunk);
|
|
120
|
+
chunk = "";
|
|
121
|
+
encodedLength = 0;
|
|
122
|
+
}
|
|
123
|
+
chunk += char;
|
|
124
|
+
encodedLength += charLength;
|
|
125
|
+
}
|
|
126
|
+
if (chunk !== "") chunks.push(chunk);
|
|
127
|
+
return chunks;
|
|
128
|
+
}
|
|
129
|
+
function chunkCookieValue(name, value, opts = {}, existing) {
|
|
130
|
+
const cookies = [];
|
|
131
|
+
const fresh = /* @__PURE__ */ new Set();
|
|
132
|
+
let chunkCount = 0;
|
|
133
|
+
const chunks = splitByEncodedLength(value, COOKIE_CHUNK_SIZE);
|
|
134
|
+
if (chunks.length <= 1) {
|
|
135
|
+
cookies.push(serializeCookie(name, value, opts));
|
|
136
|
+
fresh.add(name);
|
|
137
|
+
} else {
|
|
138
|
+
cookies.push(clearCookie(name, opts));
|
|
139
|
+
for (const chunk of chunks) {
|
|
140
|
+
const chunkName = `${name}.${chunkCount}`;
|
|
141
|
+
cookies.push(serializeCookie(chunkName, chunk, opts));
|
|
142
|
+
fresh.add(chunkName);
|
|
143
|
+
chunkCount++;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const cleared = new Set(fresh.has(name) ? [] : [name]);
|
|
147
|
+
const clearOnce = (chunkName) => {
|
|
148
|
+
if (fresh.has(chunkName) || cleared.has(chunkName)) return;
|
|
149
|
+
cleared.add(chunkName);
|
|
150
|
+
cookies.push(clearCookie(chunkName, opts));
|
|
151
|
+
};
|
|
152
|
+
clearOnce(`${name}.${chunkCount}`);
|
|
153
|
+
if (existing !== void 0) {
|
|
154
|
+
const chunkPattern = new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.\\d+$`);
|
|
155
|
+
for (const key of Object.keys(existing)) {
|
|
156
|
+
if (chunkPattern.test(key)) clearOnce(key);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return cookies;
|
|
160
|
+
}
|
|
161
|
+
function readChunkedCookie(cookies, name) {
|
|
162
|
+
const whole = cookies[name];
|
|
163
|
+
if (whole !== void 0) return whole;
|
|
164
|
+
let value = "";
|
|
165
|
+
for (let index = 0; ; index++) {
|
|
166
|
+
const part = cookies[`${name}.${index}`];
|
|
167
|
+
if (part === void 0) break;
|
|
168
|
+
value += part;
|
|
169
|
+
}
|
|
170
|
+
return value === "" ? void 0 : value;
|
|
171
|
+
}
|
|
172
|
+
function clearChunkedCookie(name, cookies, opts = {}) {
|
|
173
|
+
const out = [clearCookie(name, opts)];
|
|
174
|
+
for (let index = 0; ; index++) {
|
|
175
|
+
if (cookies[`${name}.${index}`] === void 0) break;
|
|
176
|
+
out.push(clearCookie(`${name}.${index}`, opts));
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/auth/types.ts
|
|
182
|
+
import { z } from "zod";
|
|
183
|
+
var authTokensSchema = z.object({
|
|
184
|
+
accessToken: z.string(),
|
|
185
|
+
refreshToken: z.string().optional(),
|
|
186
|
+
idToken: z.string().optional()
|
|
187
|
+
});
|
|
188
|
+
var authSessionSchema = z.object({
|
|
189
|
+
tokens: authTokensSchema,
|
|
190
|
+
expiresAt: z.number().optional(),
|
|
191
|
+
tenant: z.string().nullable().optional(),
|
|
192
|
+
culture: z.string().nullable().optional()
|
|
193
|
+
});
|
|
194
|
+
var handshakeSchema = z.object({
|
|
195
|
+
state: z.string(),
|
|
196
|
+
nonce: z.string(),
|
|
197
|
+
codeVerifier: z.string(),
|
|
198
|
+
returnUrl: z.string(),
|
|
199
|
+
issuedAt: z.number()
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// src/auth/cookie-store.ts
|
|
203
|
+
function createCookieSessionStore(opts) {
|
|
204
|
+
const codec = createCodec(opts.secret, authSessionSchema, {
|
|
205
|
+
usage: "session",
|
|
206
|
+
onError: (error) => opts.logger?.debug("session cookie open failed", { error: String(error) })
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
load: async (cookieHeader) => {
|
|
210
|
+
const sealed = readChunkedCookie(parseCookieHeader(cookieHeader), opts.cookieName);
|
|
211
|
+
if (sealed === void 0) return null;
|
|
212
|
+
const session = await codec.open(sealed);
|
|
213
|
+
opts.logger?.debug("session opened", { found: session !== null, sealedBytes: sealed.length });
|
|
214
|
+
return session;
|
|
215
|
+
},
|
|
216
|
+
save: async (session, cookieHeader) => {
|
|
217
|
+
const sealed = await codec.seal(session);
|
|
218
|
+
const cookies = chunkCookieValue(
|
|
219
|
+
opts.cookieName,
|
|
220
|
+
sealed,
|
|
221
|
+
{ ...opts.cookieOptions, maxAge: opts.maxAge },
|
|
222
|
+
parseCookieHeader(cookieHeader)
|
|
223
|
+
);
|
|
224
|
+
opts.logger?.debug("session sealed", { sealedBytes: sealed.length, cookies: cookies.length });
|
|
225
|
+
return cookies;
|
|
226
|
+
},
|
|
227
|
+
clear: async (cookieHeader) => clearChunkedCookie(opts.cookieName, parseCookieHeader(cookieHeader), opts.cookieOptions)
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/auth/oidc/token-client.ts
|
|
232
|
+
import { z as z3 } from "zod";
|
|
233
|
+
|
|
234
|
+
// src/auth/errors.ts
|
|
235
|
+
var AuthError = class extends Error {
|
|
236
|
+
code;
|
|
237
|
+
constructor(code, message, opts) {
|
|
238
|
+
super(message ?? code, opts);
|
|
239
|
+
this.name = "AuthError";
|
|
240
|
+
this.code = code;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// src/auth/oidc/metadata.ts
|
|
245
|
+
import { z as z2 } from "zod";
|
|
246
|
+
var oidcMetadataSchema = z2.object({
|
|
247
|
+
issuer: z2.string(),
|
|
248
|
+
authorization_endpoint: z2.string(),
|
|
249
|
+
token_endpoint: z2.string(),
|
|
250
|
+
end_session_endpoint: z2.string().optional(),
|
|
251
|
+
revocation_endpoint: z2.string().optional()
|
|
252
|
+
});
|
|
253
|
+
async function discoverMetadata(issuer, opts = {}) {
|
|
254
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
255
|
+
const url = `${issuer.replace(/\/$/, "")}/.well-known/openid-configuration`;
|
|
256
|
+
try {
|
|
257
|
+
const res = await fetchFn(url, { signal: AbortSignal.timeout(opts.timeoutMs ?? 3e4) });
|
|
258
|
+
if (!res.ok) throw new Error(`discovery returned ${res.status}`);
|
|
259
|
+
const metadata = oidcMetadataSchema.parse(await res.json());
|
|
260
|
+
const normalize = (v) => v.replace(/\/$/, "");
|
|
261
|
+
if (normalize(metadata.issuer) !== normalize(issuer)) {
|
|
262
|
+
throw new Error(`issuer mismatch: expected ${issuer}, got ${metadata.issuer}`);
|
|
263
|
+
}
|
|
264
|
+
return metadata;
|
|
265
|
+
} catch (error) {
|
|
266
|
+
throw new AuthError("discovery_failed", `OIDC discovery failed for ${issuer}`, {
|
|
267
|
+
cause: error
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/auth/oidc/token-client.ts
|
|
273
|
+
var tokenResponseSchema = z3.object({
|
|
274
|
+
access_token: z3.string(),
|
|
275
|
+
refresh_token: z3.string().optional(),
|
|
276
|
+
id_token: z3.string().optional(),
|
|
277
|
+
expires_in: z3.number().optional()
|
|
278
|
+
});
|
|
279
|
+
function toTokenResult(grant, nowMs) {
|
|
280
|
+
return {
|
|
281
|
+
tokens: {
|
|
282
|
+
accessToken: grant.accessToken,
|
|
283
|
+
refreshToken: grant.refreshToken,
|
|
284
|
+
idToken: grant.idToken
|
|
285
|
+
},
|
|
286
|
+
expiresAt: grant.expiresIn !== void 0 ? nowMs + grant.expiresIn * 1e3 : void 0
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function createTokenClient(cfg) {
|
|
290
|
+
const fetchFn = cfg.fetchFn ?? fetch;
|
|
291
|
+
const timeoutMs = cfg.timeoutMs ?? 3e4;
|
|
292
|
+
let metadataPromise;
|
|
293
|
+
const metadata = () => {
|
|
294
|
+
metadataPromise ??= discoverMetadata(cfg.issuer, { fetchFn, timeoutMs }).catch((error) => {
|
|
295
|
+
metadataPromise = void 0;
|
|
296
|
+
throw error;
|
|
297
|
+
});
|
|
298
|
+
return metadataPromise;
|
|
299
|
+
};
|
|
300
|
+
async function postToken(form, errorOf, headers = {}) {
|
|
301
|
+
const grantType = form.grant_type;
|
|
302
|
+
const md = await metadata();
|
|
303
|
+
let res;
|
|
304
|
+
try {
|
|
305
|
+
res = await fetchFn(md.token_endpoint, {
|
|
306
|
+
method: "POST",
|
|
307
|
+
headers: {
|
|
308
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
309
|
+
Accept: "application/json",
|
|
310
|
+
...headers
|
|
311
|
+
},
|
|
312
|
+
body: new URLSearchParams({ client_id: cfg.clientId, ...form }).toString(),
|
|
313
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
314
|
+
});
|
|
315
|
+
} catch (error) {
|
|
316
|
+
throw new AuthError(errorOf(0), `token request (${grantType}) failed`, { cause: error });
|
|
317
|
+
}
|
|
318
|
+
cfg.logger?.debug("token endpoint responded", { grantType, status: res.status });
|
|
319
|
+
if (!res.ok) {
|
|
320
|
+
throw new AuthError(
|
|
321
|
+
errorOf(res.status),
|
|
322
|
+
`token request (${grantType}) returned ${res.status}`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const parsed = tokenResponseSchema.parse(await res.json());
|
|
327
|
+
return {
|
|
328
|
+
accessToken: parsed.access_token,
|
|
329
|
+
refreshToken: parsed.refresh_token,
|
|
330
|
+
idToken: parsed.id_token,
|
|
331
|
+
expiresIn: parsed.expires_in
|
|
332
|
+
};
|
|
333
|
+
} catch (error) {
|
|
334
|
+
throw new AuthError(errorOf(0), `token response (${grantType}) malformed`, { cause: error });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const withSecret = (form) => cfg.clientSecret === void 0 ? form : { ...form, client_secret: cfg.clientSecret };
|
|
338
|
+
const tenantParts = (tenant) => tenant && cfg.tenantPropagation !== void 0 ? cfg.tenantPropagation(tenant) : {};
|
|
339
|
+
return {
|
|
340
|
+
metadata,
|
|
341
|
+
exchangeCode: (p) => postToken(
|
|
342
|
+
withSecret({
|
|
343
|
+
grant_type: "authorization_code",
|
|
344
|
+
code: p.code,
|
|
345
|
+
code_verifier: p.codeVerifier,
|
|
346
|
+
redirect_uri: p.redirectUri
|
|
347
|
+
}),
|
|
348
|
+
() => "exchange_failed"
|
|
349
|
+
),
|
|
350
|
+
passwordGrant: (p) => postToken(
|
|
351
|
+
withSecret({
|
|
352
|
+
grant_type: "password",
|
|
353
|
+
username: p.userName,
|
|
354
|
+
password: p.password,
|
|
355
|
+
...cfg.scope === void 0 ? {} : { scope: cfg.scope }
|
|
356
|
+
}),
|
|
357
|
+
(status) => status === 400 ? "invalid_credentials" : "exchange_failed",
|
|
358
|
+
tenantParts(p.tenant).headers
|
|
359
|
+
),
|
|
360
|
+
refreshGrant: (refreshToken) => postToken(
|
|
361
|
+
withSecret({ grant_type: "refresh_token", refresh_token: refreshToken }),
|
|
362
|
+
() => "refresh_failed"
|
|
363
|
+
),
|
|
364
|
+
revoke: async (refreshToken) => {
|
|
365
|
+
const md = await metadata();
|
|
366
|
+
if (md.revocation_endpoint === void 0) return;
|
|
367
|
+
await fetchFn(md.revocation_endpoint, {
|
|
368
|
+
method: "POST",
|
|
369
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
370
|
+
body: new URLSearchParams(
|
|
371
|
+
withSecret({
|
|
372
|
+
token: refreshToken,
|
|
373
|
+
token_type_hint: "refresh_token",
|
|
374
|
+
client_id: cfg.clientId
|
|
375
|
+
})
|
|
376
|
+
).toString(),
|
|
377
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
378
|
+
});
|
|
379
|
+
},
|
|
380
|
+
authorizeUrl: async (p) => {
|
|
381
|
+
const md = await metadata();
|
|
382
|
+
const url = new URL(md.authorization_endpoint);
|
|
383
|
+
url.searchParams.set("response_type", "code");
|
|
384
|
+
url.searchParams.set("client_id", cfg.clientId);
|
|
385
|
+
url.searchParams.set("redirect_uri", p.redirectUri);
|
|
386
|
+
url.searchParams.set("scope", cfg.scope ?? "openid profile");
|
|
387
|
+
url.searchParams.set("state", p.state);
|
|
388
|
+
url.searchParams.set("nonce", p.nonce);
|
|
389
|
+
url.searchParams.set("code_challenge", p.codeChallenge);
|
|
390
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
391
|
+
for (const [key, value] of Object.entries(tenantParts(p.tenant).query ?? {})) {
|
|
392
|
+
url.searchParams.set(key, value);
|
|
393
|
+
}
|
|
394
|
+
return url.toString();
|
|
395
|
+
},
|
|
396
|
+
endSessionUrl: async (p) => {
|
|
397
|
+
const md = await metadata();
|
|
398
|
+
if (md.end_session_endpoint === void 0) return null;
|
|
399
|
+
const url = new URL(md.end_session_endpoint);
|
|
400
|
+
if (p.idToken !== void 0) url.searchParams.set("id_token_hint", p.idToken);
|
|
401
|
+
if (p.postLogoutRedirectUri !== void 0)
|
|
402
|
+
url.searchParams.set("post_logout_redirect_uri", p.postLogoutRedirectUri);
|
|
403
|
+
return url.toString();
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/auth/manager.ts
|
|
409
|
+
function createSessionManager(deps) {
|
|
410
|
+
const now = deps.now ?? (() => Date.now());
|
|
411
|
+
const skewMs = (deps.skewSeconds ?? 60) * 1e3;
|
|
412
|
+
const ttlMs = deps.coalesceTtlMs ?? 1e4;
|
|
413
|
+
const revokeTimeoutMs = deps.revokeTimeoutMs ?? 2e3;
|
|
414
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
415
|
+
const coalescedGrant = (refreshToken) => {
|
|
416
|
+
const at = now();
|
|
417
|
+
for (const [key, entry] of inflight) {
|
|
418
|
+
if (at - entry.at >= ttlMs) inflight.delete(key);
|
|
419
|
+
}
|
|
420
|
+
const hit = inflight.get(refreshToken);
|
|
421
|
+
if (hit !== void 0) return hit.promise;
|
|
422
|
+
const promise = deps.refreshGrant(refreshToken);
|
|
423
|
+
inflight.set(refreshToken, { at, promise });
|
|
424
|
+
promise.catch(() => inflight.delete(refreshToken));
|
|
425
|
+
return promise;
|
|
426
|
+
};
|
|
427
|
+
const revokeWithTimeout = async (revoke, refreshToken) => {
|
|
428
|
+
const revoking = revoke(refreshToken);
|
|
429
|
+
revoking.catch(() => {
|
|
430
|
+
});
|
|
431
|
+
let timer;
|
|
432
|
+
try {
|
|
433
|
+
await Promise.race([
|
|
434
|
+
revoking,
|
|
435
|
+
new Promise((_, reject) => {
|
|
436
|
+
timer = setTimeout(
|
|
437
|
+
() => reject(new Error(`revoke timed out after ${revokeTimeoutMs}ms`)),
|
|
438
|
+
revokeTimeoutMs
|
|
439
|
+
);
|
|
440
|
+
})
|
|
441
|
+
]);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
deps.logger?.warn("token revocation failed", { error: String(error) });
|
|
444
|
+
} finally {
|
|
445
|
+
clearTimeout(timer);
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
return {
|
|
449
|
+
establish: (result, ctx) => deps.store.save(
|
|
450
|
+
{
|
|
451
|
+
tokens: result.tokens,
|
|
452
|
+
expiresAt: result.expiresAt,
|
|
453
|
+
tenant: ctx?.tenant,
|
|
454
|
+
culture: ctx?.culture
|
|
455
|
+
},
|
|
456
|
+
ctx?.cookieHeader
|
|
457
|
+
),
|
|
458
|
+
current: (cookieHeader) => deps.store.load(cookieHeader),
|
|
459
|
+
isExpired: (session) => session.expiresAt !== void 0 && now() >= session.expiresAt - skewMs,
|
|
460
|
+
refresh: async (session, cookieHeader) => {
|
|
461
|
+
const refreshToken = session.tokens.refreshToken;
|
|
462
|
+
if (refreshToken === void 0) return null;
|
|
463
|
+
try {
|
|
464
|
+
const grant = await coalescedGrant(refreshToken);
|
|
465
|
+
const result = toTokenResult(grant, now());
|
|
466
|
+
const next = {
|
|
467
|
+
tokens: {
|
|
468
|
+
...result.tokens,
|
|
469
|
+
// IdP 未轮换时沿用旧 refresh token,否则会话将失去续期能力。
|
|
470
|
+
refreshToken: result.tokens.refreshToken ?? refreshToken
|
|
471
|
+
},
|
|
472
|
+
expiresAt: result.expiresAt,
|
|
473
|
+
tenant: session.tenant,
|
|
474
|
+
culture: session.culture
|
|
475
|
+
};
|
|
476
|
+
const setCookies = await deps.store.save(next, cookieHeader);
|
|
477
|
+
deps.logger?.debug("session refreshed", {
|
|
478
|
+
rotated: result.tokens.refreshToken !== void 0 && result.tokens.refreshToken !== refreshToken
|
|
479
|
+
});
|
|
480
|
+
return { session: next, setCookies };
|
|
481
|
+
} catch (error) {
|
|
482
|
+
deps.logger?.warn("session refresh failed", { error: String(error) });
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
destroy: async (cookieHeader) => {
|
|
487
|
+
const session = await deps.store.load(cookieHeader);
|
|
488
|
+
const refreshToken = session?.tokens.refreshToken;
|
|
489
|
+
if (refreshToken !== void 0 && deps.revoke !== void 0) {
|
|
490
|
+
await revokeWithTimeout(deps.revoke, refreshToken);
|
|
491
|
+
}
|
|
492
|
+
deps.logger?.debug("session destroyed", { hadSession: session !== null });
|
|
493
|
+
return deps.store.clear(cookieHeader);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/auth/create-auth.ts
|
|
499
|
+
function createAuth(opts) {
|
|
500
|
+
const strategies = new Map(opts.strategies.map((s) => [s.name, s]));
|
|
501
|
+
const session = createSessionManager({
|
|
502
|
+
store: opts.store,
|
|
503
|
+
refreshGrant: opts.refreshGrant,
|
|
504
|
+
revoke: opts.revoke,
|
|
505
|
+
logger: opts.logger,
|
|
506
|
+
now: opts.now,
|
|
507
|
+
revokeTimeoutMs: opts.revokeTimeoutMs,
|
|
508
|
+
skewSeconds: opts.skewSeconds,
|
|
509
|
+
coalesceTtlMs: opts.coalesceTtlMs
|
|
510
|
+
});
|
|
511
|
+
return {
|
|
512
|
+
strategy: (name) => {
|
|
513
|
+
const strategy = strategies.get(name);
|
|
514
|
+
if (strategy === void 0) throw new Error(`unknown auth strategy: ${name}`);
|
|
515
|
+
return strategy;
|
|
516
|
+
},
|
|
517
|
+
session,
|
|
518
|
+
identity: async (session2, ctx = { cookieHeader: null }) => {
|
|
519
|
+
const startedAt = (opts.now ?? Date.now)();
|
|
520
|
+
const identity = await opts.resolveIdentity(session2, ctx);
|
|
521
|
+
opts.logger?.debug("identity resolved", {
|
|
522
|
+
isAuthenticated: identity.isAuthenticated,
|
|
523
|
+
policies: Object.keys(identity.grantedPolicies).length,
|
|
524
|
+
ms: (opts.now ?? Date.now)() - startedAt
|
|
525
|
+
});
|
|
526
|
+
return identity;
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/auth/culture.ts
|
|
532
|
+
function parseCultureCookie(value) {
|
|
533
|
+
if (!value) return null;
|
|
534
|
+
for (const part of value.split("|")) {
|
|
535
|
+
if (part.startsWith("c=")) return part.slice(2);
|
|
536
|
+
}
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
function formatCultureCookie(culture) {
|
|
540
|
+
return `c=${culture}|uic=${culture}`;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/auth/oidc/claims.ts
|
|
544
|
+
function decodeIdTokenClaims(idToken) {
|
|
545
|
+
const payload = idToken.split(".")[1];
|
|
546
|
+
if (payload === void 0) return {};
|
|
547
|
+
try {
|
|
548
|
+
return JSON.parse(new TextDecoder().decode(base64UrlToBytes(payload)));
|
|
549
|
+
} catch {
|
|
550
|
+
return {};
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/auth/pkce.ts
|
|
555
|
+
function generateRandomString(byteLength = 32) {
|
|
556
|
+
const bytes = new Uint8Array(byteLength);
|
|
557
|
+
crypto.getRandomValues(bytes);
|
|
558
|
+
return bytesToBase64Url(bytes);
|
|
559
|
+
}
|
|
560
|
+
async function generatePkce() {
|
|
561
|
+
const verifier = generateRandomString(32);
|
|
562
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
563
|
+
return { verifier, challenge: bytesToBase64Url(new Uint8Array(digest)) };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/auth/return-url.ts
|
|
567
|
+
function hasBrowserStrippedChars(value) {
|
|
568
|
+
for (const char of value) {
|
|
569
|
+
const code = char.codePointAt(0) ?? 0;
|
|
570
|
+
if (code <= 32 || code === 127) return true;
|
|
571
|
+
}
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
function sanitizeReturnUrl(value) {
|
|
575
|
+
if (!value?.startsWith("/") || value.startsWith("//") || value.startsWith("/\\")) {
|
|
576
|
+
return "/";
|
|
577
|
+
}
|
|
578
|
+
if (hasBrowserStrippedChars(value)) {
|
|
579
|
+
return "/";
|
|
580
|
+
}
|
|
581
|
+
return value;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/auth/strategies/oidc.ts
|
|
585
|
+
function oidcStrategy(cfg) {
|
|
586
|
+
const now = cfg.now ?? (() => Date.now());
|
|
587
|
+
const random = cfg.random ?? generateRandomString;
|
|
588
|
+
const pkce = cfg.pkce ?? generatePkce;
|
|
589
|
+
const handshakeMaxAgeMs = (cfg.handshakeMaxAgeSeconds ?? 600) * 1e3;
|
|
590
|
+
return {
|
|
591
|
+
name: "oidc",
|
|
592
|
+
async begin(input) {
|
|
593
|
+
const { verifier, challenge } = await pkce();
|
|
594
|
+
const handshake = {
|
|
595
|
+
state: random(),
|
|
596
|
+
nonce: random(),
|
|
597
|
+
codeVerifier: verifier,
|
|
598
|
+
returnUrl: input.returnUrl,
|
|
599
|
+
issuedAt: now()
|
|
600
|
+
};
|
|
601
|
+
const redirectUrl = await cfg.tokenClient.authorizeUrl({
|
|
602
|
+
state: handshake.state,
|
|
603
|
+
nonce: handshake.nonce,
|
|
604
|
+
codeChallenge: challenge,
|
|
605
|
+
redirectUri: cfg.redirectUri,
|
|
606
|
+
tenant: input.tenant
|
|
607
|
+
});
|
|
608
|
+
cfg.logger?.debug("oidc begin", {
|
|
609
|
+
host: new URL(redirectUrl).host,
|
|
610
|
+
stateBytes: handshake.state.length
|
|
611
|
+
});
|
|
612
|
+
return { redirectUrl, handshake };
|
|
613
|
+
},
|
|
614
|
+
async complete(input) {
|
|
615
|
+
if (input.kind !== "callback") {
|
|
616
|
+
throw new AuthError("invalid_input", "oidc strategy only completes callback inputs");
|
|
617
|
+
}
|
|
618
|
+
if (now() - input.handshake.issuedAt > handshakeMaxAgeMs) {
|
|
619
|
+
throw new AuthError("handshake_expired", "handshake outlived its server-side lifetime");
|
|
620
|
+
}
|
|
621
|
+
const code = input.params.get("code");
|
|
622
|
+
const state = input.params.get("state");
|
|
623
|
+
if (state !== input.handshake.state) throw new AuthError("invalid_state");
|
|
624
|
+
const providerError = input.params.get("error");
|
|
625
|
+
if (providerError !== null) {
|
|
626
|
+
cfg.logger?.debug("oidc provider error", {
|
|
627
|
+
error: providerError,
|
|
628
|
+
description: input.params.get("error_description") ?? void 0
|
|
629
|
+
});
|
|
630
|
+
throw new AuthError("provider_denied", `provider returned error: ${providerError}`);
|
|
631
|
+
}
|
|
632
|
+
if (code === null) throw new AuthError("exchange_failed", "callback carried no code");
|
|
633
|
+
const grant = await cfg.tokenClient.exchangeCode({
|
|
634
|
+
code,
|
|
635
|
+
codeVerifier: input.handshake.codeVerifier,
|
|
636
|
+
redirectUri: cfg.redirectUri
|
|
637
|
+
});
|
|
638
|
+
if (grant.idToken !== void 0) {
|
|
639
|
+
if (decodeIdTokenClaims(grant.idToken).nonce !== input.handshake.nonce) {
|
|
640
|
+
throw new AuthError("invalid_nonce");
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
cfg.logger?.debug("oidc complete", { hasRefreshToken: grant.refreshToken !== void 0 });
|
|
644
|
+
return toTokenResult(grant, now());
|
|
645
|
+
},
|
|
646
|
+
logoutUrl: (p) => cfg.tokenClient.endSessionUrl(p)
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/auth/strategies/password.ts
|
|
651
|
+
function passwordStrategy(cfg) {
|
|
652
|
+
const now = cfg.now ?? (() => Date.now());
|
|
653
|
+
return {
|
|
654
|
+
name: "password",
|
|
655
|
+
async complete(input) {
|
|
656
|
+
if (input.kind !== "credentials") {
|
|
657
|
+
throw new AuthError("invalid_input", "password strategy only completes credentials");
|
|
658
|
+
}
|
|
659
|
+
const grant = await cfg.tokenClient.passwordGrant({
|
|
660
|
+
userName: input.userName,
|
|
661
|
+
password: input.password,
|
|
662
|
+
tenant: input.tenant
|
|
663
|
+
});
|
|
664
|
+
cfg.logger?.debug("password attempt succeeded", {
|
|
665
|
+
userName: `${input.userName[0] ?? ""}***`
|
|
666
|
+
});
|
|
667
|
+
return toTokenResult(grant, now());
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export {
|
|
673
|
+
createCodec,
|
|
674
|
+
serializeCookie,
|
|
675
|
+
clearCookie,
|
|
676
|
+
parseCookieHeader,
|
|
677
|
+
COOKIE_CHUNK_SIZE,
|
|
678
|
+
chunkCookieValue,
|
|
679
|
+
readChunkedCookie,
|
|
680
|
+
clearChunkedCookie,
|
|
681
|
+
authTokensSchema,
|
|
682
|
+
authSessionSchema,
|
|
683
|
+
handshakeSchema,
|
|
684
|
+
createCookieSessionStore,
|
|
685
|
+
AuthError,
|
|
686
|
+
oidcMetadataSchema,
|
|
687
|
+
discoverMetadata,
|
|
688
|
+
toTokenResult,
|
|
689
|
+
createTokenClient,
|
|
690
|
+
createSessionManager,
|
|
691
|
+
createAuth,
|
|
692
|
+
parseCultureCookie,
|
|
693
|
+
formatCultureCookie,
|
|
694
|
+
decodeIdTokenClaims,
|
|
695
|
+
generateRandomString,
|
|
696
|
+
generatePkce,
|
|
697
|
+
sanitizeReturnUrl,
|
|
698
|
+
oidcStrategy,
|
|
699
|
+
passwordStrategy
|
|
700
|
+
};
|