@dloizides/auth-client 2.0.0 → 2.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/dist/index.js CHANGED
@@ -159,11 +159,63 @@ var AuthClient = class _AuthClient {
159
159
  ...config,
160
160
  scope: config.scope ?? DEFAULT_SCOPE
161
161
  };
162
+ this.directKcAuth = config.useDirectKcAuth === true;
162
163
  this.tokenStorage = storage;
163
164
  this.api = collaborators.api;
164
165
  this.interceptor = collaborators.interceptor;
165
166
  this.inactivityTracker = collaborators.inactivityTracker;
166
167
  this.events = collaborators.events ?? new AuthEventEmitter();
168
+ this.onTokenAcquired = collaborators.onTokenAcquired;
169
+ this.onTokenRefreshed = collaborators.onTokenRefreshed;
170
+ }
171
+ /**
172
+ * Whether this client is configured to route auth flows directly to
173
+ * Keycloak (v2.1.0 direct-KC path) instead of through the proxied
174
+ * identity-api `/auth/*` endpoints.
175
+ *
176
+ * Apps can render conditionally on this — e.g. to swap a login form for
177
+ * a "Sign in with Keycloak" redirect button.
178
+ */
179
+ isDirectMode() {
180
+ return this.directKcAuth;
181
+ }
182
+ /**
183
+ * Persist a token bundle produced by an external flow (e.g. the
184
+ * app-side `useKeycloakExchange` hook that consumes the shared
185
+ * `exchangeAuthorizationCode` primitive). Fires `onTokenAcquired` after
186
+ * persistence and marks the inactivity tracker active.
187
+ *
188
+ * Designed for the v2.1.0 direct-KC path where the PKCE code exchange
189
+ * happens in the app's React-Query hook (which needs `useDispatch`/etc.)
190
+ * but the token persistence + observability should still flow through
191
+ * the shared client.
192
+ */
193
+ async acceptDirectKcTokens(response) {
194
+ const tokens = tokenResponseToAuthTokens(response);
195
+ await this.tokenStorage.write(tokens);
196
+ if (this.inactivityTracker !== void 0) {
197
+ await this.inactivityTracker.markActive();
198
+ }
199
+ if (this.onTokenAcquired !== void 0) {
200
+ this.onTokenAcquired(tokens);
201
+ }
202
+ return tokens;
203
+ }
204
+ /**
205
+ * Same as {@link acceptDirectKcTokens} but fires `onTokenRefreshed`.
206
+ * Use after a `refreshAccessToken()` swap to keep observability counts
207
+ * separated between "fresh login" and "silent refresh".
208
+ */
209
+ async acceptDirectKcRefresh(response) {
210
+ const tokens = tokenResponseToAuthTokens(response);
211
+ await this.tokenStorage.write(tokens);
212
+ if (this.inactivityTracker !== void 0) {
213
+ await this.inactivityTracker.markActive();
214
+ }
215
+ if (this.onTokenRefreshed !== void 0) {
216
+ this.onTokenRefreshed(tokens);
217
+ }
218
+ return tokens;
167
219
  }
