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