@dloizides/auth-client 1.0.0 → 2.0.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 +113 -0
- package/README.md +138 -42
- package/dist/AuthClient-Dim7HPRz.d.mts +433 -0
- package/dist/AuthClient-Dim7HPRz.d.ts +433 -0
- package/dist/index.d.mts +204 -109
- package/dist/index.d.ts +204 -109
- package/dist/index.js +587 -34
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +580 -35
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +62 -0
- package/dist/react.d.ts +62 -0
- package/dist/react.js +65 -0
- package/dist/react.js.map +1 -0
- package/dist/react.mjs +58 -0
- package/dist/react.mjs.map +1 -0
- package/package.json +43 -5
package/dist/index.d.mts
CHANGED
|
@@ -1,111 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* `realm` and `clientId` are **required** and never have sensible portfolio-wide
|
|
5
|
-
* defaults. Each consumer (Questioner web, OnlineMenu web, future apps) supplies
|
|
6
|
-
* its own values. This is the contract that enables the cross-realm hard wall
|
|
7
|
-
* planned in Phase 2 of the product split.
|
|
8
|
-
*/
|
|
9
|
-
interface AuthClientConfig {
|
|
10
|
-
/** Keycloak base URL, e.g. `https://identity.dloizides.com`. Trailing slash optional. */
|
|
11
|
-
baseUrl: string;
|
|
12
|
-
/** Realm name, e.g. `OnlineMenu` or `Questioner`. NEVER hardcoded. */
|
|
13
|
-
realm: string;
|
|
14
|
-
/** OAuth client id registered in the realm. */
|
|
15
|
-
clientId: string;
|
|
16
|
-
/** Where Keycloak should redirect after authorization (PKCE / authorization-code flow). */
|
|
17
|
-
redirectUri?: string;
|
|
18
|
-
/** Space-separated OAuth scopes. Defaults to `openid profile email`. */
|
|
19
|
-
scope?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Persisted token bundle.
|
|
24
|
-
*
|
|
25
|
-
* `expiresAt` is an absolute UNIX millisecond timestamp computed at
|
|
26
|
-
* acquisition time (`Date.now() + expires_in * 1000`) so consumers can
|
|
27
|
-
* trivially compare against `Date.now()` without re-deriving an expiry clock.
|
|
28
|
-
*/
|
|
29
|
-
interface AuthTokens {
|
|
30
|
-
accessToken: string;
|
|
31
|
-
refreshToken?: string;
|
|
32
|
-
idToken?: string;
|
|
33
|
-
/** Absolute UNIX millis. `0` or missing means unknown — treat as expired. */
|
|
34
|
-
expiresAt: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Pluggable token persistence.
|
|
39
|
-
*
|
|
40
|
-
* Consumers (web, native, server-side) provide an implementation tailored to
|
|
41
|
-
* their platform: `localStorage`, `sessionStorage`, `expo-secure-store`,
|
|
42
|
-
* `AsyncStorage`, or an in-memory map for tests. Keeping the interface narrow
|
|
43
|
-
* (read / write / clear) keeps the package transport-agnostic.
|
|
44
|
-
*/
|
|
45
|
-
interface TokenStorage {
|
|
46
|
-
read(): Promise<AuthTokens | null>;
|
|
47
|
-
write(tokens: AuthTokens): Promise<void>;
|
|
48
|
-
clear(): Promise<void>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Inputs to {@link AuthClient.fromIssuerUrl}.
|
|
53
|
-
*
|
|
54
|
-
* Used by consumers that store only an issuer URL and want to derive `realm`
|
|
55
|
-
* + `baseUrl` rather than configure them separately.
|
|
56
|
-
*/
|
|
57
|
-
interface AuthClientFromIssuerInput {
|
|
58
|
-
issuerUrl: string;
|
|
59
|
-
clientId: string;
|
|
60
|
-
redirectUri?: string;
|
|
61
|
-
scope?: string;
|
|
62
|
-
}
|
|
63
|
-
declare class AuthClient {
|
|
64
|
-
private readonly config;
|
|
65
|
-
private readonly tokenStorage;
|
|
66
|
-
/**
|
|
67
|
-
* @throws Error when `baseUrl`, `realm`, or `clientId` is missing or empty.
|
|
68
|
-
*/
|
|
69
|
-
constructor(config: AuthClientConfig, storage: TokenStorage);
|
|
70
|
-
/**
|
|
71
|
-
* Build an {@link AuthClient} from a standalone issuer URL by parsing the
|
|
72
|
-
* realm and base URL. Useful when migrating from the legacy
|
|
73
|
-
* `KEYCLOAK_ISSUER` env var convention.
|
|
74
|
-
*
|
|
75
|
-
* @throws Error when the issuer URL doesn't match `{base}/realms/{realm}`.
|
|
76
|
-
*/
|
|
77
|
-
static fromIssuerUrl(input: AuthClientFromIssuerInput, storage: TokenStorage): AuthClient;
|
|
78
|
-
private static validateConfig;
|
|
79
|
-
get realm(): string;
|
|
80
|
-
get clientId(): string;
|
|
81
|
-
get baseUrl(): string;
|
|
82
|
-
get scope(): string;
|
|
83
|
-
get redirectUri(): string | undefined;
|
|
84
|
-
/** Issuer URL: `{baseUrl}/realms/{realm}`. */
|
|
85
|
-
get issuerUrl(): string;
|
|
86
|
-
get authorizationEndpoint(): string;
|
|
87
|
-
get tokenEndpoint(): string;
|
|
88
|
-
get userInfoEndpoint(): string;
|
|
89
|
-
get logoutEndpoint(): string;
|
|
90
|
-
/**
|
|
91
|
-
* Build a fully-formed authorization URL the user agent can navigate to.
|
|
92
|
-
*
|
|
93
|
-
* @throws Error when `redirectUri` is not configured.
|
|
94
|
-
*/
|
|
95
|
-
buildAuthorizationUrl(input?: {
|
|
96
|
-
state?: string;
|
|
97
|
-
codeChallenge?: string;
|
|
98
|
-
codeChallengeMethod?: 'S256' | 'plain';
|
|
99
|
-
}): string;
|
|
100
|
-
getTokens(): Promise<AuthTokens | null>;
|
|
101
|
-
setTokens(tokens: AuthTokens): Promise<void>;
|
|
102
|
-
clearTokens(): Promise<void>;
|
|
103
|
-
/**
|
|
104
|
-
* Read the current access token if it exists and is not expired.
|
|
105
|
-
* Returns `null` for "no usable token".
|
|
106
|
-
*/
|
|
107
|
-
getAccessToken(now?: number): Promise<string | null>;
|
|
108
|
-
}
|
|
1
|
+
import { T as TokenStorage, A as AuthTokens } from './AuthClient-Dim7HPRz.mjs';
|
|
2
|
+
export { a as AuthApiClient, b as AuthApiClientOptions, c as AuthClient, d as AuthClientCollaborators, e as AuthClientConfig, f as AuthClientFromIssuerInput, g as AuthEventEmitter, h as AuthEventListener, i as AuthEventName, j as AuthEventUnsubscribe, k as AuthSessionInfo, F as ForgotPasswordRequest, H as HttpClient, l as HttpRequest, m as HttpResponse, I as InactivityStore, n as InactivityTracker, o as InactivityTrackerOptions, L as LoginOptions, p as LogoutOptions, O as OtpLoginRequest, P as PasswordLoginRequest, R as RawAuthLoginResponse, q as RefreshFn, r as RefreshInterceptor, s as RefreshInterceptorOptions, t as ResetPasswordRequest, u as createFetchHttpClient } from './AuthClient-Dim7HPRz.mjs';
|
|
109
3
|
|
|
110
4
|
/**
|
|
111
5
|
* Roles emitted by Keycloak realms in the dloizides.com portfolio.
|
|
@@ -242,6 +136,207 @@ declare class InMemoryTokenStorage implements TokenStorage {
|
|
|
242
136
|
clear(): Promise<void>;
|
|
243
137
|
}
|
|
244
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Web token storage that pairs an in-memory access token with a backend-managed
|
|
141
|
+
* httpOnly + Secure + SameSite=Lax refresh-token cookie.
|
|
142
|
+
*
|
|
143
|
+
* The browser handles the refresh cookie (`__Host-refresh` by default — set by
|
|
144
|
+
* the IdentityService on login and rotated on every `/auth/refresh-cookie`
|
|
145
|
+
* call). JavaScript MUST NOT have access to it, so this adapter intentionally
|
|
146
|
+
* does NOT persist `refreshToken` into the cookie itself; that's the backend's
|
|
147
|
+
* job. The adapter just keeps the access token in memory and exposes the same
|
|
148
|
+
* `TokenStorage` interface as `BrowserStorageTokenStorage` so the rest of the
|
|
149
|
+
* library doesn't need to know which transport is in use.
|
|
150
|
+
*
|
|
151
|
+
* Page reloads drop the access token (memory clears), but the refresh cookie
|
|
152
|
+
* survives — `RefreshInterceptor` swaps it for a new access token via
|
|
153
|
+
* `/auth/refresh-cookie` with `credentials: 'include'`.
|
|
154
|
+
*/
|
|
155
|
+
declare class CookieTokenStorage implements TokenStorage {
|
|
156
|
+
private accessToken;
|
|
157
|
+
private idToken;
|
|
158
|
+
private expiresAt;
|
|
159
|
+
read(): Promise<AuthTokens | null>;
|
|
160
|
+
write(tokens: AuthTokens): Promise<void>;
|
|
161
|
+
clear(): Promise<void>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Subset of `expo-secure-store` we use, abstracted so the package itself never
|
|
166
|
+
* imports `expo-secure-store` (and so web bundles never pull it in).
|
|
167
|
+
*
|
|
168
|
+
* Mobile consumers wire this up at the edge:
|
|
169
|
+
*
|
|
170
|
+
* ```ts
|
|
171
|
+
* import * as SecureStore from 'expo-secure-store';
|
|
172
|
+
* const adapter: SecureStoreLike = {
|
|
173
|
+
* getItemAsync: SecureStore.getItemAsync,
|
|
174
|
+
* setItemAsync: SecureStore.setItemAsync,
|
|
175
|
+
* deleteItemAsync: SecureStore.deleteItemAsync,
|
|
176
|
+
* };
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
interface SecureStoreLike {
|
|
180
|
+
getItemAsync(key: string, options?: {
|
|
181
|
+
requireAuthentication?: boolean;
|
|
182
|
+
}): Promise<string | null>;
|
|
183
|
+
setItemAsync(key: string, value: string, options?: {
|
|
184
|
+
requireAuthentication?: boolean;
|
|
185
|
+
}): Promise<void>;
|
|
186
|
+
deleteItemAsync(key: string, options?: {
|
|
187
|
+
requireAuthentication?: boolean;
|
|
188
|
+
}): Promise<void>;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Optional biometric gate. When provided AND `requireBiometric` is `true`, the
|
|
192
|
+
* gate's `unlock()` is called before reading the refresh token. Used by mobile
|
|
193
|
+
* consumers that opt in to biometric-protected sessions.
|
|
194
|
+
*/
|
|
195
|
+
interface BiometricGateLike {
|
|
196
|
+
unlock(): Promise<void>;
|
|
197
|
+
isEnabled(): boolean;
|
|
198
|
+
}
|
|
199
|
+
interface SecureStoreTokenStorageOptions {
|
|
200
|
+
secureStore: SecureStoreLike;
|
|
201
|
+
/** Defaults applied to every key — usually `'auth'`. */
|
|
202
|
+
keyPrefix?: string;
|
|
203
|
+
/**
|
|
204
|
+
* When true, secure-store reads use `requireAuthentication: true`, prompting
|
|
205
|
+
* the OS biometric / device-passcode dialog (iOS Keychain access control,
|
|
206
|
+
* Android Keystore strongbox).
|
|
207
|
+
*/
|
|
208
|
+
requireAuthentication?: boolean;
|
|
209
|
+
/**
|
|
210
|
+
* Optional biometric gate run BEFORE the secure-store read. Belt-and-braces
|
|
211
|
+
* with `requireAuthentication`: the gate enforces our own retry/lockout
|
|
212
|
+
* semantics, while `requireAuthentication` enforces the OS keychain ACL.
|
|
213
|
+
*/
|
|
214
|
+
biometricGate?: BiometricGateLike;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Persist tokens in iOS Keychain / Android Keystore via `expo-secure-store`.
|
|
218
|
+
*
|
|
219
|
+
* Keys are split (access / refresh / id / expiresAt) rather than stored as a
|
|
220
|
+
* single JSON blob so the OS-level ACL on the refresh token slot can be
|
|
221
|
+
* tightened independently. With `requireAuthentication: true`, reads of any
|
|
222
|
+
* key trigger the OS biometric prompt — that's why we keep it OFF for writes
|
|
223
|
+
* (login flows must not prompt) and ON for reads (boot-time session restore).
|
|
224
|
+
*
|
|
225
|
+
* Storage key shape: `{prefix}.{slot}` (e.g. `auth.refresh`).
|
|
226
|
+
*/
|
|
227
|
+
declare class SecureStoreTokenStorage implements TokenStorage {
|
|
228
|
+
private readonly secureStore;
|
|
229
|
+
private readonly prefix;
|
|
230
|
+
private readonly requireAuthentication;
|
|
231
|
+
private readonly biometricGate;
|
|
232
|
+
constructor(options: SecureStoreTokenStorageOptions);
|
|
233
|
+
read(): Promise<AuthTokens | null>;
|
|
234
|
+
write(tokens: AuthTokens): Promise<void>;
|
|
235
|
+
clear(): Promise<void>;
|
|
236
|
+
private shouldRunBiometricGate;
|
|
237
|
+
private fullKey;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Subset of `expo-local-authentication` we use, abstracted so the package
|
|
242
|
+
* itself never imports `expo-local-authentication`.
|
|
243
|
+
*
|
|
244
|
+
* Mobile consumers wire this up at the edge:
|
|
245
|
+
*
|
|
246
|
+
* ```ts
|
|
247
|
+
* import * as LocalAuthentication from 'expo-local-authentication';
|
|
248
|
+
* const adapter: LocalAuthLike = {
|
|
249
|
+
* hasHardwareAsync: LocalAuthentication.hasHardwareAsync,
|
|
250
|
+
* isEnrolledAsync: LocalAuthentication.isEnrolledAsync,
|
|
251
|
+
* authenticateAsync: (opts) => LocalAuthentication.authenticateAsync(opts),
|
|
252
|
+
* };
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
interface LocalAuthLike {
|
|
256
|
+
hasHardwareAsync(): Promise<boolean>;
|
|
257
|
+
isEnrolledAsync(): Promise<boolean>;
|
|
258
|
+
authenticateAsync(options?: {
|
|
259
|
+
promptMessage?: string;
|
|
260
|
+
cancelLabel?: string;
|
|
261
|
+
disableDeviceFallback?: boolean;
|
|
262
|
+
}): Promise<{
|
|
263
|
+
success: boolean;
|
|
264
|
+
error?: string;
|
|
265
|
+
}>;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Optional persistence so the "enabled" flag survives app restart. Backed by
|
|
269
|
+
* any `TokenStorage`-shaped key/value store via the calling consumer (or the
|
|
270
|
+
* app's settings store). We keep it pluggable to avoid coupling
|
|
271
|
+
* `BiometricGate` to a specific storage adapter.
|
|
272
|
+
*/
|
|
273
|
+
interface BiometricFlagStore {
|
|
274
|
+
read(): Promise<boolean>;
|
|
275
|
+
write(enabled: boolean): Promise<void>;
|
|
276
|
+
}
|
|
277
|
+
interface BiometricGateOptions {
|
|
278
|
+
localAuth: LocalAuthLike;
|
|
279
|
+
/** Optional persistence for the user's opt-in choice. */
|
|
280
|
+
flagStore?: BiometricFlagStore;
|
|
281
|
+
/** Default prompt message; consumers usually override. */
|
|
282
|
+
promptMessage?: string;
|
|
283
|
+
/** Max consecutive prompt failures before {@link unlock} throws. Default 3. */
|
|
284
|
+
maxFailures?: number;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Biometric gate wrapping `expo-local-authentication`.
|
|
288
|
+
*
|
|
289
|
+
* Lifecycle:
|
|
290
|
+
*
|
|
291
|
+
* 1. `isAvailable()` — checks hardware + enrolment. Pure read.
|
|
292
|
+
* 2. `setEnabled(true|false)` — consumer's settings UI flips this. Persisted
|
|
293
|
+
* via the optional flag store. Default = disabled (opt-in).
|
|
294
|
+
* 3. `unlock()` — called by `SecureStoreTokenStorage` (when wired) or by
|
|
295
|
+
* consumer code before sensitive operations. Counts consecutive failures;
|
|
296
|
+
* after `maxFailures` (default 3), throws `BiometricLockedOutError` and
|
|
297
|
+
* consumers MUST navigate to login.
|
|
298
|
+
* 4. `prompt()` — one-shot biometric prompt that doesn't change the failure
|
|
299
|
+
* counter. Useful for re-confirming an action mid-session.
|
|
300
|
+
*
|
|
301
|
+
* The failure counter resets on success.
|
|
302
|
+
*/
|
|
303
|
+
declare class BiometricGate {
|
|
304
|
+
private readonly localAuth;
|
|
305
|
+
private readonly flagStore;
|
|
306
|
+
private readonly promptMessage;
|
|
307
|
+
private readonly maxFailures;
|
|
308
|
+
private enabled;
|
|
309
|
+
private failureCount;
|
|
310
|
+
private hydrated;
|
|
311
|
+
constructor(options: BiometricGateOptions);
|
|
312
|
+
/** Hardware present AND a fingerprint/face ID is enrolled. */
|
|
313
|
+
isAvailable(): Promise<boolean>;
|
|
314
|
+
/** Synchronous read of the current enabled flag (post-hydration). */
|
|
315
|
+
isEnabled(): boolean;
|
|
316
|
+
/** Read the persisted opt-in flag once at app boot. Idempotent. */
|
|
317
|
+
hydrate(): Promise<void>;
|
|
318
|
+
/**
|
|
319
|
+
* Toggle biometric requirement. Persists via {@link BiometricFlagStore} when
|
|
320
|
+
* configured. Resets the failure counter so a re-enable starts fresh.
|
|
321
|
+
*/
|
|
322
|
+
setEnabled(enabled: boolean): Promise<void>;
|
|
323
|
+
/** Reset the failure counter. Tests + consumer recovery flows. */
|
|
324
|
+
resetFailures(): void;
|
|
325
|
+
/**
|
|
326
|
+
* One-shot biometric prompt. Returns `true` on success. Does NOT throw on
|
|
327
|
+
* failure or update the failure counter — useful for action confirmation.
|
|
328
|
+
*/
|
|
329
|
+
prompt(): Promise<boolean>;
|
|
330
|
+
/**
|
|
331
|
+
* Required pre-condition for sensitive token reads. No-op when disabled.
|
|
332
|
+
*
|
|
333
|
+
* @throws Error after {@link maxFailures} consecutive failures.
|
|
334
|
+
* @throws Error on a single failure (lower in the count, but still throws so
|
|
335
|
+
* `SecureStoreTokenStorage.read()` short-circuits).
|
|
336
|
+
*/
|
|
337
|
+
unlock(): Promise<void>;
|
|
338
|
+
}
|
|
339
|
+
|
|
245
340
|
/**
|
|
246
341
|
* Convert a Keycloak `/userinfo` payload into a flat, app-friendly user object.
|
|
247
342
|
*
|
|
@@ -415,4 +510,4 @@ declare function normalizeTokenResponse(raw: RawTokenResponse): TokenResponse;
|
|
|
415
510
|
*/
|
|
416
511
|
declare function tokenResponseToAuthTokens(response: TokenResponse, now?: number): AuthTokens;
|
|
417
512
|
|
|
418
|
-
export {
|
|
513
|
+
export { AuthTokens, type AuthorizationCodeBodyInput, type AuthorizationResponseLike, type AuthorizationUrlInput, type BiometricFlagStore, BiometricGate, type BiometricGateLike, type BiometricGateOptions, BrowserStorageTokenStorage, type BrowserStorageTokenStorageOptions, CookieTokenStorage, InMemoryTokenStorage, KeycloakRoles, type KeycloakUserInfo, type LocalAuthLike, type NormalizedUser, type RawTokenResponse, type RefreshTokenBodyInput, type SecureStoreLike, SecureStoreTokenStorage, type SecureStoreTokenStorageOptions, type StorageLike, type TokenResponse, TokenStorage, buildAuthorizationCodeBody, buildAuthorizationEndpoint, buildAuthorizationUrl, buildIssuerUrl, buildLogoutEndpoint, buildRefreshTokenBody, buildTokenEndpoint, buildUserInfoEndpoint, computeExpiresAt, decodeJwt, extractAuthCode, isKeycloakRole, isTokenExpired, normalizeKeycloakUser, normalizeTokenResponse, parseBaseUrlFromIssuer, parseRealmFromIssuer, tokenResponseToAuthTokens };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,111 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* `realm` and `clientId` are **required** and never have sensible portfolio-wide
|
|
5
|
-
* defaults. Each consumer (Questioner web, OnlineMenu web, future apps) supplies
|
|
6
|
-
* its own values. This is the contract that enables the cross-realm hard wall
|
|
7
|
-
* planned in Phase 2 of the product split.
|
|
8
|
-
*/
|
|
9
|
-
interface AuthClientConfig {
|
|
10
|
-
/** Keycloak base URL, e.g. `https://identity.dloizides.com`. Trailing slash optional. */
|
|
11
|
-
baseUrl: string;
|
|
12
|
-
/** Realm name, e.g. `OnlineMenu` or `Questioner`. NEVER hardcoded. */
|
|
13
|
-
realm: string;
|
|
14
|
-
/** OAuth client id registered in the realm. */
|
|
15
|
-
clientId: string;
|
|
16
|
-
/** Where Keycloak should redirect after authorization (PKCE / authorization-code flow). */
|
|
17
|
-
redirectUri?: string;
|
|
18
|
-
/** Space-separated OAuth scopes. Defaults to `openid profile email`. */
|
|
19
|
-
scope?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Persisted token bundle.
|
|
24
|
-
*
|
|
25
|
-
* `expiresAt` is an absolute UNIX millisecond timestamp computed at
|
|
26
|
-
* acquisition time (`Date.now() + expires_in * 1000`) so consumers can
|
|
27
|
-
* trivially compare against `Date.now()` without re-deriving an expiry clock.
|
|
28
|
-
*/
|
|
29
|
-
interface AuthTokens {
|
|
30
|
-
accessToken: string;
|
|
31
|
-
refreshToken?: string;
|
|
32
|
-
idToken?: string;
|
|
33
|
-
/** Absolute UNIX millis. `0` or missing means unknown — treat as expired. */
|
|
34
|
-
expiresAt: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Pluggable token persistence.
|
|
39
|
-
*
|
|
40
|
-
* Consumers (web, native, server-side) provide an implementation tailored to
|
|
41
|
-
* their platform: `localStorage`, `sessionStorage`, `expo-secure-store`,
|
|
42
|
-
* `AsyncStorage`, or an in-memory map for tests. Keeping the interface narrow
|
|
43
|
-
* (read / write / clear) keeps the package transport-agnostic.
|
|
44
|
-
*/
|
|
45
|
-
interface TokenStorage {
|
|
46
|
-
read(): Promise<AuthTokens | null>;
|
|
47
|
-
write(tokens: AuthTokens): Promise<void>;
|
|
48
|
-
clear(): Promise<void>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Inputs to {@link AuthClient.fromIssuerUrl}.
|
|
53
|
-
*
|
|
54
|
-
* Used by consumers that store only an issuer URL and want to derive `realm`
|
|
55
|
-
* + `baseUrl` rather than configure them separately.
|
|
56
|
-
*/
|
|
57
|
-
interface AuthClientFromIssuerInput {
|
|
58
|
-
issuerUrl: string;
|
|
59
|
-
clientId: string;
|
|
60
|
-
redirectUri?: string;
|
|
61
|
-
scope?: string;
|
|
62
|
-
}
|
|
63
|
-
declare class AuthClient {
|
|
64
|
-
private readonly config;
|
|
65
|
-
private readonly tokenStorage;
|
|
66
|
-
/**
|
|
67
|
-
* @throws Error when `baseUrl`, `realm`, or `clientId` is missing or empty.
|
|
68
|
-
*/
|
|
69
|
-
constructor(config: AuthClientConfig, storage: TokenStorage);
|
|
70
|
-
/**
|
|
71
|
-
* Build an {@link AuthClient} from a standalone issuer URL by parsing the
|
|
72
|
-
* realm and base URL. Useful when migrating from the legacy
|
|
73
|
-
* `KEYCLOAK_ISSUER` env var convention.
|
|
74
|
-
*
|
|
75
|
-
* @throws Error when the issuer URL doesn't match `{base}/realms/{realm}`.
|
|
76
|
-
*/
|
|
77
|
-
static fromIssuerUrl(input: AuthClientFromIssuerInput, storage: TokenStorage): AuthClient;
|
|
78
|
-
private static validateConfig;
|
|
79
|
-
get realm(): string;
|
|
80
|
-
get clientId(): string;
|
|
81
|
-
get baseUrl(): string;
|
|
82
|
-
get scope(): string;
|
|
83
|
-
get redirectUri(): string | undefined;
|
|
84
|
-
/** Issuer URL: `{baseUrl}/realms/{realm}`. */
|
|
85
|
-
get issuerUrl(): string;
|
|
86
|
-
get authorizationEndpoint(): string;
|
|
87
|
-
get tokenEndpoint(): string;
|
|
88
|
-
get userInfoEndpoint(): string;
|
|
89
|
-
get logoutEndpoint(): string;
|
|
90
|
-
/**
|
|
91
|
-
* Build a fully-formed authorization URL the user agent can navigate to.
|
|
92
|
-
*
|
|
93
|
-
* @throws Error when `redirectUri` is not configured.
|
|
94
|
-
*/
|
|
95
|
-
buildAuthorizationUrl(input?: {
|
|
96
|
-
state?: string;
|
|
97
|
-
codeChallenge?: string;
|
|
98
|
-
codeChallengeMethod?: 'S256' | 'plain';
|
|
99
|
-
}): string;
|
|
100
|
-
getTokens(): Promise<AuthTokens | null>;
|
|
101
|
-
setTokens(tokens: AuthTokens): Promise<void>;
|
|
102
|
-
clearTokens(): Promise<void>;
|
|
103
|
-
/**
|
|
104
|
-
* Read the current access token if it exists and is not expired.
|
|
105
|
-
* Returns `null` for "no usable token".
|
|
106
|
-
*/
|
|
107
|
-
getAccessToken(now?: number): Promise<string | null>;
|
|
108
|
-
}
|
|
1
|
+
import { T as TokenStorage, A as AuthTokens } from './AuthClient-Dim7HPRz.js';
|
|
2
|
+
export { a as AuthApiClient, b as AuthApiClientOptions, c as AuthClient, d as AuthClientCollaborators, e as AuthClientConfig, f as AuthClientFromIssuerInput, g as AuthEventEmitter, h as AuthEventListener, i as AuthEventName, j as AuthEventUnsubscribe, k as AuthSessionInfo, F as ForgotPasswordRequest, H as HttpClient, l as HttpRequest, m as HttpResponse, I as InactivityStore, n as InactivityTracker, o as InactivityTrackerOptions, L as LoginOptions, p as LogoutOptions, O as OtpLoginRequest, P as PasswordLoginRequest, R as RawAuthLoginResponse, q as RefreshFn, r as RefreshInterceptor, s as RefreshInterceptorOptions, t as ResetPasswordRequest, u as createFetchHttpClient } from './AuthClient-Dim7HPRz.js';
|
|
109
3
|
|
|
110
4
|
/**
|
|
111
5
|
* Roles emitted by Keycloak realms in the dloizides.com portfolio.
|
|
@@ -242,6 +136,207 @@ declare class InMemoryTokenStorage implements TokenStorage {
|
|
|
242
136
|
clear(): Promise<void>;
|
|
243
137
|
}
|
|
244
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Web token storage that pairs an in-memory access token with a backend-managed
|
|
141
|
+
* httpOnly + Secure + SameSite=Lax refresh-token cookie.
|
|
142
|
+
*
|
|
143
|
+
* The browser handles the refresh cookie (`__Host-refresh` by default — set by
|
|
144
|
+
* the IdentityService on login and rotated on every `/auth/refresh-cookie`
|
|
145
|
+
* call). JavaScript MUST NOT have access to it, so this adapter intentionally
|
|
146
|
+
* does NOT persist `refreshToken` into the cookie itself; that's the backend's
|
|
147
|
+
* job. The adapter just keeps the access token in memory and exposes the same
|
|
148
|
+
* `TokenStorage` interface as `BrowserStorageTokenStorage` so the rest of the
|
|
149
|
+
* library doesn't need to know which transport is in use.
|
|
150
|
+
*
|
|
151
|
+
* Page reloads drop the access token (memory clears), but the refresh cookie
|
|
152
|
+
* survives — `RefreshInterceptor` swaps it for a new access token via
|
|
153
|
+
* `/auth/refresh-cookie` with `credentials: 'include'`.
|
|
154
|
+
*/
|
|
155
|
+
declare class CookieTokenStorage implements TokenStorage {
|
|
156
|
+
private accessToken;
|
|
157
|
+
private idToken;
|
|
158
|
+
private expiresAt;
|
|
159
|
+
read(): Promise<AuthTokens | null>;
|
|
160
|
+
write(tokens: AuthTokens): Promise<void>;
|
|
161
|
+
clear(): Promise<void>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Subset of `expo-secure-store` we use, abstracted so the package itself never
|
|
166
|
+
* imports `expo-secure-store` (and so web bundles never pull it in).
|
|
167
|
+
*
|
|
168
|
+
* Mobile consumers wire this up at the edge:
|
|
169
|
+
*
|
|
170
|
+
* ```ts
|
|
171
|
+
* import * as SecureStore from 'expo-secure-store';
|
|
172
|
+
* const adapter: SecureStoreLike = {
|
|
173
|
+
* getItemAsync: SecureStore.getItemAsync,
|
|
174
|
+
* setItemAsync: SecureStore.setItemAsync,
|
|
175
|
+
* deleteItemAsync: SecureStore.deleteItemAsync,
|
|
176
|
+
* };
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
interface SecureStoreLike {
|
|
180
|
+
getItemAsync(key: string, options?: {
|
|
181
|
+
requireAuthentication?: boolean;
|
|
182
|
+
}): Promise<string | null>;
|
|
183
|
+
setItemAsync(key: string, value: string, options?: {
|
|
184
|
+
requireAuthentication?: boolean;
|
|
185
|
+
}): Promise<void>;
|
|
186
|
+
deleteItemAsync(key: string, options?: {
|
|
187
|
+
requireAuthentication?: boolean;
|
|
188
|
+
}): Promise<void>;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Optional biometric gate. When provided AND `requireBiometric` is `true`, the
|
|
192
|
+
* gate's `unlock()` is called before reading the refresh token. Used by mobile
|
|
193
|
+
* consumers that opt in to biometric-protected sessions.
|
|
194
|
+
*/
|
|
195
|
+
interface BiometricGateLike {
|
|
196
|
+
unlock(): Promise<void>;
|
|
197
|
+
isEnabled(): boolean;
|
|
198
|
+
}
|
|
199
|
+
interface SecureStoreTokenStorageOptions {
|
|
200
|
+
secureStore: SecureStoreLike;
|
|
201
|
+
/** Defaults applied to every key — usually `'auth'`. */
|
|
202
|
+
keyPrefix?: string;
|
|
203
|
+
/**
|
|
204
|
+
* When true, secure-store reads use `requireAuthentication: true`, prompting
|
|
205
|
+
* the OS biometric / device-passcode dialog (iOS Keychain access control,
|
|
206
|
+
* Android Keystore strongbox).
|
|
207
|
+
*/
|
|
208
|
+
requireAuthentication?: boolean;
|
|
209
|
+
/**
|
|
210
|
+
* Optional biometric gate run BEFORE the secure-store read. Belt-and-braces
|
|
211
|
+
* with `requireAuthentication`: the gate enforces our own retry/lockout
|
|
212
|
+
* semantics, while `requireAuthentication` enforces the OS keychain ACL.
|
|
213
|
+
*/
|
|
214
|
+
biometricGate?: BiometricGateLike;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Persist tokens in iOS Keychain / Android Keystore via `expo-secure-store`.
|
|
218
|
+
*
|
|
219
|
+
* Keys are split (access / refresh / id / expiresAt) rather than stored as a
|
|
220
|
+
* single JSON blob so the OS-level ACL on the refresh token slot can be
|
|
221
|
+
* tightened independently. With `requireAuthentication: true`, reads of any
|
|
222
|
+
* key trigger the OS biometric prompt — that's why we keep it OFF for writes
|
|
223
|
+
* (login flows must not prompt) and ON for reads (boot-time session restore).
|
|
224
|
+
*
|
|
225
|
+
* Storage key shape: `{prefix}.{slot}` (e.g. `auth.refresh`).
|
|
226
|
+
*/
|
|
227
|
+
declare class SecureStoreTokenStorage implements TokenStorage {
|
|
228
|
+
private readonly secureStore;
|
|
229
|
+
private readonly prefix;
|
|
230
|
+
private readonly requireAuthentication;
|
|
231
|
+
private readonly biometricGate;
|
|
232
|
+
constructor(options: SecureStoreTokenStorageOptions);
|
|
233
|
+
read(): Promise<AuthTokens | null>;
|
|
234
|
+
write(tokens: AuthTokens): Promise<void>;
|
|
235
|
+
clear(): Promise<void>;
|
|
236
|
+
private shouldRunBiometricGate;
|
|
237
|
+
private fullKey;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Subset of `expo-local-authentication` we use, abstracted so the package
|
|
242
|
+
* itself never imports `expo-local-authentication`.
|
|
243
|
+
*
|
|
244
|
+
* Mobile consumers wire this up at the edge:
|
|
245
|
+
*
|
|
246
|
+
* ```ts
|
|
247
|
+
* import * as LocalAuthentication from 'expo-local-authentication';
|
|
248
|
+
* const adapter: LocalAuthLike = {
|
|
249
|
+
* hasHardwareAsync: LocalAuthentication.hasHardwareAsync,
|
|
250
|
+
* isEnrolledAsync: LocalAuthentication.isEnrolledAsync,
|
|
251
|
+
* authenticateAsync: (opts) => LocalAuthentication.authenticateAsync(opts),
|
|
252
|
+
* };
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
interface LocalAuthLike {
|
|
256
|
+
hasHardwareAsync(): Promise<boolean>;
|
|
257
|
+
isEnrolledAsync(): Promise<boolean>;
|
|
258
|
+
authenticateAsync(options?: {
|
|
259
|
+
promptMessage?: string;
|
|
260
|
+
cancelLabel?: string;
|
|
261
|
+
disableDeviceFallback?: boolean;
|
|
262
|
+
}): Promise<{
|
|
263
|
+
success: boolean;
|
|
264
|
+
error?: string;
|
|
265
|
+
}>;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Optional persistence so the "enabled" flag survives app restart. Backed by
|
|
269
|
+
* any `TokenStorage`-shaped key/value store via the calling consumer (or the
|
|
270
|
+
* app's settings store). We keep it pluggable to avoid coupling
|
|
271
|
+
* `BiometricGate` to a specific storage adapter.
|
|
272
|
+
*/
|
|
273
|
+
interface BiometricFlagStore {
|
|
274
|
+
read(): Promise<boolean>;
|
|
275
|
+
write(enabled: boolean): Promise<void>;
|
|
276
|
+
}
|
|
277
|
+
interface BiometricGateOptions {
|
|
278
|
+
localAuth: LocalAuthLike;
|
|
279
|
+
/** Optional persistence for the user's opt-in choice. */
|
|
280
|
+
flagStore?: BiometricFlagStore;
|
|
281
|
+
/** Default prompt message; consumers usually override. */
|
|
282
|
+
promptMessage?: string;
|
|
283
|
+
/** Max consecutive prompt failures before {@link unlock} throws. Default 3. */
|
|
284
|
+
maxFailures?: number;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Biometric gate wrapping `expo-local-authentication`.
|
|
288
|
+
*
|
|
289
|
+
* Lifecycle:
|
|
290
|
+
*
|
|
291
|
+
* 1. `isAvailable()` — checks hardware + enrolment. Pure read.
|
|
292
|
+
* 2. `setEnabled(true|false)` — consumer's settings UI flips this. Persisted
|
|
293
|
+
* via the optional flag store. Default = disabled (opt-in).
|
|
294
|
+
* 3. `unlock()` — called by `SecureStoreTokenStorage` (when wired) or by
|
|
295
|
+
* consumer code before sensitive operations. Counts consecutive failures;
|
|
296
|
+
* after `maxFailures` (default 3), throws `BiometricLockedOutError` and
|
|
297
|
+
* consumers MUST navigate to login.
|
|
298
|
+
* 4. `prompt()` — one-shot biometric prompt that doesn't change the failure
|
|
299
|
+
* counter. Useful for re-confirming an action mid-session.
|
|
300
|
+
*
|
|
301
|
+
* The failure counter resets on success.
|
|
302
|
+
*/
|
|
303
|
+
declare class BiometricGate {
|
|
304
|
+
private readonly localAuth;
|
|
305
|
+
private readonly flagStore;
|
|
306
|
+
private readonly promptMessage;
|
|
307
|
+
private readonly maxFailures;
|
|
308
|
+
private enabled;
|
|
309
|
+
private failureCount;
|
|
310
|
+
private hydrated;
|
|
311
|
+
constructor(options: BiometricGateOptions);
|
|
312
|
+
/** Hardware present AND a fingerprint/face ID is enrolled. */
|
|
313
|
+
isAvailable(): Promise<boolean>;
|
|
314
|
+
/** Synchronous read of the current enabled flag (post-hydration). */
|
|
315
|
+
isEnabled(): boolean;
|
|
316
|
+
/** Read the persisted opt-in flag once at app boot. Idempotent. */
|
|
317
|
+
hydrate(): Promise<void>;
|
|
318
|
+
/**
|
|
319
|
+
* Toggle biometric requirement. Persists via {@link BiometricFlagStore} when
|
|
320
|
+
* configured. Resets the failure counter so a re-enable starts fresh.
|
|
321
|
+
*/
|
|
322
|
+
setEnabled(enabled: boolean): Promise<void>;
|
|
323
|
+
/** Reset the failure counter. Tests + consumer recovery flows. */
|
|
324
|
+
resetFailures(): void;
|
|
325
|
+
/**
|
|
326
|
+
* One-shot biometric prompt. Returns `true` on success. Does NOT throw on
|
|
327
|
+
* failure or update the failure counter — useful for action confirmation.
|
|
328
|
+
*/
|
|
329
|
+
prompt(): Promise<boolean>;
|
|
330
|
+
/**
|
|
331
|
+
* Required pre-condition for sensitive token reads. No-op when disabled.
|
|
332
|
+
*
|
|
333
|
+
* @throws Error after {@link maxFailures} consecutive failures.
|
|
334
|
+
* @throws Error on a single failure (lower in the count, but still throws so
|
|
335
|
+
* `SecureStoreTokenStorage.read()` short-circuits).
|
|
336
|
+
*/
|
|
337
|
+
unlock(): Promise<void>;
|
|
338
|
+
}
|
|
339
|
+
|
|
245
340
|
/**
|
|
246
341
|
* Convert a Keycloak `/userinfo` payload into a flat, app-friendly user object.
|
|
247
342
|
*
|
|
@@ -415,4 +510,4 @@ declare function normalizeTokenResponse(raw: RawTokenResponse): TokenResponse;
|
|
|
415
510
|
*/
|
|
416
511
|
declare function tokenResponseToAuthTokens(response: TokenResponse, now?: number): AuthTokens;
|
|
417
512
|
|
|
418
|
-
export {
|
|
513
|
+
export { AuthTokens, type AuthorizationCodeBodyInput, type AuthorizationResponseLike, type AuthorizationUrlInput, type BiometricFlagStore, BiometricGate, type BiometricGateLike, type BiometricGateOptions, BrowserStorageTokenStorage, type BrowserStorageTokenStorageOptions, CookieTokenStorage, InMemoryTokenStorage, KeycloakRoles, type KeycloakUserInfo, type LocalAuthLike, type NormalizedUser, type RawTokenResponse, type RefreshTokenBodyInput, type SecureStoreLike, SecureStoreTokenStorage, type SecureStoreTokenStorageOptions, type StorageLike, type TokenResponse, TokenStorage, buildAuthorizationCodeBody, buildAuthorizationEndpoint, buildAuthorizationUrl, buildIssuerUrl, buildLogoutEndpoint, buildRefreshTokenBody, buildTokenEndpoint, buildUserInfoEndpoint, computeExpiresAt, decodeJwt, extractAuthCode, isKeycloakRole, isTokenExpired, normalizeKeycloakUser, normalizeTokenResponse, parseBaseUrlFromIssuer, parseRealmFromIssuer, tokenResponseToAuthTokens };
|