168
220
  /**
169
221
  * Build an {@link AuthClient} from a standalone issuer URL by parsing the
@@ -308,7 +360,11 @@ var AuthClient = class _AuthClient {
308
360
  if (this.interceptor === void 0) {
309
361
  throw new Error("AuthClient.refresh: no RefreshInterceptor configured");
310
362
  }
311
- return this.interceptor.refreshTokens();
363
+ const tokens = await this.interceptor.refreshTokens();
364
+ if (tokens !== null && this.onTokenRefreshed !== void 0) {
365
+ this.onTokenRefreshed(tokens);
366
+ }
367
+ return tokens;
312
368
  }
313
369
  async loginWithOtp(input) {
314
370
  return this.runLogin(this.requireApi().loginWithOtp({
@@ -355,6 +411,9 @@ var AuthClient = class _AuthClient {
355
411
  if (this.inactivityTracker !== void 0) {
356
412
  await this.inactivityTracker.markActive();
357
413
  }
414
+ if (this.onTokenAcquired !== void 0) {
415
+ this.onTokenAcquired(tokens);
416
+ }
358
417
  return tokens;
359
418
  }
360
419
  requireApi() {
@@ -374,6 +433,152 @@ var AuthClient = class _AuthClient {
374
433
  }
375
434
  };
376
435
 
436
+ // src/oidc/discovery.ts
437
+ var cache = /* @__PURE__ */ new Map();
438
+ function normalizeIssuer(issuerUrl) {
439
+ return issuerUrl.replace(/\/$/, "");
440
+ }
441
+ function isOidcDiscoveryDocument(data) {
442
+ if (data === null || typeof data !== "object") {
443
+ return false;
444
+ }
445
+ const d = data;
446
+ return typeof d.issuer === "string" && d.issuer !== "" && typeof d.authorization_endpoint === "string" && d.authorization_endpoint !== "" && typeof d.token_endpoint === "string" && d.token_endpoint !== "";
447
+ }
448
+ async function fetchDiscoveryDocument(input) {
449
+ const key = normalizeIssuer(input.issuerUrl);
450
+ const cached = cache.get(key);
451
+ if (cached !== void 0) {
452
+ return cached;
453
+ }
454
+ const response = await input.http({
455
+ url: `${key}/.well-known/openid-configuration`,
456
+ method: "GET"
457
+ });
458
+ if (!response.ok) {
459
+ throw new Error(
460
+ `OIDC discovery failed: ${String(response.status)} for ${key}`
461
+ );
462
+ }
463
+ if (!isOidcDiscoveryDocument(response.data)) {
464
+ throw new Error(`OIDC discovery returned invalid metadata for ${key}`);
465
+ }
466
+ cache.set(key, response.data);
467
+ return response.data;
468
+ }
469
+ function clearDiscoveryCache() {
470
+ cache.clear();
471
+ }
472
+
473
+ // src/oidc/pkce.ts
474
+ var VERIFIER_MIN_LENGTH = 43;
475
+ var VERIFIER_MAX_LENGTH = 128;
476
+ var DEFAULT_VERIFIER_LENGTH = 64;
477
+ var RANDOM_BYTES_PER_CHAR = 1;
478
+ var UNRESERVED_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
479
+ function getCrypto() {
480
+ const c = globalThis.crypto;
481
+ if (c === void 0 || c.subtle === void 0) {
482
+ throw new Error("pkce: globalThis.crypto.subtle is required (Node 16+ / modern browser)");
483
+ }
484
+ return c;
485
+ }
486
+ function assertVerifierLength(length) {
487
+ if (length < VERIFIER_MIN_LENGTH || length > VERIFIER_MAX_LENGTH) {
488
+ throw new Error(`pkce: code_verifier length must be ${String(VERIFIER_MIN_LENGTH)}-${String(VERIFIER_MAX_LENGTH)} chars (RFC 7636)`);
489
+ }
490
+ }
491
+ function base64UrlEncode(buffer) {
492
+ const bytes = new Uint8Array(buffer);
493
+ let binary = "";
494
+ for (let i = 0; i < bytes.length; i++) {
495
+ binary += String.fromCharCode(bytes[i]);
496
+ }
497
+ const b64 = globalThis.btoa?.(binary) ?? Buffer.from(binary, "binary").toString("base64");
498
+ let end = b64.length;
499
+ while (end > 0 && b64.charCodeAt(end - 1) === "=".charCodeAt(0)) {
500
+ end -= 1;
501
+ }
502
+ return b64.slice(0, end).replace(/\+/g, "-").replace(/\//g, "_");
503
+ }
504
+ function generateCodeVerifier(length = DEFAULT_VERIFIER_LENGTH) {
505
+ assertVerifierLength(length);
506
+ const crypto = getCrypto();
507
+ const bytes = new Uint8Array(length * RANDOM_BYTES_PER_CHAR);
508
+ crypto.getRandomValues(bytes);
509
+ let out = "";
510
+ for (let i = 0; i < length; i++) {
511
+ const byte = bytes[i];
512
+ out += UNRESERVED_CHARS[byte % UNRESERVED_CHARS.length];
513
+ }
514
+ return out;
515
+ }
516
+ async function deriveCodeChallenge(verifier) {
517
+ assertVerifierLength(verifier.length);
518
+ const crypto = getCrypto();
519
+ const data = new TextEncoder().encode(verifier);
520
+ const digest = await crypto.subtle.digest("SHA-256", data);
521
+ return base64UrlEncode(digest);
522
+ }
523
+ async function generatePkcePair(length) {
524
+ const codeVerifier = generateCodeVerifier(length);
525
+ const codeChallenge = await deriveCodeChallenge(codeVerifier);
526
+ return { codeVerifier, codeChallenge, codeChallengeMethod: "S256" };
527
+ }
528
+
529
+ // src/utils/buildTokenRequestBody.ts
530
+ function buildAuthorizationCodeBody(input) {
531
+ return new URLSearchParams({
532
+ client_id: input.clientId,
533
+ grant_type: "authorization_code",
534
+ code: input.code,
535
+ redirect_uri: input.redirectUri,
536
+ code_verifier: input.codeVerifier
537
+ }).toString();
538
+ }
539
+ function buildRefreshTokenBody(input) {
540
+ return new URLSearchParams({
541
+ client_id: input.clientId,
542
+ grant_type: "refresh_token",
543
+ refresh_token: input.refreshToken
544
+ }).toString();
545
+ }
546
+
547
+ // src/oidc/tokenExchange.ts
548
+ var FORM_HEADERS = {
549
+ "Content-Type": "application/x-www-form-urlencoded"
550
+ };
551
+ async function postTokenEndpoint(http, url, body) {
552
+ const response = await http({
553
+ url,
554
+ method: "POST",
555
+ headers: FORM_HEADERS,
556
+ body
557
+ });
558
+ if (!response.ok) {
559
+ throw new Error(`token endpoint POST failed: ${String(response.status)}`);
560
+ }
561
+ return normalizeTokenResponse(response.data);
562
+ }
563
+ async function exchangeAuthorizationCode(input) {
564
+ const url = buildTokenEndpoint(input.baseUrl, input.realm);
565
+ const body = buildAuthorizationCodeBody({
566
+ clientId: input.clientId,
567
+ code: input.code,
568
+ redirectUri: input.redirectUri,
569
+ codeVerifier: input.codeVerifier
570
+ });
571
+ return postTokenEndpoint(input.http, url, body);
572
+ }
573
+ async function refreshAccessToken(input) {
574
+ const url = buildTokenEndpoint(input.baseUrl, input.realm);
575
+ const body = buildRefreshTokenBody({
576
+ clientId: input.clientId,
577
+ refreshToken: input.refreshToken
578
+ });
579
+ return postTokenEndpoint(input.http, url, body);
580
+ }
581
+
377
582
  // src/types/KeycloakRoles.ts
378
583
  var KeycloakRoles = /* @__PURE__ */ ((KeycloakRoles2) => {
379
584
  KeycloakRoles2["SuperUser"] = "superUser";
@@ -904,24 +1109,6 @@ function normalizeKeycloakUser(u) {
904
1109
  };
905
1110
  }
906
1111
 
907
- // src/utils/buildTokenRequestBody.ts
908
- function buildAuthorizationCodeBody(input) {
909
- return new URLSearchParams({
910
- client_id: input.clientId,
911
- grant_type: "authorization_code",
912
- code: input.code,
913
- redirect_uri: input.redirectUri,
914
- code_verifier: input.codeVerifier
915
- }).toString();
916
- }
917
- function buildRefreshTokenBody(input) {
918
- return new URLSearchParams({
919
- client_id: input.clientId,
920
- grant_type: "refresh_token",
921
- refresh_token: input.refreshToken
922
- }).toString();
923
- }
924
-
925
1112
  // src/utils/extractAuthCode.ts
926
1113
  function extractAuthCode(response) {
927
1114
  if (!response) {
@@ -1001,16 +1188,23 @@ exports.buildLogoutEndpoint = buildLogoutEndpoint;
1001
1188
  exports.buildRefreshTokenBody = buildRefreshTokenBody;
1002
1189
  exports.buildTokenEndpoint = buildTokenEndpoint;
1003
1190
  exports.buildUserInfoEndpoint = buildUserInfoEndpoint;
1191
+ exports.clearDiscoveryCache = clearDiscoveryCache;
1004
1192
  exports.computeExpiresAt = computeExpiresAt;
1005
1193
  exports.createFetchHttpClient = createFetchHttpClient;
1006
1194
  exports.decodeJwt = decodeJwt;
1195
+ exports.deriveCodeChallenge = deriveCodeChallenge;
1196
+ exports.exchangeAuthorizationCode = exchangeAuthorizationCode;
1007
1197
  exports.extractAuthCode = extractAuthCode;
1198
+ exports.fetchDiscoveryDocument = fetchDiscoveryDocument;
1199
+ exports.generateCodeVerifier = generateCodeVerifier;
1200
+ exports.generatePkcePair = generatePkcePair;
1008
1201
  exports.isKeycloakRole = isKeycloakRole;
1009
1202
  exports.isTokenExpired = isTokenExpired;
1010
1203
  exports.normalizeKeycloakUser = normalizeKeycloakUser;
1011
1204
  exports.normalizeTokenResponse = normalizeTokenResponse;
1012
1205
  exports.parseBaseUrlFromIssuer = parseBaseUrlFromIssuer;
1013
1206
  exports.parseRealmFromIssuer = parseRealmFromIssuer;
1207
+ exports.refreshAccessToken = refreshAccessToken;
1014
1208
  exports.tokenResponseToAuthTokens = tokenResponseToAuthTokens;
1015
1209
  //# sourceMappingURL=index.js.map
1016
1210
  //# sourceMappingURL=index.js.map