@stackwright-pro/auth 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/README.md +33 -0
- package/dist/index.d.mts +1009 -0
- package/dist/index.d.ts +1009 -0
- package/dist/index.js +1182 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1124 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +46 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1124 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { X509Certificate } from '@peculiar/x509';
|
|
3
|
+
import * as jose from 'jose';
|
|
4
|
+
import { createContext, useContext, useMemo } from 'react';
|
|
5
|
+
import { jsx } from 'react/jsx-runtime';
|
|
6
|
+
|
|
7
|
+
// src/schemas/auth-schemas.ts
|
|
8
|
+
var pkiConfigSchema = z.object({
|
|
9
|
+
type: z.literal("pki"),
|
|
10
|
+
profile: z.enum(["dod_cac", "piv", "custom"]),
|
|
11
|
+
source: z.enum(["gateway_headers", "direct_tls"]),
|
|
12
|
+
headerPrefix: z.string().optional().default("x-client-cert-"),
|
|
13
|
+
verifiedHeader: z.string().optional().default("x-client-cert-verified"),
|
|
14
|
+
requiredValue: z.string().optional().default("SUCCESS"),
|
|
15
|
+
caChain: z.string().optional(),
|
|
16
|
+
requiredOU: z.array(z.string()).optional(),
|
|
17
|
+
allowedIssuers: z.array(z.string()).optional()
|
|
18
|
+
});
|
|
19
|
+
var oidcConfigSchema = z.object({
|
|
20
|
+
type: z.literal("oidc"),
|
|
21
|
+
provider: z.enum([
|
|
22
|
+
"cognito",
|
|
23
|
+
"azure_ad",
|
|
24
|
+
"authentik",
|
|
25
|
+
"keycloak",
|
|
26
|
+
"okta",
|
|
27
|
+
"auth0",
|
|
28
|
+
"custom"
|
|
29
|
+
]),
|
|
30
|
+
discoveryUrl: z.string().url(),
|
|
31
|
+
clientId: z.string(),
|
|
32
|
+
clientSecret: z.string(),
|
|
33
|
+
redirectUri: z.string().optional(),
|
|
34
|
+
claimsMapping: z.object({
|
|
35
|
+
user_id: z.string().optional(),
|
|
36
|
+
email: z.string().optional(),
|
|
37
|
+
name: z.string().optional(),
|
|
38
|
+
roles: z.string().optional()
|
|
39
|
+
}).optional(),
|
|
40
|
+
quirks: z.object({
|
|
41
|
+
skipIssuerCheck: z.boolean().optional(),
|
|
42
|
+
useRefreshTokenRotation: z.boolean().optional()
|
|
43
|
+
}).optional()
|
|
44
|
+
});
|
|
45
|
+
var authConfigSchema = z.discriminatedUnion("type", [
|
|
46
|
+
pkiConfigSchema,
|
|
47
|
+
oidcConfigSchema
|
|
48
|
+
]);
|
|
49
|
+
var componentAuthSchema = z.object({
|
|
50
|
+
required_roles: z.array(z.string()).optional(),
|
|
51
|
+
required_permissions: z.array(z.string()).optional(),
|
|
52
|
+
fallback: z.enum(["hide", "placeholder", "message"]).optional().default("hide"),
|
|
53
|
+
fallback_message: z.string().optional()
|
|
54
|
+
}).optional();
|
|
55
|
+
var rbacConfigSchema = z.object({
|
|
56
|
+
roles: z.array(
|
|
57
|
+
z.object({
|
|
58
|
+
name: z.string(),
|
|
59
|
+
permissions: z.array(z.string()).optional()
|
|
60
|
+
})
|
|
61
|
+
),
|
|
62
|
+
protected_routes: z.array(
|
|
63
|
+
z.object({
|
|
64
|
+
path: z.string(),
|
|
65
|
+
roles: z.array(z.string())
|
|
66
|
+
})
|
|
67
|
+
).optional(),
|
|
68
|
+
public_routes: z.array(z.string()).optional()
|
|
69
|
+
});
|
|
70
|
+
var authUserSchema = z.object({
|
|
71
|
+
id: z.string(),
|
|
72
|
+
email: z.string().email().optional(),
|
|
73
|
+
name: z.string().optional(),
|
|
74
|
+
roles: z.array(z.string()),
|
|
75
|
+
permissions: z.array(z.string()).optional(),
|
|
76
|
+
metadata: z.record(z.string(), z.any()).optional()
|
|
77
|
+
});
|
|
78
|
+
var authSessionSchema = z.object({
|
|
79
|
+
user: authUserSchema,
|
|
80
|
+
expiresAt: z.number(),
|
|
81
|
+
issuedAt: z.number(),
|
|
82
|
+
refreshToken: z.string().optional()
|
|
83
|
+
});
|
|
84
|
+
function parseCertificate(pemOrDer) {
|
|
85
|
+
const cert = new X509Certificate(pemOrDer);
|
|
86
|
+
const subjectAttrs = cert.subject.split(",").map((s) => s.trim());
|
|
87
|
+
const issuerAttrs = cert.issuer.split(",").map((s) => s.trim());
|
|
88
|
+
return {
|
|
89
|
+
subject: parseNameAttributes(subjectAttrs),
|
|
90
|
+
issuer: {
|
|
91
|
+
commonName: extractAttribute(issuerAttrs, "CN"),
|
|
92
|
+
organization: extractAttribute(issuerAttrs, "O")
|
|
93
|
+
},
|
|
94
|
+
serialNumber: cert.serialNumber,
|
|
95
|
+
notBefore: cert.notBefore,
|
|
96
|
+
notAfter: cert.notAfter,
|
|
97
|
+
isValid: Date.now() >= cert.notBefore.getTime() && Date.now() <= cert.notAfter.getTime()
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function extractAttribute(attrs, key) {
|
|
101
|
+
for (const attr of attrs) {
|
|
102
|
+
const [attrKey, ...valueParts] = attr.split("=");
|
|
103
|
+
if (attrKey.trim().toUpperCase() === key.toUpperCase()) {
|
|
104
|
+
return valueParts.join("=").trim();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return void 0;
|
|
108
|
+
}
|
|
109
|
+
function extractAttributes(attrs, key) {
|
|
110
|
+
const results = [];
|
|
111
|
+
for (const attr of attrs) {
|
|
112
|
+
const [attrKey, ...valueParts] = attr.split("=");
|
|
113
|
+
if (attrKey.trim().toUpperCase() === key.toUpperCase()) {
|
|
114
|
+
results.push(valueParts.join("=").trim());
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return results;
|
|
118
|
+
}
|
|
119
|
+
function parseNameAttributes(attrs) {
|
|
120
|
+
return {
|
|
121
|
+
commonName: extractAttribute(attrs, "CN"),
|
|
122
|
+
email: extractAttribute(attrs, "E") || extractAttribute(attrs, "emailAddress"),
|
|
123
|
+
organizationalUnit: extractAttributes(attrs, "OU"),
|
|
124
|
+
organization: extractAttribute(attrs, "O"),
|
|
125
|
+
country: extractAttribute(attrs, "C")
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function extractEDIPI(cert) {
|
|
129
|
+
const EDIPI_OID = "2.16.840.1.101.2.1.11.42";
|
|
130
|
+
for (const ext of cert.extensions) {
|
|
131
|
+
if (ext.type === EDIPI_OID) {
|
|
132
|
+
const value = ext.value;
|
|
133
|
+
return Buffer.from(value).toString("hex");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return void 0;
|
|
137
|
+
}
|
|
138
|
+
function validateDoDCAC(parsed) {
|
|
139
|
+
const hasDoDOU = parsed.subject.organizationalUnit?.some(
|
|
140
|
+
(ou) => ou.toUpperCase().includes("DOD") || ou === "DoD"
|
|
141
|
+
);
|
|
142
|
+
if (!parsed.isValid) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
if (!hasDoDOU) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
function parseCertFromHeaders(headers, prefix = "x-client-cert-") {
|
|
151
|
+
const dnHeader = headers[`${prefix}dn`] || headers[`${prefix}subject-dn`];
|
|
152
|
+
const serialHeader = headers[`${prefix}serial`];
|
|
153
|
+
const verifiedHeader = headers[`${prefix}verified`];
|
|
154
|
+
if (!dnHeader) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const subject = parseDN(dnHeader);
|
|
158
|
+
return {
|
|
159
|
+
subject,
|
|
160
|
+
issuer: {},
|
|
161
|
+
// Not available from headers
|
|
162
|
+
serialNumber: serialHeader || "unknown",
|
|
163
|
+
notBefore: /* @__PURE__ */ new Date(0),
|
|
164
|
+
// Not available from headers
|
|
165
|
+
notAfter: new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3),
|
|
166
|
+
// Assume valid for a year
|
|
167
|
+
isValid: verifiedHeader?.toUpperCase() === "SUCCESS"
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function parseDN(dn) {
|
|
171
|
+
const parts = dn.split(",").map((p) => p.trim());
|
|
172
|
+
const result = {
|
|
173
|
+
organizationalUnit: []
|
|
174
|
+
};
|
|
175
|
+
for (const part of parts) {
|
|
176
|
+
const [key, ...valueParts] = part.split("=");
|
|
177
|
+
const value = valueParts.join("=");
|
|
178
|
+
switch (key.toUpperCase()) {
|
|
179
|
+
case "CN":
|
|
180
|
+
result.commonName = value;
|
|
181
|
+
break;
|
|
182
|
+
case "E":
|
|
183
|
+
case "EMAILADDRESS":
|
|
184
|
+
result.email = value;
|
|
185
|
+
break;
|
|
186
|
+
case "OU":
|
|
187
|
+
result.organizationalUnit?.push(value);
|
|
188
|
+
break;
|
|
189
|
+
case "O":
|
|
190
|
+
result.organization = value;
|
|
191
|
+
break;
|
|
192
|
+
case "C":
|
|
193
|
+
result.country = value;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/providers/pki-provider.ts
|
|
201
|
+
var PKIProvider = class {
|
|
202
|
+
constructor(config) {
|
|
203
|
+
this.config = config;
|
|
204
|
+
}
|
|
205
|
+
async authenticate(context) {
|
|
206
|
+
let parsed = null;
|
|
207
|
+
if (this.config.source === "gateway_headers") {
|
|
208
|
+
if (!context.headers) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
parsed = parseCertFromHeaders(context.headers, this.config.headerPrefix);
|
|
212
|
+
} else {
|
|
213
|
+
const certData = context.headers?.["x-client-cert"];
|
|
214
|
+
if (!certData) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
parsed = parseCertificate(certData);
|
|
218
|
+
}
|
|
219
|
+
if (!parsed) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
if (!parsed.isValid) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
if (this.config.profile === "dod_cac") {
|
|
226
|
+
if (!validateDoDCAC(parsed)) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (this.config.requiredOU) {
|
|
231
|
+
const hasRequiredOU = this.config.requiredOU.some(
|
|
232
|
+
(required) => parsed.subject.organizationalUnit?.some((ou) => ou.includes(required))
|
|
233
|
+
);
|
|
234
|
+
if (!hasRequiredOU) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (this.config.allowedIssuers) {
|
|
239
|
+
const issuerCN = parsed.issuer.commonName;
|
|
240
|
+
if (!issuerCN || !this.config.allowedIssuers.includes(issuerCN)) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const user = {
|
|
245
|
+
id: parsed.serialNumber,
|
|
246
|
+
// Use serial as unique ID
|
|
247
|
+
name: parsed.subject.commonName,
|
|
248
|
+
email: parsed.subject.email,
|
|
249
|
+
roles: this.extractRolesFromCertificate(parsed),
|
|
250
|
+
metadata: {
|
|
251
|
+
organizationalUnit: parsed.subject.organizationalUnit,
|
|
252
|
+
organization: parsed.subject.organization,
|
|
253
|
+
country: parsed.subject.country,
|
|
254
|
+
issuer: parsed.issuer.commonName,
|
|
255
|
+
notBefore: parsed.notBefore.toISOString(),
|
|
256
|
+
notAfter: parsed.notAfter.toISOString()
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
return user;
|
|
260
|
+
}
|
|
261
|
+
async validate(session) {
|
|
262
|
+
if (Date.now() > session.expiresAt) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Extract roles from certificate based on organizational units
|
|
269
|
+
* Can be customized per deployment via subclassing
|
|
270
|
+
*/
|
|
271
|
+
extractRolesFromCertificate(parsed) {
|
|
272
|
+
const roles = [];
|
|
273
|
+
const ous = parsed.subject.organizationalUnit || [];
|
|
274
|
+
if (ous.some((ou) => ou.includes("ADMIN") || ou.includes("ADMINISTRATOR"))) {
|
|
275
|
+
roles.push("ADMIN");
|
|
276
|
+
} else if (ous.some((ou) => ou.includes("ANALYST"))) {
|
|
277
|
+
roles.push("ANALYST");
|
|
278
|
+
} else {
|
|
279
|
+
roles.push("VIEWER");
|
|
280
|
+
}
|
|
281
|
+
return roles;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// src/oidc/discovery.ts
|
|
286
|
+
async function discoverOIDC(discoveryUrl) {
|
|
287
|
+
const response = await fetch(discoveryUrl);
|
|
288
|
+
if (!response.ok) {
|
|
289
|
+
throw new Error(`OIDC discovery failed: ${response.statusText}`);
|
|
290
|
+
}
|
|
291
|
+
const metadata = await response.json();
|
|
292
|
+
if (!metadata.issuer || !metadata.authorization_endpoint || !metadata.token_endpoint || !metadata.jwks_uri) {
|
|
293
|
+
throw new Error("Invalid OIDC metadata: missing required fields");
|
|
294
|
+
}
|
|
295
|
+
return metadata;
|
|
296
|
+
}
|
|
297
|
+
function buildAuthorizationUrl(metadata, clientId, redirectUri, state, scopes = ["openid", "profile", "email"]) {
|
|
298
|
+
const url = new URL(metadata.authorization_endpoint);
|
|
299
|
+
url.searchParams.set("client_id", clientId);
|
|
300
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
301
|
+
url.searchParams.set("response_type", "code");
|
|
302
|
+
url.searchParams.set("scope", scopes.join(" "));
|
|
303
|
+
if (state) {
|
|
304
|
+
url.searchParams.set("state", state);
|
|
305
|
+
}
|
|
306
|
+
return url.toString();
|
|
307
|
+
}
|
|
308
|
+
async function exchangeCodeForTokens(metadata, code, clientId, clientSecret, redirectUri) {
|
|
309
|
+
const response = await fetch(metadata.token_endpoint, {
|
|
310
|
+
method: "POST",
|
|
311
|
+
headers: {
|
|
312
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
313
|
+
"Authorization": `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`
|
|
314
|
+
},
|
|
315
|
+
body: new URLSearchParams({
|
|
316
|
+
grant_type: "authorization_code",
|
|
317
|
+
code,
|
|
318
|
+
redirect_uri: redirectUri
|
|
319
|
+
})
|
|
320
|
+
});
|
|
321
|
+
if (!response.ok) {
|
|
322
|
+
const error = await response.text();
|
|
323
|
+
throw new Error(`Token exchange failed: ${error}`);
|
|
324
|
+
}
|
|
325
|
+
return await response.json();
|
|
326
|
+
}
|
|
327
|
+
async function refreshAccessToken(metadata, refreshToken, clientId, clientSecret) {
|
|
328
|
+
const response = await fetch(metadata.token_endpoint, {
|
|
329
|
+
method: "POST",
|
|
330
|
+
headers: {
|
|
331
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
332
|
+
"Authorization": `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`
|
|
333
|
+
},
|
|
334
|
+
body: new URLSearchParams({
|
|
335
|
+
grant_type: "refresh_token",
|
|
336
|
+
refresh_token: refreshToken
|
|
337
|
+
})
|
|
338
|
+
});
|
|
339
|
+
if (!response.ok) {
|
|
340
|
+
throw new Error("Token refresh failed");
|
|
341
|
+
}
|
|
342
|
+
return await response.json();
|
|
343
|
+
}
|
|
344
|
+
async function validateIdToken(idToken, jwksUri, issuer, clientId, skipIssuerCheck = false) {
|
|
345
|
+
const JWKS = jose.createRemoteJWKSet(new URL(jwksUri));
|
|
346
|
+
const { payload } = await jose.jwtVerify(idToken, JWKS, {
|
|
347
|
+
issuer: skipIssuerCheck ? void 0 : issuer,
|
|
348
|
+
audience: clientId
|
|
349
|
+
});
|
|
350
|
+
return payload;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/oidc/providers/keycloak-adapter.ts
|
|
354
|
+
var KeycloakAdapter = class {
|
|
355
|
+
/**
|
|
356
|
+
* Normalize Keycloak issuer (remove /auth prefix if present)
|
|
357
|
+
*
|
|
358
|
+
* Keycloak's issuer URLs changed between versions:
|
|
359
|
+
* - Pre-17: https://keycloak.example.com/auth/realms/myrealm
|
|
360
|
+
* - Post-17: https://keycloak.example.com/realms/myrealm
|
|
361
|
+
*
|
|
362
|
+
* This normalizes to the post-17 format.
|
|
363
|
+
*
|
|
364
|
+
* @param issuer - Issuer from token or metadata
|
|
365
|
+
* @returns Normalized issuer
|
|
366
|
+
*/
|
|
367
|
+
static normalizeIssuer(issuer) {
|
|
368
|
+
return issuer.replace("/auth/realms/", "/realms/");
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Map Keycloak-specific claims to standard format
|
|
372
|
+
*
|
|
373
|
+
* Keycloak stores roles and groups in non-standard locations:
|
|
374
|
+
* - Roles: realm_access.roles (not standard)
|
|
375
|
+
* - Groups: groups array (sometimes, if configured)
|
|
376
|
+
* - Username: preferred_username (not always 'name')
|
|
377
|
+
*
|
|
378
|
+
* @param tokenPayload - Raw JWT payload from Keycloak
|
|
379
|
+
* @returns Normalized claims matching AuthUser interface
|
|
380
|
+
*/
|
|
381
|
+
static mapClaims(tokenPayload) {
|
|
382
|
+
return {
|
|
383
|
+
user_id: tokenPayload.sub,
|
|
384
|
+
email: tokenPayload.email,
|
|
385
|
+
name: tokenPayload.name || tokenPayload.preferred_username,
|
|
386
|
+
roles: tokenPayload.realm_access?.roles || [],
|
|
387
|
+
groups: tokenPayload.groups || [],
|
|
388
|
+
// Keep original payload for advanced use cases
|
|
389
|
+
...tokenPayload
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Keycloak refresh tokens should be refreshed earlier than spec suggests
|
|
394
|
+
*
|
|
395
|
+
* Keycloak's refresh token rotation is buggy and sometimes fails if you
|
|
396
|
+
* wait too long. Refresh aggressively when less than 10 minutes remain.
|
|
397
|
+
*
|
|
398
|
+
* @param expiresIn - Seconds until token expires
|
|
399
|
+
* @returns true if token should be refreshed now
|
|
400
|
+
*/
|
|
401
|
+
static shouldRefreshToken(expiresIn) {
|
|
402
|
+
return expiresIn < 600;
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
// src/providers/oidc-provider.ts
|
|
407
|
+
var OIDCProvider = class {
|
|
408
|
+
constructor(config) {
|
|
409
|
+
this.config = config;
|
|
410
|
+
}
|
|
411
|
+
metadata = null;
|
|
412
|
+
/**
|
|
413
|
+
* Initialize provider by discovering OIDC configuration
|
|
414
|
+
*
|
|
415
|
+
* Call this during app startup to pre-fetch OIDC metadata.
|
|
416
|
+
* If not called, metadata will be lazily loaded on first use.
|
|
417
|
+
*/
|
|
418
|
+
async initialize() {
|
|
419
|
+
this.metadata = await discoverOIDC(this.config.discoveryUrl);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Get metadata (lazy load if not initialized)
|
|
423
|
+
*
|
|
424
|
+
* @returns OIDC metadata
|
|
425
|
+
*/
|
|
426
|
+
async getMetadata() {
|
|
427
|
+
if (!this.metadata) {
|
|
428
|
+
await this.initialize();
|
|
429
|
+
}
|
|
430
|
+
return this.metadata;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Authenticate user by exchanging authorization code for tokens
|
|
434
|
+
*
|
|
435
|
+
* This is called after the user is redirected back from the OIDC provider
|
|
436
|
+
* with an authorization code in the query parameters.
|
|
437
|
+
*
|
|
438
|
+
* @param context - Auth context with query params containing authorization code
|
|
439
|
+
* @returns Authenticated user or null if no code present
|
|
440
|
+
* @throws Error if token exchange or validation fails
|
|
441
|
+
*/
|
|
442
|
+
async authenticate(context) {
|
|
443
|
+
const code = context.query?.code;
|
|
444
|
+
const redirectUri = this.config.redirectUri || "/api/auth/callback";
|
|
445
|
+
if (!code) {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
const metadata = await this.getMetadata();
|
|
449
|
+
const tokens = await exchangeCodeForTokens(
|
|
450
|
+
metadata,
|
|
451
|
+
code,
|
|
452
|
+
this.config.clientId,
|
|
453
|
+
this.config.clientSecret,
|
|
454
|
+
redirectUri
|
|
455
|
+
);
|
|
456
|
+
const issuer = this.config.quirks?.skipIssuerCheck ? void 0 : this.config.provider === "keycloak" ? KeycloakAdapter.normalizeIssuer(metadata.issuer) : metadata.issuer;
|
|
457
|
+
const claims = await validateIdToken(
|
|
458
|
+
tokens.id_token,
|
|
459
|
+
metadata.jwks_uri,
|
|
460
|
+
issuer || metadata.issuer,
|
|
461
|
+
this.config.clientId,
|
|
462
|
+
this.config.quirks?.skipIssuerCheck
|
|
463
|
+
);
|
|
464
|
+
const mappedClaims = this.config.provider === "keycloak" ? KeycloakAdapter.mapClaims(claims) : claims;
|
|
465
|
+
const user = {
|
|
466
|
+
id: this.getClaimValue(mappedClaims, this.config.claimsMapping?.user_id, "sub"),
|
|
467
|
+
email: this.getClaimValue(mappedClaims, this.config.claimsMapping?.email, "email"),
|
|
468
|
+
name: this.getClaimValue(mappedClaims, this.config.claimsMapping?.name, "name"),
|
|
469
|
+
roles: this.extractRoles(mappedClaims),
|
|
470
|
+
metadata: {
|
|
471
|
+
provider: this.config.provider,
|
|
472
|
+
originalClaims: mappedClaims
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
return user;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Validate session (check if token is still valid)
|
|
479
|
+
*
|
|
480
|
+
* Simple time-based validation. For more security, you could:
|
|
481
|
+
* - Call the userinfo endpoint
|
|
482
|
+
* - Verify token hasn't been revoked
|
|
483
|
+
* - Check against a session store
|
|
484
|
+
*
|
|
485
|
+
* @param session - Session to validate
|
|
486
|
+
* @returns true if session is still valid
|
|
487
|
+
*/
|
|
488
|
+
async validate(session) {
|
|
489
|
+
if (Date.now() > session.expiresAt) {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Refresh session using refresh token
|
|
496
|
+
*
|
|
497
|
+
* When the access token expires, use the refresh token to get new tokens
|
|
498
|
+
* without requiring the user to re-authenticate.
|
|
499
|
+
*
|
|
500
|
+
* @param session - Session with refresh token
|
|
501
|
+
* @returns Updated session with new tokens, or null if refresh failed
|
|
502
|
+
*/
|
|
503
|
+
async refresh(session) {
|
|
504
|
+
if (!session.refreshToken) {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
const metadata = await this.getMetadata();
|
|
508
|
+
try {
|
|
509
|
+
const tokens = await refreshAccessToken(
|
|
510
|
+
metadata,
|
|
511
|
+
session.refreshToken,
|
|
512
|
+
this.config.clientId,
|
|
513
|
+
this.config.clientSecret
|
|
514
|
+
);
|
|
515
|
+
return {
|
|
516
|
+
...session,
|
|
517
|
+
expiresAt: Date.now() + tokens.expires_in * 1e3,
|
|
518
|
+
refreshToken: tokens.refresh_token || session.refreshToken
|
|
519
|
+
};
|
|
520
|
+
} catch (error) {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Get authorization URL for initiating OIDC login
|
|
526
|
+
*
|
|
527
|
+
* Redirect users to this URL to start the authentication flow.
|
|
528
|
+
*
|
|
529
|
+
* @param redirectUri - Where to redirect after authentication
|
|
530
|
+
* @param state - CSRF protection token (recommended)
|
|
531
|
+
* @returns Authorization URL
|
|
532
|
+
*/
|
|
533
|
+
async getAuthorizationUrl(redirectUri, state) {
|
|
534
|
+
const metadata = await this.getMetadata();
|
|
535
|
+
return buildAuthorizationUrl(metadata, this.config.clientId, redirectUri, state);
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Extract claim value with fallback
|
|
539
|
+
*
|
|
540
|
+
* Tries custom mapped key first, then falls back to default key.
|
|
541
|
+
*
|
|
542
|
+
* @param claims - JWT claims
|
|
543
|
+
* @param mappedKey - Custom mapped key from config
|
|
544
|
+
* @param defaultKey - Default OIDC key
|
|
545
|
+
* @returns Claim value or undefined
|
|
546
|
+
*/
|
|
547
|
+
getClaimValue(claims, mappedKey, defaultKey) {
|
|
548
|
+
if (mappedKey && claims[mappedKey]) {
|
|
549
|
+
return claims[mappedKey];
|
|
550
|
+
}
|
|
551
|
+
if (defaultKey && claims[defaultKey]) {
|
|
552
|
+
return claims[defaultKey];
|
|
553
|
+
}
|
|
554
|
+
return void 0;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Extract roles from claims (provider-specific logic)
|
|
558
|
+
*
|
|
559
|
+
* Different OIDC providers store roles in different places:
|
|
560
|
+
* - Standard: 'roles' claim
|
|
561
|
+
* - Keycloak: realm_access.roles
|
|
562
|
+
* - Cognito: cognito:groups
|
|
563
|
+
* - Azure AD: roles claim
|
|
564
|
+
*
|
|
565
|
+
* @param claims - JWT claims
|
|
566
|
+
* @returns Array of role strings
|
|
567
|
+
*/
|
|
568
|
+
extractRoles(claims) {
|
|
569
|
+
if (this.config.claimsMapping?.roles) {
|
|
570
|
+
const rolesValue = claims[this.config.claimsMapping.roles];
|
|
571
|
+
if (Array.isArray(rolesValue)) {
|
|
572
|
+
return rolesValue;
|
|
573
|
+
}
|
|
574
|
+
if (typeof rolesValue === "string") {
|
|
575
|
+
return [rolesValue];
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (Array.isArray(claims.roles)) {
|
|
579
|
+
return claims.roles;
|
|
580
|
+
}
|
|
581
|
+
if (claims.realm_access?.roles) {
|
|
582
|
+
return claims.realm_access.roles;
|
|
583
|
+
}
|
|
584
|
+
if (claims["cognito:groups"]) {
|
|
585
|
+
return claims["cognito:groups"];
|
|
586
|
+
}
|
|
587
|
+
return [];
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
var SessionManager = class {
|
|
591
|
+
secret;
|
|
592
|
+
sessionDuration;
|
|
593
|
+
algorithm;
|
|
594
|
+
issuer;
|
|
595
|
+
audience;
|
|
596
|
+
constructor(config) {
|
|
597
|
+
if (config.secret.length < 32) {
|
|
598
|
+
throw new Error("Session secret must be at least 32 characters");
|
|
599
|
+
}
|
|
600
|
+
this.secret = new TextEncoder().encode(config.secret);
|
|
601
|
+
this.sessionDuration = config.sessionDuration || 900;
|
|
602
|
+
this.algorithm = config.algorithm || "HS256";
|
|
603
|
+
this.issuer = config.issuer;
|
|
604
|
+
this.audience = config.audience;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Create a new session for authenticated user
|
|
608
|
+
*/
|
|
609
|
+
async createSession(user, refreshToken) {
|
|
610
|
+
const now = Date.now();
|
|
611
|
+
const expiresAt = now + this.sessionDuration * 1e3;
|
|
612
|
+
const session = {
|
|
613
|
+
user,
|
|
614
|
+
issuedAt: now,
|
|
615
|
+
expiresAt,
|
|
616
|
+
refreshToken
|
|
617
|
+
};
|
|
618
|
+
return session;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Sign session into a JWT
|
|
622
|
+
*/
|
|
623
|
+
async signSession(session) {
|
|
624
|
+
const jwt = await new jose.SignJWT({
|
|
625
|
+
user: session.user,
|
|
626
|
+
refreshToken: session.refreshToken
|
|
627
|
+
}).setProtectedHeader({ alg: this.algorithm }).setIssuedAt(session.issuedAt / 1e3).setExpirationTime(session.expiresAt / 1e3).setIssuer(this.issuer || "stackwright-auth").setAudience(this.audience || "stackwright-app").sign(this.secret);
|
|
628
|
+
return jwt;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Verify and decode session JWT
|
|
632
|
+
*/
|
|
633
|
+
async verifySession(jwt) {
|
|
634
|
+
try {
|
|
635
|
+
const { payload } = await jose.jwtVerify(jwt, this.secret, {
|
|
636
|
+
issuer: this.issuer || "stackwright-auth",
|
|
637
|
+
audience: this.audience || "stackwright-app"
|
|
638
|
+
});
|
|
639
|
+
const session = {
|
|
640
|
+
user: payload.user,
|
|
641
|
+
issuedAt: (payload.iat || 0) * 1e3,
|
|
642
|
+
// Convert back to milliseconds
|
|
643
|
+
expiresAt: (payload.exp || 0) * 1e3,
|
|
644
|
+
refreshToken: payload.refreshToken
|
|
645
|
+
};
|
|
646
|
+
return session;
|
|
647
|
+
} catch (error) {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Check if session is expired
|
|
653
|
+
*/
|
|
654
|
+
isExpired(session) {
|
|
655
|
+
return Date.now() > session.expiresAt;
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Check if session should be refreshed (within 5 minutes of expiry)
|
|
659
|
+
*/
|
|
660
|
+
shouldRefresh(session) {
|
|
661
|
+
const timeUntilExpiry = session.expiresAt - Date.now();
|
|
662
|
+
const REFRESH_THRESHOLD = 5 * 60 * 1e3;
|
|
663
|
+
return timeUntilExpiry < REFRESH_THRESHOLD && timeUntilExpiry > 0;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Refresh session (extend expiration)
|
|
667
|
+
*/
|
|
668
|
+
async refreshSession(session) {
|
|
669
|
+
return {
|
|
670
|
+
...session,
|
|
671
|
+
issuedAt: Date.now(),
|
|
672
|
+
expiresAt: Date.now() + this.sessionDuration * 1e3
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Serialize session to string (for cookies/localStorage)
|
|
677
|
+
*/
|
|
678
|
+
async serialize(session) {
|
|
679
|
+
return this.signSession(session);
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Deserialize session from string
|
|
683
|
+
*/
|
|
684
|
+
async deserialize(serialized) {
|
|
685
|
+
return this.verifySession(serialized);
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
// src/session/cookie-helpers.ts
|
|
690
|
+
function serializeCookie(name, value, options = {}) {
|
|
691
|
+
const {
|
|
692
|
+
domain,
|
|
693
|
+
path = "/",
|
|
694
|
+
maxAge,
|
|
695
|
+
httpOnly = true,
|
|
696
|
+
secure = process.env.NODE_ENV === "production",
|
|
697
|
+
sameSite = "lax"
|
|
698
|
+
} = options;
|
|
699
|
+
const parts = [
|
|
700
|
+
`${name}=${encodeURIComponent(value)}`
|
|
701
|
+
];
|
|
702
|
+
if (domain) {
|
|
703
|
+
parts.push(`Domain=${domain}`);
|
|
704
|
+
}
|
|
705
|
+
parts.push(`Path=${path}`);
|
|
706
|
+
if (maxAge !== void 0) {
|
|
707
|
+
parts.push(`Max-Age=${maxAge}`);
|
|
708
|
+
}
|
|
709
|
+
if (httpOnly) {
|
|
710
|
+
parts.push("HttpOnly");
|
|
711
|
+
}
|
|
712
|
+
if (secure) {
|
|
713
|
+
parts.push("Secure");
|
|
714
|
+
}
|
|
715
|
+
if (sameSite) {
|
|
716
|
+
parts.push(`SameSite=${sameSite.charAt(0).toUpperCase() + sameSite.slice(1)}`);
|
|
717
|
+
}
|
|
718
|
+
return parts.join("; ");
|
|
719
|
+
}
|
|
720
|
+
function parseCookies(cookieHeader) {
|
|
721
|
+
if (!cookieHeader) {
|
|
722
|
+
return {};
|
|
723
|
+
}
|
|
724
|
+
const cookies = {};
|
|
725
|
+
for (const cookie of cookieHeader.split(";")) {
|
|
726
|
+
const [key, ...valueParts] = cookie.trim().split("=");
|
|
727
|
+
if (key) {
|
|
728
|
+
cookies[key] = decodeURIComponent(valueParts.join("="));
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return cookies;
|
|
732
|
+
}
|
|
733
|
+
function clearCookie(name, options = {}) {
|
|
734
|
+
return serializeCookie(name, "", {
|
|
735
|
+
...options,
|
|
736
|
+
maxAge: 0
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// src/rbac/rbac-engine.ts
|
|
741
|
+
var RBACEngine = class {
|
|
742
|
+
config;
|
|
743
|
+
rolePermissions;
|
|
744
|
+
constructor(config) {
|
|
745
|
+
this.config = config;
|
|
746
|
+
this.rolePermissions = this.buildRolePermissionMap();
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Build internal map of role → permissions for fast lookups
|
|
750
|
+
*/
|
|
751
|
+
buildRolePermissionMap() {
|
|
752
|
+
const map = /* @__PURE__ */ new Map();
|
|
753
|
+
for (const role of this.config.roles) {
|
|
754
|
+
map.set(role.name, new Set(role.permissions || []));
|
|
755
|
+
}
|
|
756
|
+
return map;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Check if user has a specific role
|
|
760
|
+
*/
|
|
761
|
+
hasRole(user, role) {
|
|
762
|
+
return user.roles.includes(role);
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Check if user has any of the specified roles
|
|
766
|
+
*/
|
|
767
|
+
hasAnyRole(user, roles) {
|
|
768
|
+
return roles.some((role) => this.hasRole(user, role));
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Check if user has all of the specified roles
|
|
772
|
+
*/
|
|
773
|
+
hasAllRoles(user, roles) {
|
|
774
|
+
return roles.every((role) => this.hasRole(user, role));
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Check if user has a specific permission
|
|
778
|
+
* Permissions are checked both:
|
|
779
|
+
* 1. Directly in user.permissions array
|
|
780
|
+
* 2. Through role-based permissions from config
|
|
781
|
+
*/
|
|
782
|
+
hasPermission(user, permission) {
|
|
783
|
+
if (user.permissions?.includes(permission)) {
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
for (const role of user.roles) {
|
|
787
|
+
const permissions = this.rolePermissions.get(role);
|
|
788
|
+
if (permissions?.has(permission)) {
|
|
789
|
+
return true;
|
|
790
|
+
}
|
|
791
|
+
if (permissions) {
|
|
792
|
+
for (const p of permissions) {
|
|
793
|
+
if (p.endsWith(":*")) {
|
|
794
|
+
const prefix = p.slice(0, -1);
|
|
795
|
+
if (permission.startsWith(prefix)) {
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Check if user has any of the specified permissions
|
|
806
|
+
*/
|
|
807
|
+
hasAnyPermission(user, permissions) {
|
|
808
|
+
return permissions.some((permission) => this.hasPermission(user, permission));
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Check if user has all of the specified permissions
|
|
812
|
+
*/
|
|
813
|
+
hasAllPermissions(user, permissions) {
|
|
814
|
+
return permissions.every((permission) => this.hasPermission(user, permission));
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Check if route is public (no auth required)
|
|
818
|
+
*/
|
|
819
|
+
isPublicRoute(path) {
|
|
820
|
+
if (!this.config.public_routes) {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
return this.config.public_routes.some((publicPath) => {
|
|
824
|
+
return this.matchPath(path, publicPath);
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Check if user can access a route based on protected_routes config
|
|
829
|
+
*/
|
|
830
|
+
canAccessRoute(user, path) {
|
|
831
|
+
if (this.isPublicRoute(path)) {
|
|
832
|
+
return true;
|
|
833
|
+
}
|
|
834
|
+
if (!user) {
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
if (!this.config.protected_routes || this.config.protected_routes.length === 0) {
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
const matchingRoute = this.config.protected_routes.find((route) => {
|
|
841
|
+
return this.matchPath(path, route.path);
|
|
842
|
+
});
|
|
843
|
+
if (!matchingRoute) {
|
|
844
|
+
return true;
|
|
845
|
+
}
|
|
846
|
+
return this.hasAnyRole(user, matchingRoute.roles);
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Check if user can access a component based on component auth config
|
|
850
|
+
*/
|
|
851
|
+
canAccessComponent(user, authConfig) {
|
|
852
|
+
if (!authConfig) {
|
|
853
|
+
return true;
|
|
854
|
+
}
|
|
855
|
+
if (!user) {
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
if (authConfig.required_roles && authConfig.required_roles.length > 0) {
|
|
859
|
+
if (!this.hasAnyRole(user, authConfig.required_roles)) {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (authConfig.required_permissions && authConfig.required_permissions.length > 0) {
|
|
864
|
+
if (!this.hasAllPermissions(user, authConfig.required_permissions)) {
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return true;
|
|
869
|
+
}
|
|
870
|
+
// Match a path against a pattern with wildcard support
|
|
871
|
+
matchPath(path, pattern) {
|
|
872
|
+
if (path === pattern) {
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
if (!pattern.includes("*")) {
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
const regexPattern = pattern.replace(/\*/g, ".*").replace(/\//g, "\\/");
|
|
879
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
880
|
+
return regex.test(path);
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
var AuthContext = createContext(null);
|
|
884
|
+
function useAuth() {
|
|
885
|
+
const context = useContext(AuthContext);
|
|
886
|
+
if (!context) {
|
|
887
|
+
throw new Error("useAuth must be used within AuthProvider");
|
|
888
|
+
}
|
|
889
|
+
return context;
|
|
890
|
+
}
|
|
891
|
+
function useRequireAuth() {
|
|
892
|
+
const auth = useAuth();
|
|
893
|
+
if (!auth.isAuthenticated) {
|
|
894
|
+
console.warn("useRequireAuth: User is not authenticated");
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
return auth;
|
|
898
|
+
}
|
|
899
|
+
function AuthProvider({
|
|
900
|
+
user,
|
|
901
|
+
session,
|
|
902
|
+
rbacConfig,
|
|
903
|
+
isLoading = false,
|
|
904
|
+
children
|
|
905
|
+
}) {
|
|
906
|
+
const rbac = useMemo(() => new RBACEngine(rbacConfig), [rbacConfig]);
|
|
907
|
+
const value = useMemo(() => ({
|
|
908
|
+
user,
|
|
909
|
+
session,
|
|
910
|
+
isAuthenticated: user !== null,
|
|
911
|
+
isLoading,
|
|
912
|
+
hasRole: (role) => {
|
|
913
|
+
if (!user) return false;
|
|
914
|
+
return rbac.hasRole(user, role);
|
|
915
|
+
},
|
|
916
|
+
hasPermission: (permission) => {
|
|
917
|
+
if (!user) return false;
|
|
918
|
+
return rbac.hasPermission(user, permission);
|
|
919
|
+
},
|
|
920
|
+
hasAnyRole: (roles) => {
|
|
921
|
+
if (!user) return false;
|
|
922
|
+
return rbac.hasAnyRole(user, roles);
|
|
923
|
+
},
|
|
924
|
+
hasAllPermissions: (permissions) => {
|
|
925
|
+
if (!user) return false;
|
|
926
|
+
return rbac.hasAllPermissions(user, permissions);
|
|
927
|
+
}
|
|
928
|
+
}), [user, session, isLoading, rbac]);
|
|
929
|
+
return /* @__PURE__ */ jsx(AuthContext.Provider, { value, children });
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// src/profiles/dod-cac.ts
|
|
933
|
+
var DOD_CAC_PROFILE = {
|
|
934
|
+
profile: "dod_cac",
|
|
935
|
+
source: "gateway_headers",
|
|
936
|
+
headerPrefix: "x-client-cert-",
|
|
937
|
+
verifiedHeader: "x-client-cert-verified",
|
|
938
|
+
requiredValue: "SUCCESS",
|
|
939
|
+
requiredOU: ["DOD", "DoD", "U.S. Government"],
|
|
940
|
+
// DoD Root CAs (add current list)
|
|
941
|
+
// These are sample CA names - verify against current DoD PKI infrastructure
|
|
942
|
+
allowedIssuers: [
|
|
943
|
+
// DoD Interoperability Root CA 2
|
|
944
|
+
"CN=DoD Interoperability Root CA 2",
|
|
945
|
+
// DoD Root CA 3-6 (current generation)
|
|
946
|
+
"CN=DoD Root CA 3",
|
|
947
|
+
"CN=DoD Root CA 4",
|
|
948
|
+
"CN=DoD Root CA 5",
|
|
949
|
+
"CN=DoD Root CA 6",
|
|
950
|
+
// DoD ID CAs (email/PIV auth)
|
|
951
|
+
"CN=DOD ID CA-59",
|
|
952
|
+
"CN=DOD ID CA-60",
|
|
953
|
+
"CN=DOD ID CA-61",
|
|
954
|
+
"CN=DOD ID CA-62",
|
|
955
|
+
"CN=DOD ID CA-63",
|
|
956
|
+
"CN=DOD ID CA-64",
|
|
957
|
+
"CN=DOD ID CA-65",
|
|
958
|
+
"CN=DOD ID CA-66",
|
|
959
|
+
"CN=DOD ID CA-67",
|
|
960
|
+
"CN=DOD ID CA-68",
|
|
961
|
+
"CN=DOD ID CA-69",
|
|
962
|
+
"CN=DOD ID CA-70",
|
|
963
|
+
// DoD SW CAs (software/device auth)
|
|
964
|
+
"CN=DOD SW CA-59",
|
|
965
|
+
"CN=DOD SW CA-60",
|
|
966
|
+
"CN=DOD SW CA-61",
|
|
967
|
+
"CN=DOD SW CA-62",
|
|
968
|
+
"CN=DOD SW CA-63",
|
|
969
|
+
"CN=DOD SW CA-64",
|
|
970
|
+
"CN=DOD SW CA-65",
|
|
971
|
+
"CN=DOD SW CA-66",
|
|
972
|
+
// Legacy DoD Email CAs (being phased out but may still be in use)
|
|
973
|
+
"CN=DOD EMAIL CA-59",
|
|
974
|
+
"CN=DOD EMAIL CA-60",
|
|
975
|
+
"CN=DOD EMAIL CA-61",
|
|
976
|
+
"CN=DOD EMAIL CA-62",
|
|
977
|
+
"CN=DOD EMAIL CA-63",
|
|
978
|
+
"CN=DOD EMAIL CA-64",
|
|
979
|
+
"CN=DOD EMAIL CA-65",
|
|
980
|
+
"CN=DOD EMAIL CA-66"
|
|
981
|
+
]
|
|
982
|
+
};
|
|
983
|
+
function createDoDCACConfig(overrides) {
|
|
984
|
+
return {
|
|
985
|
+
type: "pki",
|
|
986
|
+
...DOD_CAC_PROFILE,
|
|
987
|
+
...overrides
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
function createDoDCACDevConfig() {
|
|
991
|
+
return {
|
|
992
|
+
type: "pki",
|
|
993
|
+
profile: "dod_cac",
|
|
994
|
+
source: "gateway_headers",
|
|
995
|
+
headerPrefix: "x-client-cert-",
|
|
996
|
+
verifiedHeader: "x-client-cert-verified",
|
|
997
|
+
requiredValue: "SUCCESS",
|
|
998
|
+
// Only require 'DOD' in OU for dev
|
|
999
|
+
requiredOU: ["DOD"],
|
|
1000
|
+
// Don't restrict issuers in dev mode
|
|
1001
|
+
allowedIssuers: void 0
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
var FallbackComponents = {
|
|
1005
|
+
/**
|
|
1006
|
+
* Hide component (render nothing)
|
|
1007
|
+
*/
|
|
1008
|
+
hide: () => null,
|
|
1009
|
+
/**
|
|
1010
|
+
* Show placeholder message
|
|
1011
|
+
*/
|
|
1012
|
+
placeholder: ({ className }) => /* @__PURE__ */ jsx("div", { className: className || "auth-placeholder", style: {
|
|
1013
|
+
padding: "1rem",
|
|
1014
|
+
border: "1px dashed #ccc",
|
|
1015
|
+
borderRadius: "4px",
|
|
1016
|
+
color: "#666",
|
|
1017
|
+
fontStyle: "italic",
|
|
1018
|
+
textAlign: "center"
|
|
1019
|
+
}, children: "Content requires authorization" }),
|
|
1020
|
+
/**
|
|
1021
|
+
* Show custom message
|
|
1022
|
+
*/
|
|
1023
|
+
message: ({ message, className }) => /* @__PURE__ */ jsx("div", { className: className || "auth-message", style: {
|
|
1024
|
+
padding: "1rem",
|
|
1025
|
+
border: "1px solid #f0ad4e",
|
|
1026
|
+
borderRadius: "4px",
|
|
1027
|
+
backgroundColor: "#fcf8e3",
|
|
1028
|
+
color: "#8a6d3b"
|
|
1029
|
+
}, children: message || "Unauthorized" })
|
|
1030
|
+
};
|
|
1031
|
+
function withAuth(Component, authConfig) {
|
|
1032
|
+
if (!authConfig) {
|
|
1033
|
+
return Component;
|
|
1034
|
+
}
|
|
1035
|
+
const WrappedComponent = (props) => {
|
|
1036
|
+
const auth = useAuth();
|
|
1037
|
+
if (authConfig.required_roles && authConfig.required_roles.length > 0) {
|
|
1038
|
+
if (!auth.hasAnyRole(authConfig.required_roles)) {
|
|
1039
|
+
return renderFallback(authConfig);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
if (authConfig.required_permissions && authConfig.required_permissions.length > 0) {
|
|
1043
|
+
if (!auth.hasAllPermissions(authConfig.required_permissions)) {
|
|
1044
|
+
return renderFallback(authConfig);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return /* @__PURE__ */ jsx(Component, { ...props });
|
|
1048
|
+
};
|
|
1049
|
+
const componentName = Component.displayName || Component.name || "Component";
|
|
1050
|
+
WrappedComponent.displayName = `withAuth(${componentName})`;
|
|
1051
|
+
return WrappedComponent;
|
|
1052
|
+
}
|
|
1053
|
+
function renderFallback(authConfig) {
|
|
1054
|
+
const fallbackType = authConfig.fallback || "hide";
|
|
1055
|
+
switch (fallbackType) {
|
|
1056
|
+
case "hide":
|
|
1057
|
+
return FallbackComponents.hide();
|
|
1058
|
+
case "placeholder":
|
|
1059
|
+
return FallbackComponents.placeholder({});
|
|
1060
|
+
case "message":
|
|
1061
|
+
return FallbackComponents.message({
|
|
1062
|
+
message: authConfig.fallback_message
|
|
1063
|
+
});
|
|
1064
|
+
default:
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
function withAuthFallback(Component, authConfig, FallbackComponent) {
|
|
1069
|
+
const WrappedComponent = (props) => {
|
|
1070
|
+
const auth = useAuth();
|
|
1071
|
+
const isAuthorized = checkAuthorization(auth, authConfig);
|
|
1072
|
+
if (!isAuthorized) {
|
|
1073
|
+
return /* @__PURE__ */ jsx(FallbackComponent, {});
|
|
1074
|
+
}
|
|
1075
|
+
return /* @__PURE__ */ jsx(Component, { ...props });
|
|
1076
|
+
};
|
|
1077
|
+
const componentName = Component.displayName || Component.name || "Component";
|
|
1078
|
+
WrappedComponent.displayName = `withAuthFallback(${componentName})`;
|
|
1079
|
+
return WrappedComponent;
|
|
1080
|
+
}
|
|
1081
|
+
function checkAuthorization(auth, authConfig) {
|
|
1082
|
+
if (authConfig.required_roles && authConfig.required_roles.length > 0) {
|
|
1083
|
+
if (!auth.hasAnyRole(authConfig.required_roles)) {
|
|
1084
|
+
return false;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
if (authConfig.required_permissions && authConfig.required_permissions.length > 0) {
|
|
1088
|
+
if (!auth.hasAllPermissions(authConfig.required_permissions)) {
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return true;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// src/registration.ts
|
|
1096
|
+
var authDecoratorRegistry = {
|
|
1097
|
+
decorator: null
|
|
1098
|
+
};
|
|
1099
|
+
function registerAuthDecorator() {
|
|
1100
|
+
authDecoratorRegistry.decorator = withAuth;
|
|
1101
|
+
if (typeof window !== "undefined" && window.__STACKWRIGHT_DEBUG__) {
|
|
1102
|
+
console.log("\u{1F510} Auth decorator registered");
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
function getAuthDecorator() {
|
|
1106
|
+
return authDecoratorRegistry.decorator;
|
|
1107
|
+
}
|
|
1108
|
+
function maybeWrapWithAuth(Component, authConfig) {
|
|
1109
|
+
const decorator = getAuthDecorator();
|
|
1110
|
+
if (!decorator || !authConfig) {
|
|
1111
|
+
return Component;
|
|
1112
|
+
}
|
|
1113
|
+
return decorator(Component, authConfig);
|
|
1114
|
+
}
|
|
1115
|
+
function hasAuthConfig(item) {
|
|
1116
|
+
if (!item || typeof item !== "object") {
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
return "auth" in item;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
export { AuthContext, AuthProvider, DOD_CAC_PROFILE, KeycloakAdapter, OIDCProvider, PKIProvider, RBACEngine, SessionManager, authConfigSchema, authSessionSchema, authUserSchema, buildAuthorizationUrl, clearCookie, componentAuthSchema, createDoDCACConfig, createDoDCACDevConfig, discoverOIDC, exchangeCodeForTokens, extractEDIPI, getAuthDecorator, hasAuthConfig, maybeWrapWithAuth, oidcConfigSchema, parseCertFromHeaders, parseCertificate, parseCookies, pkiConfigSchema, rbacConfigSchema, refreshAccessToken, registerAuthDecorator, serializeCookie, useAuth, useRequireAuth, validateDoDCAC, validateIdToken, withAuth, withAuthFallback };
|
|
1123
|
+
//# sourceMappingURL=index.mjs.map
|
|
1124
|
+
//# sourceMappingURL=index.mjs.map
|