@happyvertical/auth 0.74.8
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/AGENT.md +33 -0
- package/LICENSE +7 -0
- package/README.md +73 -0
- package/dist/chunks/cognito-dmypylFX.js +128 -0
- package/dist/chunks/cognito-dmypylFX.js.map +1 -0
- package/dist/chunks/decode_jwt-D2OK1b8a.js +1395 -0
- package/dist/chunks/decode_jwt-D2OK1b8a.js.map +1 -0
- package/dist/chunks/github-NSZp5tVm.js +413 -0
- package/dist/chunks/github-NSZp5tVm.js.map +1 -0
- package/dist/chunks/google-HXk2ctYR.js +483 -0
- package/dist/chunks/google-HXk2ctYR.js.map +1 -0
- package/dist/chunks/index-BpsMhFXS.js +151 -0
- package/dist/chunks/index-BpsMhFXS.js.map +1 -0
- package/dist/chunks/kanidm-hkw-YPVF.js +747 -0
- package/dist/chunks/kanidm-hkw-YPVF.js.map +1 -0
- package/dist/chunks/keycloak-t6JEUeOz.js +871 -0
- package/dist/chunks/keycloak-t6JEUeOz.js.map +1 -0
- package/dist/cli/claude-context.d.ts +3 -0
- package/dist/cli/claude-context.d.ts.map +1 -0
- package/dist/cli/claude-context.js +21 -0
- package/dist/cli/claude-context.js.map +1 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +499 -0
- package/dist/index.js.map +1 -0
- package/dist/shared/errors.d.ts +227 -0
- package/dist/shared/errors.d.ts.map +1 -0
- package/dist/shared/factory.d.ts +85 -0
- package/dist/shared/factory.d.ts.map +1 -0
- package/dist/shared/providers/cognito.d.ts +38 -0
- package/dist/shared/providers/cognito.d.ts.map +1 -0
- package/dist/shared/providers/github.d.ts +65 -0
- package/dist/shared/providers/github.d.ts.map +1 -0
- package/dist/shared/providers/google.d.ts +58 -0
- package/dist/shared/providers/google.d.ts.map +1 -0
- package/dist/shared/providers/kanidm.d.ts +78 -0
- package/dist/shared/providers/kanidm.d.ts.map +1 -0
- package/dist/shared/providers/keycloak.d.ts +67 -0
- package/dist/shared/providers/keycloak.d.ts.map +1 -0
- package/dist/shared/providers/nostr/index.d.ts +47 -0
- package/dist/shared/providers/nostr/index.d.ts.map +1 -0
- package/dist/shared/types.d.ts +812 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/metadata.json +32 -0
- package/package.json +60 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
import { ConfigurationError, NetworkError, InvalidCredentialsError, AccessDeniedError, InvalidGrantError, InvalidClientError, UserNotFoundError, ProviderError, UserAlreadyExistsError, NotImplementedError, InvalidNonceError, TokenExpiredError, InvalidTokenError } from "../index.js";
|
|
2
|
+
import { c as createRemoteJWKSet, d as decodeJwt, j as jwtVerify, J as JWTExpired, a as JWTClaimValidationFailed, b as JWSSignatureVerificationFailed } from "./decode_jwt-D2OK1b8a.js";
|
|
3
|
+
function generateRandomString(length = 32) {
|
|
4
|
+
const array = new Uint8Array(length);
|
|
5
|
+
crypto.getRandomValues(array);
|
|
6
|
+
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
|
|
7
|
+
""
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
async function generatePKCE() {
|
|
11
|
+
const verifier = generateRandomString(32);
|
|
12
|
+
const encoder = new TextEncoder();
|
|
13
|
+
const data = encoder.encode(verifier);
|
|
14
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
15
|
+
const challenge = btoa(String.fromCharCode(...new Uint8Array(hash))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
16
|
+
return { verifier, challenge };
|
|
17
|
+
}
|
|
18
|
+
class KanidmProvider {
|
|
19
|
+
options;
|
|
20
|
+
discoveryDocument = null;
|
|
21
|
+
jwks = null;
|
|
22
|
+
adminToken = null;
|
|
23
|
+
adminTokenExpiry = 0;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
if (!options.serverUrl) {
|
|
26
|
+
throw new ConfigurationError("serverUrl is required", "kanidm");
|
|
27
|
+
}
|
|
28
|
+
if (!options.clientId) {
|
|
29
|
+
throw new ConfigurationError("clientId is required", "kanidm");
|
|
30
|
+
}
|
|
31
|
+
this.options = {
|
|
32
|
+
usePKCE: true,
|
|
33
|
+
// Required by Kanidm
|
|
34
|
+
verifySsl: true,
|
|
35
|
+
scopes: ["openid", "profile", "email", "groups"],
|
|
36
|
+
timeout: 3e4,
|
|
37
|
+
maxRetries: 3,
|
|
38
|
+
...options
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// INTERNAL HELPERS
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
/**
|
|
45
|
+
* Get the OIDC base URL for this client.
|
|
46
|
+
* Kanidm uses client-specific OIDC endpoints.
|
|
47
|
+
*/
|
|
48
|
+
getOidcBaseUrl() {
|
|
49
|
+
return `${this.options.serverUrl}/oauth2/openid/${this.options.clientId}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get the native API base URL.
|
|
53
|
+
*/
|
|
54
|
+
getApiBaseUrl() {
|
|
55
|
+
return `${this.options.serverUrl}/v1`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Make an HTTP request with error handling.
|
|
59
|
+
*/
|
|
60
|
+
async request(url, options = {}, token) {
|
|
61
|
+
const headers = {
|
|
62
|
+
"Content-Type": "application/json",
|
|
63
|
+
...this.options.headers,
|
|
64
|
+
...options.headers
|
|
65
|
+
};
|
|
66
|
+
if (token) {
|
|
67
|
+
headers.Authorization = `Bearer ${token}`;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch(url, {
|
|
71
|
+
...options,
|
|
72
|
+
headers,
|
|
73
|
+
signal: AbortSignal.timeout(this.options.timeout || 3e4)
|
|
74
|
+
});
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
const errorBody = await response.text().catch(() => "");
|
|
77
|
+
let errorData = {};
|
|
78
|
+
try {
|
|
79
|
+
errorData = JSON.parse(errorBody);
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
this.handleHttpError(response.status, errorData, errorBody);
|
|
83
|
+
}
|
|
84
|
+
const text = await response.text();
|
|
85
|
+
if (!text) return {};
|
|
86
|
+
return JSON.parse(text);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error instanceof Error && error.name === "TimeoutError") {
|
|
89
|
+
throw new NetworkError("Request timed out", "kanidm", error);
|
|
90
|
+
}
|
|
91
|
+
if (error instanceof InvalidCredentialsError || error instanceof AccessDeniedError || error instanceof InvalidGrantError || error instanceof InvalidClientError || error instanceof UserNotFoundError || error instanceof ProviderError) {
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
throw new NetworkError(
|
|
95
|
+
`Network error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
96
|
+
"kanidm",
|
|
97
|
+
error instanceof Error ? error : void 0
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Handle HTTP error responses.
|
|
103
|
+
*/
|
|
104
|
+
handleHttpError(status, data, rawBody) {
|
|
105
|
+
const error = data.error;
|
|
106
|
+
const errorDescription = data.message || data.error_description || data.errorMessage || rawBody;
|
|
107
|
+
switch (status) {
|
|
108
|
+
case 400:
|
|
109
|
+
if (error === "invalid_grant") {
|
|
110
|
+
throw new InvalidGrantError(errorDescription, "kanidm");
|
|
111
|
+
}
|
|
112
|
+
if (error === "invalid_client") {
|
|
113
|
+
throw new InvalidClientError("kanidm");
|
|
114
|
+
}
|
|
115
|
+
throw new ProviderError(`Bad request: ${errorDescription}`, "kanidm");
|
|
116
|
+
case 401:
|
|
117
|
+
throw new InvalidCredentialsError("kanidm");
|
|
118
|
+
case 403:
|
|
119
|
+
throw new AccessDeniedError(errorDescription, "kanidm");
|
|
120
|
+
case 404:
|
|
121
|
+
throw new UserNotFoundError(void 0, "kanidm");
|
|
122
|
+
case 409:
|
|
123
|
+
throw new UserAlreadyExistsError(void 0, "kanidm");
|
|
124
|
+
default:
|
|
125
|
+
throw new ProviderError(
|
|
126
|
+
`Kanidm error (${status}): ${errorDescription}`,
|
|
127
|
+
"kanidm"
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Fetch and cache the OIDC discovery document.
|
|
133
|
+
*/
|
|
134
|
+
async fetchDiscoveryDocument() {
|
|
135
|
+
if (this.discoveryDocument) {
|
|
136
|
+
return this.discoveryDocument;
|
|
137
|
+
}
|
|
138
|
+
const url = `${this.getOidcBaseUrl()}/.well-known/openid-configuration`;
|
|
139
|
+
this.discoveryDocument = await this.request(url);
|
|
140
|
+
return this.discoveryDocument;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get JWKS for token validation.
|
|
144
|
+
*/
|
|
145
|
+
async getJWKS() {
|
|
146
|
+
if (this.jwks) {
|
|
147
|
+
return this.jwks;
|
|
148
|
+
}
|
|
149
|
+
const discovery = await this.fetchDiscoveryDocument();
|
|
150
|
+
this.jwks = createRemoteJWKSet(new URL(discovery.jwks_uri));
|
|
151
|
+
return this.jwks;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Authenticate with Kanidm's native API to get an admin token.
|
|
155
|
+
* Uses the multi-step authentication flow.
|
|
156
|
+
*/
|
|
157
|
+
async getAdminToken() {
|
|
158
|
+
if (this.adminToken && Date.now() < this.adminTokenExpiry) {
|
|
159
|
+
return this.adminToken;
|
|
160
|
+
}
|
|
161
|
+
if (!this.options.adminUsername || !this.options.adminPassword) {
|
|
162
|
+
throw new ConfigurationError(
|
|
163
|
+
"adminUsername and adminPassword are required for admin operations",
|
|
164
|
+
"kanidm"
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const authUrl = `${this.getApiBaseUrl()}/auth`;
|
|
168
|
+
const initResponse = await fetch(authUrl, {
|
|
169
|
+
method: "POST",
|
|
170
|
+
headers: { "Content-Type": "application/json" },
|
|
171
|
+
body: JSON.stringify({
|
|
172
|
+
step: {
|
|
173
|
+
init2: {
|
|
174
|
+
username: this.options.adminUsername,
|
|
175
|
+
issue: "token",
|
|
176
|
+
privileged: true
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}),
|
|
180
|
+
signal: AbortSignal.timeout(this.options.timeout || 3e4)
|
|
181
|
+
});
|
|
182
|
+
if (!initResponse.ok) {
|
|
183
|
+
throw new InvalidCredentialsError("kanidm", {
|
|
184
|
+
reason: "Failed to initialize admin authentication"
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const cookies = initResponse.headers.get("set-cookie");
|
|
188
|
+
const beginResponse = await fetch(authUrl, {
|
|
189
|
+
method: "POST",
|
|
190
|
+
headers: {
|
|
191
|
+
"Content-Type": "application/json",
|
|
192
|
+
...cookies ? { Cookie: cookies } : {}
|
|
193
|
+
},
|
|
194
|
+
body: JSON.stringify({
|
|
195
|
+
step: {
|
|
196
|
+
begin: "password"
|
|
197
|
+
}
|
|
198
|
+
}),
|
|
199
|
+
signal: AbortSignal.timeout(this.options.timeout || 3e4)
|
|
200
|
+
});
|
|
201
|
+
if (!beginResponse.ok) {
|
|
202
|
+
throw new InvalidCredentialsError("kanidm", {
|
|
203
|
+
reason: "Failed to begin password authentication"
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
const credResponse = await fetch(authUrl, {
|
|
207
|
+
method: "POST",
|
|
208
|
+
headers: {
|
|
209
|
+
"Content-Type": "application/json",
|
|
210
|
+
...cookies ? { Cookie: cookies } : {}
|
|
211
|
+
},
|
|
212
|
+
body: JSON.stringify({
|
|
213
|
+
step: {
|
|
214
|
+
cred: {
|
|
215
|
+
password: this.options.adminPassword
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}),
|
|
219
|
+
signal: AbortSignal.timeout(this.options.timeout || 3e4)
|
|
220
|
+
});
|
|
221
|
+
if (!credResponse.ok) {
|
|
222
|
+
throw new InvalidCredentialsError("kanidm", {
|
|
223
|
+
reason: "Invalid admin credentials"
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const result = await credResponse.json();
|
|
227
|
+
const token = result.state?.success || result.token;
|
|
228
|
+
if (!token) {
|
|
229
|
+
throw new ProviderError(
|
|
230
|
+
"Failed to obtain admin token from Kanidm",
|
|
231
|
+
"kanidm"
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
this.adminToken = token;
|
|
235
|
+
this.adminTokenExpiry = Date.now() + 36e5;
|
|
236
|
+
return this.adminToken;
|
|
237
|
+
}
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// AUTHENTICATION FLOWS
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
async getAuthorizationUrl(options) {
|
|
242
|
+
const discovery = await this.fetchDiscoveryDocument();
|
|
243
|
+
const state = options?.state || generateRandomString();
|
|
244
|
+
const nonce = options?.nonce || generateRandomString();
|
|
245
|
+
const scopes = options?.scopes || this.options.scopes || ["openid", "profile", "email", "groups"];
|
|
246
|
+
const redirectUri = options?.redirectUri || this.options.redirectUri;
|
|
247
|
+
if (!redirectUri) {
|
|
248
|
+
throw new ConfigurationError("redirectUri is required", "kanidm");
|
|
249
|
+
}
|
|
250
|
+
const params = new URLSearchParams({
|
|
251
|
+
client_id: this.options.clientId,
|
|
252
|
+
redirect_uri: redirectUri,
|
|
253
|
+
response_type: "code",
|
|
254
|
+
scope: scopes.join(" "),
|
|
255
|
+
state,
|
|
256
|
+
nonce
|
|
257
|
+
});
|
|
258
|
+
if (options?.prompt) {
|
|
259
|
+
params.set("prompt", options.prompt);
|
|
260
|
+
}
|
|
261
|
+
if (options?.loginHint) {
|
|
262
|
+
params.set("login_hint", options.loginHint);
|
|
263
|
+
}
|
|
264
|
+
if (options?.extraParams) {
|
|
265
|
+
for (const [key, value] of Object.entries(options.extraParams)) {
|
|
266
|
+
params.set(key, value);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const pkce = await generatePKCE();
|
|
270
|
+
params.set("code_challenge", pkce.challenge);
|
|
271
|
+
params.set("code_challenge_method", "S256");
|
|
272
|
+
const url = `${discovery.authorization_endpoint}?${params.toString()}`;
|
|
273
|
+
return {
|
|
274
|
+
url,
|
|
275
|
+
state,
|
|
276
|
+
nonce,
|
|
277
|
+
codeVerifier: pkce.verifier
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
async exchangeCode(params) {
|
|
281
|
+
const discovery = await this.fetchDiscoveryDocument();
|
|
282
|
+
const redirectUri = params.redirectUri || this.options.redirectUri;
|
|
283
|
+
if (!redirectUri) {
|
|
284
|
+
throw new ConfigurationError("redirectUri is required", "kanidm");
|
|
285
|
+
}
|
|
286
|
+
const body = new URLSearchParams({
|
|
287
|
+
grant_type: "authorization_code",
|
|
288
|
+
client_id: this.options.clientId,
|
|
289
|
+
code: params.code,
|
|
290
|
+
redirect_uri: redirectUri
|
|
291
|
+
});
|
|
292
|
+
if (this.options.clientSecret) {
|
|
293
|
+
body.set("client_secret", this.options.clientSecret);
|
|
294
|
+
}
|
|
295
|
+
if (params.codeVerifier) {
|
|
296
|
+
body.set("code_verifier", params.codeVerifier);
|
|
297
|
+
}
|
|
298
|
+
const response = await this.request(discovery.token_endpoint, {
|
|
299
|
+
method: "POST",
|
|
300
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
301
|
+
body: body.toString()
|
|
302
|
+
});
|
|
303
|
+
let userId = "";
|
|
304
|
+
if (response.id_token) {
|
|
305
|
+
const payload = decodeJwt(response.id_token);
|
|
306
|
+
userId = payload.sub || "";
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
accessToken: response.access_token,
|
|
310
|
+
tokenType: response.token_type || "Bearer",
|
|
311
|
+
expiresIn: response.expires_in,
|
|
312
|
+
refreshToken: response.refresh_token,
|
|
313
|
+
idToken: response.id_token,
|
|
314
|
+
scope: response.scope,
|
|
315
|
+
userId
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
async authenticate(_credentials) {
|
|
319
|
+
throw new NotImplementedError("authenticate", "kanidm", {
|
|
320
|
+
reason: "Kanidm only supports authorization code flow. Use getAuthorizationUrl() and exchangeCode() instead."
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
async refresh(refreshToken) {
|
|
324
|
+
const discovery = await this.fetchDiscoveryDocument();
|
|
325
|
+
const body = new URLSearchParams({
|
|
326
|
+
grant_type: "refresh_token",
|
|
327
|
+
client_id: this.options.clientId,
|
|
328
|
+
refresh_token: refreshToken
|
|
329
|
+
});
|
|
330
|
+
if (this.options.clientSecret) {
|
|
331
|
+
body.set("client_secret", this.options.clientSecret);
|
|
332
|
+
}
|
|
333
|
+
const response = await this.request(discovery.token_endpoint, {
|
|
334
|
+
method: "POST",
|
|
335
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
336
|
+
body: body.toString()
|
|
337
|
+
});
|
|
338
|
+
let userId = "";
|
|
339
|
+
if (response.access_token) {
|
|
340
|
+
const payload = decodeJwt(response.access_token);
|
|
341
|
+
userId = payload.sub || "";
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
accessToken: response.access_token,
|
|
345
|
+
tokenType: response.token_type || "Bearer",
|
|
346
|
+
expiresIn: response.expires_in,
|
|
347
|
+
refreshToken: response.refresh_token || refreshToken,
|
|
348
|
+
idToken: response.id_token,
|
|
349
|
+
scope: response.scope,
|
|
350
|
+
userId
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
async logout(options) {
|
|
354
|
+
const discovery = await this.fetchDiscoveryDocument();
|
|
355
|
+
if (options?.refreshToken && discovery.revocation_endpoint) {
|
|
356
|
+
const body = new URLSearchParams({
|
|
357
|
+
client_id: this.options.clientId,
|
|
358
|
+
token: options.refreshToken,
|
|
359
|
+
token_type_hint: "refresh_token"
|
|
360
|
+
});
|
|
361
|
+
if (this.options.clientSecret) {
|
|
362
|
+
body.set("client_secret", this.options.clientSecret);
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
await this.request(discovery.revocation_endpoint, {
|
|
366
|
+
method: "POST",
|
|
367
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
368
|
+
body: body.toString()
|
|
369
|
+
});
|
|
370
|
+
} catch {
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (options?.token && discovery.revocation_endpoint) {
|
|
374
|
+
const body = new URLSearchParams({
|
|
375
|
+
client_id: this.options.clientId,
|
|
376
|
+
token: options.token,
|
|
377
|
+
token_type_hint: "access_token"
|
|
378
|
+
});
|
|
379
|
+
if (this.options.clientSecret) {
|
|
380
|
+
body.set("client_secret", this.options.clientSecret);
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
await this.request(discovery.revocation_endpoint, {
|
|
384
|
+
method: "POST",
|
|
385
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
386
|
+
body: body.toString()
|
|
387
|
+
});
|
|
388
|
+
} catch {
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
// TOKEN OPERATIONS
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
async validateToken(token, options) {
|
|
396
|
+
try {
|
|
397
|
+
const jwks = await this.getJWKS();
|
|
398
|
+
const discovery = await this.fetchDiscoveryDocument();
|
|
399
|
+
const verifyOptions = {
|
|
400
|
+
issuer: options?.issuer || discovery.issuer,
|
|
401
|
+
clockTolerance: options?.clockTolerance || 0
|
|
402
|
+
};
|
|
403
|
+
if (options?.audience) {
|
|
404
|
+
verifyOptions.audience = options.audience;
|
|
405
|
+
}
|
|
406
|
+
const { payload } = await jwtVerify(token, jwks, verifyOptions);
|
|
407
|
+
if (options?.nonce && payload.nonce !== options.nonce) {
|
|
408
|
+
throw new InvalidNonceError("kanidm");
|
|
409
|
+
}
|
|
410
|
+
const groups = payload.groups || [];
|
|
411
|
+
return {
|
|
412
|
+
sub: payload.sub || "",
|
|
413
|
+
iss: payload.iss || "",
|
|
414
|
+
aud: payload.aud || "",
|
|
415
|
+
exp: payload.exp || 0,
|
|
416
|
+
iat: payload.iat || 0,
|
|
417
|
+
nbf: payload.nbf,
|
|
418
|
+
azp: payload.azp,
|
|
419
|
+
email: payload.email,
|
|
420
|
+
email_verified: payload.email_verified,
|
|
421
|
+
preferred_username: payload.preferred_username,
|
|
422
|
+
name: payload.name,
|
|
423
|
+
roles: groups,
|
|
424
|
+
// Map groups to roles for consistency
|
|
425
|
+
...payload
|
|
426
|
+
};
|
|
427
|
+
} catch (error) {
|
|
428
|
+
if (error instanceof JWTExpired) {
|
|
429
|
+
throw new TokenExpiredError("kanidm");
|
|
430
|
+
}
|
|
431
|
+
if (error instanceof JWTClaimValidationFailed) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
if (error instanceof JWSSignatureVerificationFailed) {
|
|
435
|
+
throw new InvalidTokenError("Invalid token signature", "kanidm");
|
|
436
|
+
}
|
|
437
|
+
if (error instanceof InvalidNonceError) {
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
decodeToken(token) {
|
|
444
|
+
try {
|
|
445
|
+
const parts = token.split(".");
|
|
446
|
+
if (parts.length !== 3) {
|
|
447
|
+
throw new InvalidTokenError("Invalid JWT format", "kanidm");
|
|
448
|
+
}
|
|
449
|
+
const header = JSON.parse(atob(parts[0]));
|
|
450
|
+
const payload = decodeJwt(token);
|
|
451
|
+
return {
|
|
452
|
+
header: {
|
|
453
|
+
alg: header.alg,
|
|
454
|
+
typ: header.typ,
|
|
455
|
+
kid: header.kid
|
|
456
|
+
},
|
|
457
|
+
payload,
|
|
458
|
+
signature: parts[2]
|
|
459
|
+
};
|
|
460
|
+
} catch {
|
|
461
|
+
throw new InvalidTokenError("Failed to decode token", "kanidm");
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async introspectToken(token) {
|
|
465
|
+
const discovery = await this.fetchDiscoveryDocument();
|
|
466
|
+
if (!discovery.introspection_endpoint) {
|
|
467
|
+
const claims = await this.validateToken(token);
|
|
468
|
+
return {
|
|
469
|
+
active: claims !== null,
|
|
470
|
+
claims: claims || void 0
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
const body = new URLSearchParams({
|
|
474
|
+
client_id: this.options.clientId,
|
|
475
|
+
token
|
|
476
|
+
});
|
|
477
|
+
if (this.options.clientSecret) {
|
|
478
|
+
body.set("client_secret", this.options.clientSecret);
|
|
479
|
+
}
|
|
480
|
+
const response = await this.request(discovery.introspection_endpoint, {
|
|
481
|
+
method: "POST",
|
|
482
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
483
|
+
body: body.toString()
|
|
484
|
+
});
|
|
485
|
+
if (!response.active) {
|
|
486
|
+
return { active: false };
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
active: true,
|
|
490
|
+
claims: {
|
|
491
|
+
sub: response.sub || "",
|
|
492
|
+
iss: response.iss || "",
|
|
493
|
+
aud: response.aud || "",
|
|
494
|
+
exp: response.exp || 0,
|
|
495
|
+
iat: response.iat || 0,
|
|
496
|
+
...response
|
|
497
|
+
},
|
|
498
|
+
tokenType: response.token_type,
|
|
499
|
+
clientId: response.client_id,
|
|
500
|
+
scope: response.scope
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
// USER OPERATIONS
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
async getProfile(tokenOrSession) {
|
|
507
|
+
const discovery = await this.fetchDiscoveryDocument();
|
|
508
|
+
const response = await this.request(discovery.userinfo_endpoint, {
|
|
509
|
+
method: "GET",
|
|
510
|
+
headers: { Authorization: `Bearer ${tokenOrSession}` }
|
|
511
|
+
});
|
|
512
|
+
return {
|
|
513
|
+
id: response.sub,
|
|
514
|
+
username: response.preferred_username,
|
|
515
|
+
email: response.email,
|
|
516
|
+
emailVerified: response.email_verified,
|
|
517
|
+
firstName: response.given_name,
|
|
518
|
+
lastName: response.family_name,
|
|
519
|
+
displayName: response.name,
|
|
520
|
+
picture: response.picture,
|
|
521
|
+
groups: response.groups
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
async updateProfile(_tokenOrSession, _profile) {
|
|
525
|
+
throw new NotImplementedError("updateProfile", "kanidm", {
|
|
526
|
+
reason: "Profile updates are not supported via OAuth2. Use admin API with createUser/updateUser."
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
async getUser(userId, _adminToken) {
|
|
530
|
+
const token = await this.getAdminToken();
|
|
531
|
+
const response = await this.request(
|
|
532
|
+
`${this.getApiBaseUrl()}/person/${userId}`,
|
|
533
|
+
{ method: "GET" },
|
|
534
|
+
token
|
|
535
|
+
);
|
|
536
|
+
return this.mapKanidmPerson(response);
|
|
537
|
+
}
|
|
538
|
+
async createUser(user, _adminToken) {
|
|
539
|
+
const token = await this.getAdminToken();
|
|
540
|
+
const kanidmPerson = {
|
|
541
|
+
attrs: {
|
|
542
|
+
name: [user.username],
|
|
543
|
+
displayname: [
|
|
544
|
+
user.firstName && user.lastName ? `${user.firstName} ${user.lastName}` : user.username
|
|
545
|
+
]
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
if (user.email && kanidmPerson.attrs) {
|
|
549
|
+
kanidmPerson.attrs.mail = [user.email];
|
|
550
|
+
}
|
|
551
|
+
const response = await fetch(`${this.getApiBaseUrl()}/person`, {
|
|
552
|
+
method: "POST",
|
|
553
|
+
headers: {
|
|
554
|
+
"Content-Type": "application/json",
|
|
555
|
+
Authorization: `Bearer ${token}`
|
|
556
|
+
},
|
|
557
|
+
body: JSON.stringify(kanidmPerson)
|
|
558
|
+
});
|
|
559
|
+
if (!response.ok) {
|
|
560
|
+
const errorBody = await response.text();
|
|
561
|
+
if (response.status === 409) {
|
|
562
|
+
throw new UserAlreadyExistsError(user.username, "kanidm");
|
|
563
|
+
}
|
|
564
|
+
throw new ProviderError(`Failed to create user: ${errorBody}`, "kanidm");
|
|
565
|
+
}
|
|
566
|
+
return this.getUser(user.username, token);
|
|
567
|
+
}
|
|
568
|
+
async updateUser(userId, updates, _adminToken) {
|
|
569
|
+
const token = await this.getAdminToken();
|
|
570
|
+
const attrs = {};
|
|
571
|
+
if (updates.email !== void 0) {
|
|
572
|
+
attrs.mail = [updates.email];
|
|
573
|
+
}
|
|
574
|
+
if (updates.firstName !== void 0 || updates.lastName !== void 0) {
|
|
575
|
+
const displayName = updates.firstName && updates.lastName ? `${updates.firstName} ${updates.lastName}` : updates.firstName || updates.lastName || "";
|
|
576
|
+
if (displayName) {
|
|
577
|
+
attrs.displayname = [displayName];
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
await this.request(
|
|
581
|
+
`${this.getApiBaseUrl()}/person/${userId}`,
|
|
582
|
+
{
|
|
583
|
+
method: "PATCH",
|
|
584
|
+
body: JSON.stringify({ attrs })
|
|
585
|
+
},
|
|
586
|
+
token
|
|
587
|
+
);
|
|
588
|
+
return this.getUser(userId, token);
|
|
589
|
+
}
|
|
590
|
+
async deleteUser(userId, _adminToken) {
|
|
591
|
+
const token = await this.getAdminToken();
|
|
592
|
+
await this.request(
|
|
593
|
+
`${this.getApiBaseUrl()}/person/${userId}`,
|
|
594
|
+
{ method: "DELETE" },
|
|
595
|
+
token
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
async listUsers(query, _adminToken) {
|
|
599
|
+
const token = await this.getAdminToken();
|
|
600
|
+
let url = `${this.getApiBaseUrl()}/person`;
|
|
601
|
+
const params = new URLSearchParams();
|
|
602
|
+
if (query.search) {
|
|
603
|
+
params.set("search", query.search);
|
|
604
|
+
}
|
|
605
|
+
if (params.toString()) {
|
|
606
|
+
url += `?${params.toString()}`;
|
|
607
|
+
}
|
|
608
|
+
const response = await this.request(
|
|
609
|
+
url,
|
|
610
|
+
{ method: "GET" },
|
|
611
|
+
token
|
|
612
|
+
);
|
|
613
|
+
const users = Array.isArray(response) ? response : [];
|
|
614
|
+
let filteredUsers = users.map((u) => this.mapKanidmPerson(u));
|
|
615
|
+
if (query.email) {
|
|
616
|
+
filteredUsers = filteredUsers.filter((u) => u.email === query.email);
|
|
617
|
+
}
|
|
618
|
+
if (query.username) {
|
|
619
|
+
filteredUsers = filteredUsers.filter(
|
|
620
|
+
(u) => u.username === query.username
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
const total = filteredUsers.length;
|
|
624
|
+
const offset = query.offset || 0;
|
|
625
|
+
const limit = query.limit || 100;
|
|
626
|
+
return {
|
|
627
|
+
users: filteredUsers.slice(offset, offset + limit),
|
|
628
|
+
total,
|
|
629
|
+
limit,
|
|
630
|
+
offset
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
async requestPasswordReset(_email) {
|
|
634
|
+
throw new NotImplementedError("requestPasswordReset", "kanidm", {
|
|
635
|
+
reason: "Password reset is not exposed via Kanidm API"
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
async resetPassword(_token, _newPassword) {
|
|
639
|
+
throw new NotImplementedError("resetPassword", "kanidm", {
|
|
640
|
+
reason: "Password reset is not exposed via Kanidm API"
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
// ---------------------------------------------------------------------------
|
|
644
|
+
// SESSION OPERATIONS
|
|
645
|
+
// ---------------------------------------------------------------------------
|
|
646
|
+
async listSessions(_userId, _adminToken) {
|
|
647
|
+
throw new NotImplementedError("listSessions", "kanidm", {
|
|
648
|
+
reason: "Session management is not exposed via Kanidm API"
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
async revokeSession(_sessionId, _adminToken) {
|
|
652
|
+
throw new NotImplementedError("revokeSession", "kanidm", {
|
|
653
|
+
reason: "Session management is not exposed via Kanidm API"
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
async revokeAllSessions(_userId, _adminToken) {
|
|
657
|
+
throw new NotImplementedError("revokeAllSessions", "kanidm", {
|
|
658
|
+
reason: "Session management is not exposed via Kanidm API"
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
// ---------------------------------------------------------------------------
|
|
662
|
+
// AUTHORIZATION
|
|
663
|
+
// ---------------------------------------------------------------------------
|
|
664
|
+
async hasRole(tokenOrUserId, role) {
|
|
665
|
+
const roles = await this.getRoles(tokenOrUserId);
|
|
666
|
+
return roles.includes(role);
|
|
667
|
+
}
|
|
668
|
+
async hasPermission(tokenOrUserId, permission, resource) {
|
|
669
|
+
const roles = await this.getRoles(tokenOrUserId);
|
|
670
|
+
const permissionRole = resource ? `${resource}:${permission}` : permission;
|
|
671
|
+
return roles.includes(permissionRole) || roles.includes(permission);
|
|
672
|
+
}
|
|
673
|
+
async getRoles(tokenOrUserId, _adminToken) {
|
|
674
|
+
try {
|
|
675
|
+
const claims = await this.validateToken(tokenOrUserId);
|
|
676
|
+
if (claims) {
|
|
677
|
+
return claims.roles || [];
|
|
678
|
+
}
|
|
679
|
+
} catch {
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
const user = await this.getUser(tokenOrUserId);
|
|
683
|
+
return user.groups || [];
|
|
684
|
+
} catch {
|
|
685
|
+
}
|
|
686
|
+
return [];
|
|
687
|
+
}
|
|
688
|
+
async assignRole(_userId, _role, _adminToken) {
|
|
689
|
+
throw new NotImplementedError("assignRole", "kanidm", {
|
|
690
|
+
reason: "Role assignment via API requires group management. Use Kanidm CLI to add users to groups."
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
async removeRole(_userId, _role, _adminToken) {
|
|
694
|
+
throw new NotImplementedError("removeRole", "kanidm", {
|
|
695
|
+
reason: "Role removal via API requires group management. Use Kanidm CLI to remove users from groups."
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
// ---------------------------------------------------------------------------
|
|
699
|
+
// PROVIDER INFORMATION
|
|
700
|
+
// ---------------------------------------------------------------------------
|
|
701
|
+
async getCapabilities() {
|
|
702
|
+
return {
|
|
703
|
+
authorizationCode: true,
|
|
704
|
+
passwordGrant: false,
|
|
705
|
+
// Not supported
|
|
706
|
+
clientCredentials: false,
|
|
707
|
+
// Not supported
|
|
708
|
+
tokenRefresh: true,
|
|
709
|
+
oidc: true,
|
|
710
|
+
userManagement: true,
|
|
711
|
+
// Via /v1/person API
|
|
712
|
+
sessionManagement: false,
|
|
713
|
+
// Not exposed
|
|
714
|
+
rbac: true,
|
|
715
|
+
// Via groups claim
|
|
716
|
+
passwordReset: false,
|
|
717
|
+
// Not exposed via API
|
|
718
|
+
mfa: true,
|
|
719
|
+
// Webauthn/passkeys supported
|
|
720
|
+
socialLogin: false,
|
|
721
|
+
federation: true,
|
|
722
|
+
decentralized: false
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
async getDiscoveryDocument() {
|
|
726
|
+
return this.fetchDiscoveryDocument();
|
|
727
|
+
}
|
|
728
|
+
// ---------------------------------------------------------------------------
|
|
729
|
+
// PRIVATE HELPERS
|
|
730
|
+
// ---------------------------------------------------------------------------
|
|
731
|
+
mapKanidmPerson(person) {
|
|
732
|
+
const attrs = person.attrs || {};
|
|
733
|
+
return {
|
|
734
|
+
id: attrs.uuid?.[0] || attrs.name?.[0] || "",
|
|
735
|
+
username: attrs.name?.[0],
|
|
736
|
+
email: attrs.mail?.[0],
|
|
737
|
+
displayName: attrs.displayname?.[0],
|
|
738
|
+
groups: attrs.memberof,
|
|
739
|
+
enabled: true
|
|
740
|
+
// Kanidm doesn't expose this directly
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
export {
|
|
745
|
+
KanidmProvider
|
|
746
|
+
};
|
|
747
|
+
//# sourceMappingURL=kanidm-hkw-YPVF.js.map
|