@happyvertical/auth 0.80.0 → 0.80.2
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/dist/chunks/cognito-thQmKf7L.js +124 -0
- package/dist/chunks/cognito-thQmKf7L.js.map +1 -0
- package/dist/chunks/decode_jwt-BvtACpi_.js +1183 -0
- package/dist/chunks/decode_jwt-BvtACpi_.js.map +1 -0
- package/dist/chunks/errors-RgVH84_1.js +343 -0
- package/dist/chunks/errors-RgVH84_1.js.map +1 -0
- package/dist/chunks/github-uNnnVjFZ.js +311 -0
- package/dist/chunks/github-uNnnVjFZ.js.map +1 -0
- package/dist/chunks/google-C_p8rExJ.js +374 -0
- package/dist/chunks/google-C_p8rExJ.js.map +1 -0
- package/dist/chunks/kanidm-DTcc6ufi.js +567 -0
- package/dist/chunks/kanidm-DTcc6ufi.js.map +1 -0
- package/dist/chunks/keycloak-CzPHgI2z.js +632 -0
- package/dist/chunks/keycloak-CzPHgI2z.js.map +1 -0
- package/dist/chunks/nostr-zrMaYMU-.js +141 -0
- package/dist/chunks/nostr-zrMaYMU-.js.map +1 -0
- package/dist/cli/claude-context.js +17 -17
- package/dist/cli/claude-context.js.map +1 -1
- package/dist/index.js +204 -486
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/dist/chunks/cognito-dmypylFX.js +0 -128
- package/dist/chunks/cognito-dmypylFX.js.map +0 -1
- package/dist/chunks/decode_jwt-D2OK1b8a.js +0 -1395
- package/dist/chunks/decode_jwt-D2OK1b8a.js.map +0 -1
- package/dist/chunks/github-NSZp5tVm.js +0 -413
- package/dist/chunks/github-NSZp5tVm.js.map +0 -1
- package/dist/chunks/google-HXk2ctYR.js +0 -483
- package/dist/chunks/google-HXk2ctYR.js.map +0 -1
- package/dist/chunks/index-BpsMhFXS.js +0 -151
- package/dist/chunks/index-BpsMhFXS.js.map +0 -1
- package/dist/chunks/kanidm-hkw-YPVF.js +0 -747
- package/dist/chunks/kanidm-hkw-YPVF.js.map +0 -1
- package/dist/chunks/keycloak-t6JEUeOz.js +0 -871
- package/dist/chunks/keycloak-t6JEUeOz.js.map +0 -1
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
import { E as UserAlreadyExistsError, O as UserNotFoundError, S as ProviderError, T as TokenExpiredError, a as ConfigurationError, b as NetworkError, c as InvalidClientError, l as InvalidCredentialsError, p as InvalidNonceError, t as AccessDeniedError, u as InvalidGrantError, v as InvalidTokenError, x as NotImplementedError, y as MfaRequiredError } from "./errors-RgVH84_1.js";
|
|
2
|
+
import { a as JWTClaimValidationFailed, i as JWSSignatureVerificationFailed, n as createRemoteJWKSet, o as JWTExpired, r as jwtVerify, t as decodeJwt } from "./decode_jwt-BvtACpi_.js";
|
|
3
|
+
//#region src/shared/providers/keycloak.ts
|
|
4
|
+
/**
|
|
5
|
+
* Keycloak Provider - OIDC/OAuth2 Authentication
|
|
6
|
+
*
|
|
7
|
+
* Implements full OIDC/OAuth2 authentication with Keycloak server including:
|
|
8
|
+
* - OIDC Discovery
|
|
9
|
+
* - Authorization Code Flow with PKCE
|
|
10
|
+
* - Token validation using JWKS
|
|
11
|
+
* - Token introspection
|
|
12
|
+
* - User management via Admin API
|
|
13
|
+
* - Session management
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Generate a random string for state/nonce/PKCE.
|
|
17
|
+
*/
|
|
18
|
+
function generateRandomString(length = 32) {
|
|
19
|
+
const array = new Uint8Array(length);
|
|
20
|
+
crypto.getRandomValues(array);
|
|
21
|
+
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Generate PKCE code verifier and challenge.
|
|
25
|
+
*/
|
|
26
|
+
async function generatePKCE() {
|
|
27
|
+
const verifier = generateRandomString(32);
|
|
28
|
+
const data = new TextEncoder().encode(verifier);
|
|
29
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
30
|
+
return {
|
|
31
|
+
verifier,
|
|
32
|
+
challenge: btoa(String.fromCharCode(...new Uint8Array(hash))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Keycloak authentication provider.
|
|
37
|
+
*
|
|
38
|
+
* Implements full OIDC/OAuth2 authentication with Keycloak server.
|
|
39
|
+
*/
|
|
40
|
+
var KeycloakProvider = class {
|
|
41
|
+
options;
|
|
42
|
+
discoveryDocument = null;
|
|
43
|
+
jwks = null;
|
|
44
|
+
constructor(options) {
|
|
45
|
+
if (!options.serverUrl) throw new ConfigurationError("serverUrl is required", "keycloak");
|
|
46
|
+
if (!options.realm) throw new ConfigurationError("realm is required", "keycloak");
|
|
47
|
+
if (!options.clientId) throw new ConfigurationError("clientId is required", "keycloak");
|
|
48
|
+
this.options = {
|
|
49
|
+
usePKCE: true,
|
|
50
|
+
verifySsl: true,
|
|
51
|
+
scopes: [
|
|
52
|
+
"openid",
|
|
53
|
+
"profile",
|
|
54
|
+
"email"
|
|
55
|
+
],
|
|
56
|
+
timeout: 3e4,
|
|
57
|
+
maxRetries: 3,
|
|
58
|
+
...options
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get the base URL for the realm.
|
|
63
|
+
*/
|
|
64
|
+
getRealmUrl() {
|
|
65
|
+
return `${this.options.serverUrl}/realms/${this.options.realm}`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get the admin API base URL.
|
|
69
|
+
*/
|
|
70
|
+
getAdminUrl() {
|
|
71
|
+
return `${this.options.serverUrl}/admin/realms/${this.options.realm}`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Make an HTTP request with error handling.
|
|
75
|
+
*/
|
|
76
|
+
async request(url, options = {}, adminToken) {
|
|
77
|
+
const headers = {
|
|
78
|
+
"Content-Type": "application/json",
|
|
79
|
+
...this.options.headers,
|
|
80
|
+
...options.headers
|
|
81
|
+
};
|
|
82
|
+
if (adminToken) headers["Authorization"] = `Bearer ${adminToken}`;
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch(url, {
|
|
85
|
+
...options,
|
|
86
|
+
headers,
|
|
87
|
+
signal: AbortSignal.timeout(this.options.timeout || 3e4)
|
|
88
|
+
});
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
const errorBody = await response.text().catch(() => "");
|
|
91
|
+
let errorData = {};
|
|
92
|
+
try {
|
|
93
|
+
errorData = JSON.parse(errorBody);
|
|
94
|
+
} catch {}
|
|
95
|
+
this.handleHttpError(response.status, errorData, errorBody);
|
|
96
|
+
}
|
|
97
|
+
const text = await response.text();
|
|
98
|
+
if (!text) return {};
|
|
99
|
+
return JSON.parse(text);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error instanceof Error && error.name === "TimeoutError") throw new NetworkError("Request timed out", "keycloak", error);
|
|
102
|
+
if (error instanceof InvalidCredentialsError || error instanceof AccessDeniedError || error instanceof InvalidGrantError || error instanceof InvalidClientError || error instanceof UserNotFoundError || error instanceof ProviderError) throw error;
|
|
103
|
+
throw new NetworkError(`Network error: ${error instanceof Error ? error.message : "Unknown error"}`, "keycloak", error instanceof Error ? error : void 0);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Handle HTTP error responses.
|
|
108
|
+
*/
|
|
109
|
+
handleHttpError(status, data, rawBody) {
|
|
110
|
+
const error = data.error;
|
|
111
|
+
const errorDescription = data.errorMessage || data.error_description || rawBody;
|
|
112
|
+
switch (status) {
|
|
113
|
+
case 400:
|
|
114
|
+
if (error === "invalid_grant") throw new InvalidGrantError(errorDescription, "keycloak");
|
|
115
|
+
if (error === "invalid_client") throw new InvalidClientError("keycloak");
|
|
116
|
+
throw new ProviderError(`Bad request: ${errorDescription}`, "keycloak");
|
|
117
|
+
case 401: throw new InvalidCredentialsError("keycloak");
|
|
118
|
+
case 403: throw new AccessDeniedError(errorDescription, "keycloak");
|
|
119
|
+
case 404: throw new UserNotFoundError(void 0, "keycloak");
|
|
120
|
+
case 409: throw new UserAlreadyExistsError(void 0, "keycloak");
|
|
121
|
+
default: throw new ProviderError(`Keycloak error (${status}): ${errorDescription}`, "keycloak");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Fetch and cache the OIDC discovery document.
|
|
126
|
+
*/
|
|
127
|
+
async fetchDiscoveryDocument() {
|
|
128
|
+
if (this.discoveryDocument) return this.discoveryDocument;
|
|
129
|
+
const url = `${this.getRealmUrl()}/.well-known/openid-configuration`;
|
|
130
|
+
this.discoveryDocument = await this.request(url);
|
|
131
|
+
return this.discoveryDocument;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Get JWKS for token validation.
|
|
135
|
+
*/
|
|
136
|
+
async getJWKS() {
|
|
137
|
+
if (this.jwks) return this.jwks;
|
|
138
|
+
const discovery = await this.fetchDiscoveryDocument();
|
|
139
|
+
this.jwks = createRemoteJWKSet(new URL(discovery.jwks_uri));
|
|
140
|
+
return this.jwks;
|
|
141
|
+
}
|
|
142
|
+
async getAuthorizationUrl(options) {
|
|
143
|
+
const discovery = await this.fetchDiscoveryDocument();
|
|
144
|
+
const state = options?.state || generateRandomString();
|
|
145
|
+
const nonce = options?.nonce || generateRandomString();
|
|
146
|
+
const scopes = options?.scopes || this.options.scopes || [
|
|
147
|
+
"openid",
|
|
148
|
+
"profile",
|
|
149
|
+
"email"
|
|
150
|
+
];
|
|
151
|
+
const redirectUri = options?.redirectUri || this.options.redirectUri;
|
|
152
|
+
if (!redirectUri) throw new ConfigurationError("redirectUri is required", "keycloak");
|
|
153
|
+
const params = new URLSearchParams({
|
|
154
|
+
client_id: this.options.clientId,
|
|
155
|
+
redirect_uri: redirectUri,
|
|
156
|
+
response_type: "code",
|
|
157
|
+
scope: scopes.join(" "),
|
|
158
|
+
state,
|
|
159
|
+
nonce
|
|
160
|
+
});
|
|
161
|
+
if (options?.prompt) params.set("prompt", options.prompt);
|
|
162
|
+
if (options?.loginHint) params.set("login_hint", options.loginHint);
|
|
163
|
+
if (options?.extraParams) for (const [key, value] of Object.entries(options.extraParams)) params.set(key, value);
|
|
164
|
+
let codeVerifier;
|
|
165
|
+
if (this.options.usePKCE) {
|
|
166
|
+
const pkce = await generatePKCE();
|
|
167
|
+
codeVerifier = pkce.verifier;
|
|
168
|
+
params.set("code_challenge", pkce.challenge);
|
|
169
|
+
params.set("code_challenge_method", "S256");
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
url: `${discovery.authorization_endpoint}?${params.toString()}`,
|
|
173
|
+
state,
|
|
174
|
+
nonce,
|
|
175
|
+
codeVerifier
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
async exchangeCode(params) {
|
|
179
|
+
const discovery = await this.fetchDiscoveryDocument();
|
|
180
|
+
const redirectUri = params.redirectUri || this.options.redirectUri;
|
|
181
|
+
if (!redirectUri) throw new ConfigurationError("redirectUri is required", "keycloak");
|
|
182
|
+
const body = new URLSearchParams({
|
|
183
|
+
grant_type: "authorization_code",
|
|
184
|
+
client_id: this.options.clientId,
|
|
185
|
+
code: params.code,
|
|
186
|
+
redirect_uri: redirectUri
|
|
187
|
+
});
|
|
188
|
+
if (this.options.clientSecret) body.set("client_secret", this.options.clientSecret);
|
|
189
|
+
if (params.codeVerifier) body.set("code_verifier", params.codeVerifier);
|
|
190
|
+
const response = await this.request(discovery.token_endpoint, {
|
|
191
|
+
method: "POST",
|
|
192
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
193
|
+
body: body.toString()
|
|
194
|
+
});
|
|
195
|
+
let userId = "";
|
|
196
|
+
if (response.id_token) userId = decodeJwt(response.id_token).sub || "";
|
|
197
|
+
return {
|
|
198
|
+
accessToken: response.access_token,
|
|
199
|
+
tokenType: response.token_type || "Bearer",
|
|
200
|
+
expiresIn: response.expires_in,
|
|
201
|
+
refreshToken: response.refresh_token,
|
|
202
|
+
idToken: response.id_token,
|
|
203
|
+
scope: response.scope,
|
|
204
|
+
userId
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
async authenticate(credentials) {
|
|
208
|
+
const discovery = await this.fetchDiscoveryDocument();
|
|
209
|
+
const grantType = credentials.grantType || "password";
|
|
210
|
+
const scopes = credentials.scopes || this.options.scopes || [
|
|
211
|
+
"openid",
|
|
212
|
+
"profile",
|
|
213
|
+
"email"
|
|
214
|
+
];
|
|
215
|
+
const body = new URLSearchParams({
|
|
216
|
+
grant_type: grantType,
|
|
217
|
+
client_id: this.options.clientId,
|
|
218
|
+
scope: scopes.join(" ")
|
|
219
|
+
});
|
|
220
|
+
if (grantType === "client_credentials") {
|
|
221
|
+
if (!this.options.clientSecret) throw new InvalidCredentialsError("keycloak", { reason: "Client secret is required for client_credentials grant" });
|
|
222
|
+
body.set("client_secret", this.options.clientSecret);
|
|
223
|
+
} else {
|
|
224
|
+
if (!credentials.username || !credentials.password) throw new InvalidCredentialsError("keycloak", { reason: "Username and password are required" });
|
|
225
|
+
body.set("username", credentials.username);
|
|
226
|
+
body.set("password", credentials.password);
|
|
227
|
+
if (this.options.clientSecret) body.set("client_secret", this.options.clientSecret);
|
|
228
|
+
if (credentials.mfaCode) body.set("totp", credentials.mfaCode);
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
const response = await this.request(discovery.token_endpoint, {
|
|
232
|
+
method: "POST",
|
|
233
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
234
|
+
body: body.toString()
|
|
235
|
+
});
|
|
236
|
+
let userId = "";
|
|
237
|
+
if (response.id_token) userId = decodeJwt(response.id_token).sub || "";
|
|
238
|
+
else if (response.access_token) userId = decodeJwt(response.access_token).sub || "";
|
|
239
|
+
return {
|
|
240
|
+
accessToken: response.access_token,
|
|
241
|
+
tokenType: response.token_type || "Bearer",
|
|
242
|
+
expiresIn: response.expires_in,
|
|
243
|
+
refreshToken: response.refresh_token,
|
|
244
|
+
idToken: response.id_token,
|
|
245
|
+
scope: response.scope,
|
|
246
|
+
userId
|
|
247
|
+
};
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (error instanceof ProviderError && error.message.includes("invalid_grant") && error.message.includes("totp")) throw new MfaRequiredError("keycloak", ["totp"]);
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async refresh(refreshToken) {
|
|
254
|
+
const discovery = await this.fetchDiscoveryDocument();
|
|
255
|
+
const body = new URLSearchParams({
|
|
256
|
+
grant_type: "refresh_token",
|
|
257
|
+
client_id: this.options.clientId,
|
|
258
|
+
refresh_token: refreshToken
|
|
259
|
+
});
|
|
260
|
+
if (this.options.clientSecret) body.set("client_secret", this.options.clientSecret);
|
|
261
|
+
const response = await this.request(discovery.token_endpoint, {
|
|
262
|
+
method: "POST",
|
|
263
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
264
|
+
body: body.toString()
|
|
265
|
+
});
|
|
266
|
+
let userId = "";
|
|
267
|
+
if (response.access_token) userId = decodeJwt(response.access_token).sub || "";
|
|
268
|
+
return {
|
|
269
|
+
accessToken: response.access_token,
|
|
270
|
+
tokenType: response.token_type || "Bearer",
|
|
271
|
+
expiresIn: response.expires_in,
|
|
272
|
+
refreshToken: response.refresh_token || refreshToken,
|
|
273
|
+
idToken: response.id_token,
|
|
274
|
+
scope: response.scope,
|
|
275
|
+
userId
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
async logout(options) {
|
|
279
|
+
const discovery = await this.fetchDiscoveryDocument();
|
|
280
|
+
if (!discovery.end_session_endpoint) {
|
|
281
|
+
if (options?.refreshToken && discovery.revocation_endpoint) await this.request(discovery.revocation_endpoint, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
284
|
+
body: new URLSearchParams({
|
|
285
|
+
client_id: this.options.clientId,
|
|
286
|
+
token: options.refreshToken,
|
|
287
|
+
token_type_hint: "refresh_token"
|
|
288
|
+
}).toString()
|
|
289
|
+
});
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const params = new URLSearchParams({ client_id: this.options.clientId });
|
|
293
|
+
if (options?.token) params.set("id_token_hint", options.token);
|
|
294
|
+
if (options?.postLogoutRedirectUri) params.set("post_logout_redirect_uri", options.postLogoutRedirectUri);
|
|
295
|
+
if (options?.refreshToken && discovery.revocation_endpoint) {
|
|
296
|
+
const revokeParams = new URLSearchParams({
|
|
297
|
+
client_id: this.options.clientId,
|
|
298
|
+
token: options.refreshToken,
|
|
299
|
+
token_type_hint: "refresh_token"
|
|
300
|
+
});
|
|
301
|
+
if (this.options.clientSecret) revokeParams.set("client_secret", this.options.clientSecret);
|
|
302
|
+
await this.request(discovery.revocation_endpoint, {
|
|
303
|
+
method: "POST",
|
|
304
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
305
|
+
body: revokeParams.toString()
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async validateToken(token, options) {
|
|
310
|
+
try {
|
|
311
|
+
const jwks = await this.getJWKS();
|
|
312
|
+
const discovery = await this.fetchDiscoveryDocument();
|
|
313
|
+
const verifyOptions = {
|
|
314
|
+
issuer: options?.issuer || discovery.issuer,
|
|
315
|
+
clockTolerance: options?.clockTolerance || 0
|
|
316
|
+
};
|
|
317
|
+
if (options?.audience) verifyOptions.audience = options.audience;
|
|
318
|
+
const { payload } = await jwtVerify(token, jwks, verifyOptions);
|
|
319
|
+
if (options?.nonce && payload.nonce !== options.nonce) throw new InvalidNonceError("keycloak");
|
|
320
|
+
const roles = [];
|
|
321
|
+
if (payload.realm_access && typeof payload.realm_access === "object") {
|
|
322
|
+
const realmAccess = payload.realm_access;
|
|
323
|
+
if (realmAccess.roles) roles.push(...realmAccess.roles);
|
|
324
|
+
}
|
|
325
|
+
if (payload.resource_access && typeof payload.resource_access === "object") {
|
|
326
|
+
const resourceAccess = payload.resource_access;
|
|
327
|
+
for (const client of Object.values(resourceAccess)) if (client.roles) roles.push(...client.roles);
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
sub: payload.sub || "",
|
|
331
|
+
iss: payload.iss || "",
|
|
332
|
+
aud: payload.aud || "",
|
|
333
|
+
exp: payload.exp || 0,
|
|
334
|
+
iat: payload.iat || 0,
|
|
335
|
+
nbf: payload.nbf,
|
|
336
|
+
azp: payload.azp,
|
|
337
|
+
email: payload.email,
|
|
338
|
+
email_verified: payload.email_verified,
|
|
339
|
+
preferred_username: payload.preferred_username,
|
|
340
|
+
name: payload.name,
|
|
341
|
+
roles,
|
|
342
|
+
...payload
|
|
343
|
+
};
|
|
344
|
+
} catch (error) {
|
|
345
|
+
if (error instanceof JWTExpired) throw new TokenExpiredError("keycloak");
|
|
346
|
+
if (error instanceof JWTClaimValidationFailed) return null;
|
|
347
|
+
if (error instanceof JWSSignatureVerificationFailed) throw new InvalidTokenError("Invalid token signature", "keycloak");
|
|
348
|
+
if (error instanceof InvalidNonceError) throw error;
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
decodeToken(token) {
|
|
353
|
+
try {
|
|
354
|
+
const parts = token.split(".");
|
|
355
|
+
if (parts.length !== 3) throw new InvalidTokenError("Invalid JWT format", "keycloak");
|
|
356
|
+
const header = JSON.parse(atob(parts[0]));
|
|
357
|
+
const payload = decodeJwt(token);
|
|
358
|
+
return {
|
|
359
|
+
header: {
|
|
360
|
+
alg: header.alg,
|
|
361
|
+
typ: header.typ,
|
|
362
|
+
kid: header.kid
|
|
363
|
+
},
|
|
364
|
+
payload,
|
|
365
|
+
signature: parts[2]
|
|
366
|
+
};
|
|
367
|
+
} catch {
|
|
368
|
+
throw new InvalidTokenError("Failed to decode token", "keycloak");
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
async introspectToken(token) {
|
|
372
|
+
const discovery = await this.fetchDiscoveryDocument();
|
|
373
|
+
if (!discovery.introspection_endpoint) {
|
|
374
|
+
const claims = await this.validateToken(token);
|
|
375
|
+
return {
|
|
376
|
+
active: claims !== null,
|
|
377
|
+
claims: claims || void 0
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const body = new URLSearchParams({
|
|
381
|
+
client_id: this.options.clientId,
|
|
382
|
+
token
|
|
383
|
+
});
|
|
384
|
+
if (this.options.clientSecret) body.set("client_secret", this.options.clientSecret);
|
|
385
|
+
const response = await this.request(discovery.introspection_endpoint, {
|
|
386
|
+
method: "POST",
|
|
387
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
388
|
+
body: body.toString()
|
|
389
|
+
});
|
|
390
|
+
if (!response.active) return { active: false };
|
|
391
|
+
return {
|
|
392
|
+
active: true,
|
|
393
|
+
claims: {
|
|
394
|
+
sub: response.sub || "",
|
|
395
|
+
iss: response.iss || "",
|
|
396
|
+
aud: response.aud || "",
|
|
397
|
+
exp: response.exp || 0,
|
|
398
|
+
iat: response.iat || 0,
|
|
399
|
+
...response
|
|
400
|
+
},
|
|
401
|
+
tokenType: response.token_type,
|
|
402
|
+
clientId: response.client_id,
|
|
403
|
+
scope: response.scope
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
async getProfile(tokenOrSession) {
|
|
407
|
+
const discovery = await this.fetchDiscoveryDocument();
|
|
408
|
+
const response = await this.request(discovery.userinfo_endpoint, {
|
|
409
|
+
method: "GET",
|
|
410
|
+
headers: { Authorization: `Bearer ${tokenOrSession}` }
|
|
411
|
+
});
|
|
412
|
+
return {
|
|
413
|
+
id: response.sub,
|
|
414
|
+
username: response.preferred_username,
|
|
415
|
+
email: response.email,
|
|
416
|
+
emailVerified: response.email_verified,
|
|
417
|
+
firstName: response.given_name,
|
|
418
|
+
lastName: response.family_name,
|
|
419
|
+
displayName: response.name,
|
|
420
|
+
picture: response.picture
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
async updateProfile(tokenOrSession, profile) {
|
|
424
|
+
if (!decodeJwt(tokenOrSession).sub) throw new InvalidTokenError("Token does not contain user ID", "keycloak");
|
|
425
|
+
const accountUrl = `${this.getRealmUrl()}/account`;
|
|
426
|
+
const updateData = {};
|
|
427
|
+
if (profile.firstName !== void 0) updateData.firstName = profile.firstName;
|
|
428
|
+
if (profile.lastName !== void 0) updateData.lastName = profile.lastName;
|
|
429
|
+
if (profile.email !== void 0) updateData.email = profile.email;
|
|
430
|
+
await this.request(accountUrl, {
|
|
431
|
+
method: "POST",
|
|
432
|
+
headers: { Authorization: `Bearer ${tokenOrSession}` },
|
|
433
|
+
body: JSON.stringify(updateData)
|
|
434
|
+
});
|
|
435
|
+
return this.getProfile(tokenOrSession);
|
|
436
|
+
}
|
|
437
|
+
async getUser(userId, adminToken) {
|
|
438
|
+
if (!adminToken) throw new AccessDeniedError("Admin token required", "keycloak");
|
|
439
|
+
const response = await this.request(`${this.getAdminUrl()}/users/${userId}`, { method: "GET" }, adminToken);
|
|
440
|
+
return this.mapKeycloakUser(response);
|
|
441
|
+
}
|
|
442
|
+
async createUser(user, adminToken) {
|
|
443
|
+
const keycloakUser = {
|
|
444
|
+
username: user.username,
|
|
445
|
+
email: user.email,
|
|
446
|
+
firstName: user.firstName,
|
|
447
|
+
lastName: user.lastName,
|
|
448
|
+
enabled: user.enabled ?? true,
|
|
449
|
+
emailVerified: user.emailVerified ?? false,
|
|
450
|
+
attributes: user.attributes
|
|
451
|
+
};
|
|
452
|
+
if (user.password) keycloakUser.credentials = [{
|
|
453
|
+
type: "password",
|
|
454
|
+
value: user.password,
|
|
455
|
+
temporary: false
|
|
456
|
+
}];
|
|
457
|
+
const response = await fetch(`${this.getAdminUrl()}/users`, {
|
|
458
|
+
method: "POST",
|
|
459
|
+
headers: {
|
|
460
|
+
"Content-Type": "application/json",
|
|
461
|
+
Authorization: `Bearer ${adminToken}`
|
|
462
|
+
},
|
|
463
|
+
body: JSON.stringify(keycloakUser)
|
|
464
|
+
});
|
|
465
|
+
if (!response.ok) {
|
|
466
|
+
const errorBody = await response.text();
|
|
467
|
+
if (response.status === 409) throw new UserAlreadyExistsError(user.username, "keycloak");
|
|
468
|
+
throw new ProviderError(`Failed to create user: ${errorBody}`, "keycloak");
|
|
469
|
+
}
|
|
470
|
+
const location = response.headers.get("Location");
|
|
471
|
+
if (!location) throw new ProviderError("Failed to get created user ID", "keycloak");
|
|
472
|
+
const userId = location.split("/").pop();
|
|
473
|
+
if (user.roles?.length) await this.assignRoles(userId, user.roles, adminToken);
|
|
474
|
+
if (user.groups?.length) for (const groupName of user.groups) await this.addUserToGroup(userId, groupName, adminToken);
|
|
475
|
+
return this.getUser(userId, adminToken);
|
|
476
|
+
}
|
|
477
|
+
async updateUser(userId, updates, adminToken) {
|
|
478
|
+
const keycloakUser = {};
|
|
479
|
+
if (updates.username !== void 0) keycloakUser.username = updates.username;
|
|
480
|
+
if (updates.email !== void 0) keycloakUser.email = updates.email;
|
|
481
|
+
if (updates.firstName !== void 0) keycloakUser.firstName = updates.firstName;
|
|
482
|
+
if (updates.lastName !== void 0) keycloakUser.lastName = updates.lastName;
|
|
483
|
+
if (updates.enabled !== void 0) keycloakUser.enabled = updates.enabled;
|
|
484
|
+
if (updates.emailVerified !== void 0) keycloakUser.emailVerified = updates.emailVerified;
|
|
485
|
+
if (updates.attributes !== void 0) keycloakUser.attributes = updates.attributes;
|
|
486
|
+
await this.request(`${this.getAdminUrl()}/users/${userId}`, {
|
|
487
|
+
method: "PUT",
|
|
488
|
+
body: JSON.stringify(keycloakUser)
|
|
489
|
+
}, adminToken);
|
|
490
|
+
if (updates.password) await this.request(`${this.getAdminUrl()}/users/${userId}/reset-password`, {
|
|
491
|
+
method: "PUT",
|
|
492
|
+
body: JSON.stringify({
|
|
493
|
+
type: "password",
|
|
494
|
+
value: updates.password,
|
|
495
|
+
temporary: false
|
|
496
|
+
})
|
|
497
|
+
}, adminToken);
|
|
498
|
+
return this.getUser(userId, adminToken);
|
|
499
|
+
}
|
|
500
|
+
async deleteUser(userId, adminToken) {
|
|
501
|
+
await this.request(`${this.getAdminUrl()}/users/${userId}`, { method: "DELETE" }, adminToken);
|
|
502
|
+
}
|
|
503
|
+
async listUsers(query, adminToken) {
|
|
504
|
+
if (!adminToken) throw new AccessDeniedError("Admin token required", "keycloak");
|
|
505
|
+
const params = new URLSearchParams();
|
|
506
|
+
if (query.search) params.set("search", query.search);
|
|
507
|
+
if (query.email) params.set("email", query.email);
|
|
508
|
+
if (query.username) params.set("username", query.username);
|
|
509
|
+
if (query.enabled !== void 0) params.set("enabled", String(query.enabled));
|
|
510
|
+
if (query.limit) params.set("max", String(query.limit));
|
|
511
|
+
if (query.offset) params.set("first", String(query.offset));
|
|
512
|
+
const users = await this.request(`${this.getAdminUrl()}/users?${params.toString()}`, { method: "GET" }, adminToken);
|
|
513
|
+
const countResponse = await this.request(`${this.getAdminUrl()}/users/count?${params.toString()}`, { method: "GET" }, adminToken);
|
|
514
|
+
return {
|
|
515
|
+
users: users.map((u) => this.mapKeycloakUser(u)),
|
|
516
|
+
total: countResponse,
|
|
517
|
+
limit: query.limit || 100,
|
|
518
|
+
offset: query.offset || 0
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
async requestPasswordReset(email) {
|
|
522
|
+
throw new NotImplementedError("requestPasswordReset", "keycloak", { reason: "Requires admin token or use Keycloak login page for self-service reset" });
|
|
523
|
+
}
|
|
524
|
+
async resetPassword(token, newPassword) {
|
|
525
|
+
throw new NotImplementedError("resetPassword", "keycloak", { reason: "Password reset is handled by Keycloak login flow" });
|
|
526
|
+
}
|
|
527
|
+
async listSessions(userId, adminToken) {
|
|
528
|
+
if (!adminToken) throw new AccessDeniedError("Admin token required", "keycloak");
|
|
529
|
+
return (await this.request(`${this.getAdminUrl()}/users/${userId}/sessions`, { method: "GET" }, adminToken)).map((s) => ({
|
|
530
|
+
id: s.id,
|
|
531
|
+
userId: s.userId,
|
|
532
|
+
clientId: s.clients ? Object.keys(s.clients).join(", ") : void 0,
|
|
533
|
+
startedAt: new Date(s.start),
|
|
534
|
+
lastAccessedAt: new Date(s.lastAccess),
|
|
535
|
+
ipAddress: s.ipAddress,
|
|
536
|
+
userAgent: s.userAgent
|
|
537
|
+
}));
|
|
538
|
+
}
|
|
539
|
+
async revokeSession(sessionId, adminToken) {
|
|
540
|
+
if (!adminToken) throw new AccessDeniedError("Admin token required", "keycloak");
|
|
541
|
+
await this.request(`${this.getAdminUrl()}/sessions/${sessionId}`, { method: "DELETE" }, adminToken);
|
|
542
|
+
}
|
|
543
|
+
async revokeAllSessions(userId, adminToken) {
|
|
544
|
+
if (!adminToken) throw new AccessDeniedError("Admin token required", "keycloak");
|
|
545
|
+
await this.request(`${this.getAdminUrl()}/users/${userId}/logout`, { method: "POST" }, adminToken);
|
|
546
|
+
}
|
|
547
|
+
async hasRole(tokenOrUserId, role) {
|
|
548
|
+
return (await this.getRoles(tokenOrUserId)).includes(role);
|
|
549
|
+
}
|
|
550
|
+
async hasPermission(tokenOrUserId, permission, resource) {
|
|
551
|
+
const roles = await this.getRoles(tokenOrUserId);
|
|
552
|
+
const permissionRole = resource ? `${resource}:${permission}` : permission;
|
|
553
|
+
return roles.includes(permissionRole) || roles.includes(permission);
|
|
554
|
+
}
|
|
555
|
+
async getRoles(tokenOrUserId, adminToken) {
|
|
556
|
+
try {
|
|
557
|
+
const claims = await this.validateToken(tokenOrUserId);
|
|
558
|
+
if (claims) return claims.roles || [];
|
|
559
|
+
} catch {}
|
|
560
|
+
if (adminToken) try {
|
|
561
|
+
return (await this.request(`${this.getAdminUrl()}/users/${tokenOrUserId}/role-mappings`, { method: "GET" }, adminToken)).realmMappings?.map((r) => r.name) || [];
|
|
562
|
+
} catch {}
|
|
563
|
+
return [];
|
|
564
|
+
}
|
|
565
|
+
async assignRole(userId, role, adminToken) {
|
|
566
|
+
const roleObj = (await this.request(`${this.getAdminUrl()}/roles`, { method: "GET" }, adminToken)).find((r) => r.name === role);
|
|
567
|
+
if (!roleObj) throw new ProviderError(`Role not found: ${role}`, "keycloak");
|
|
568
|
+
await this.request(`${this.getAdminUrl()}/users/${userId}/role-mappings/realm`, {
|
|
569
|
+
method: "POST",
|
|
570
|
+
body: JSON.stringify([roleObj])
|
|
571
|
+
}, adminToken);
|
|
572
|
+
}
|
|
573
|
+
async removeRole(userId, role, adminToken) {
|
|
574
|
+
const roleObj = (await this.request(`${this.getAdminUrl()}/roles`, { method: "GET" }, adminToken)).find((r) => r.name === role);
|
|
575
|
+
if (!roleObj) throw new ProviderError(`Role not found: ${role}`, "keycloak");
|
|
576
|
+
await this.request(`${this.getAdminUrl()}/users/${userId}/role-mappings/realm`, {
|
|
577
|
+
method: "DELETE",
|
|
578
|
+
body: JSON.stringify([roleObj])
|
|
579
|
+
}, adminToken);
|
|
580
|
+
}
|
|
581
|
+
async getCapabilities() {
|
|
582
|
+
return {
|
|
583
|
+
authorizationCode: true,
|
|
584
|
+
passwordGrant: true,
|
|
585
|
+
clientCredentials: true,
|
|
586
|
+
tokenRefresh: true,
|
|
587
|
+
oidc: true,
|
|
588
|
+
userManagement: true,
|
|
589
|
+
sessionManagement: true,
|
|
590
|
+
rbac: true,
|
|
591
|
+
passwordReset: true,
|
|
592
|
+
mfa: true,
|
|
593
|
+
socialLogin: true,
|
|
594
|
+
federation: true,
|
|
595
|
+
decentralized: false
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
async getDiscoveryDocument() {
|
|
599
|
+
return this.fetchDiscoveryDocument();
|
|
600
|
+
}
|
|
601
|
+
mapKeycloakUser(user) {
|
|
602
|
+
return {
|
|
603
|
+
id: user.id,
|
|
604
|
+
username: user.username,
|
|
605
|
+
email: user.email,
|
|
606
|
+
emailVerified: user.emailVerified,
|
|
607
|
+
firstName: user.firstName,
|
|
608
|
+
lastName: user.lastName,
|
|
609
|
+
displayName: user.firstName && user.lastName ? `${user.firstName} ${user.lastName}` : user.username,
|
|
610
|
+
enabled: user.enabled,
|
|
611
|
+
createdAt: user.createdTimestamp ? new Date(user.createdTimestamp) : void 0,
|
|
612
|
+
attributes: user.attributes,
|
|
613
|
+
groups: user.groups
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
async assignRoles(userId, roleNames, adminToken) {
|
|
617
|
+
const rolesToAssign = (await this.request(`${this.getAdminUrl()}/roles`, { method: "GET" }, adminToken)).filter((r) => roleNames.includes(r.name));
|
|
618
|
+
if (rolesToAssign.length > 0) await this.request(`${this.getAdminUrl()}/users/${userId}/role-mappings/realm`, {
|
|
619
|
+
method: "POST",
|
|
620
|
+
body: JSON.stringify(rolesToAssign)
|
|
621
|
+
}, adminToken);
|
|
622
|
+
}
|
|
623
|
+
async addUserToGroup(userId, groupName, adminToken) {
|
|
624
|
+
const group = (await this.request(`${this.getAdminUrl()}/groups?search=${encodeURIComponent(groupName)}`, { method: "GET" }, adminToken)).find((g) => g.name === groupName);
|
|
625
|
+
if (!group) throw new ProviderError(`Group not found: ${groupName}`, "keycloak");
|
|
626
|
+
await this.request(`${this.getAdminUrl()}/users/${userId}/groups/${group.id}`, { method: "PUT" }, adminToken);
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
//#endregion
|
|
630
|
+
export { KeycloakProvider };
|
|
631
|
+
|
|
632
|
+
//# sourceMappingURL=keycloak-CzPHgI2z.js.map
|