@syndew/oluso-auth 0.0.0-stage → 1.30.1
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/LICENSE +76 -0
- package/README.md +42 -2
- package/fesm2022/syndew-oluso-auth.mjs +1985 -0
- package/fesm2022/syndew-oluso-auth.mjs.map +1 -0
- package/package.json +47 -3
- package/types/syndew-oluso-auth.d.ts +776 -0
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
import * as _angular_core from '@angular/core';
|
|
3
|
+
import { InjectionToken } from '@angular/core';
|
|
4
|
+
import { CanActivateFn } from '@angular/router';
|
|
5
|
+
import { HttpInterceptorFn, HttpErrorResponse, HttpRequest, HttpClient } from '@angular/common/http';
|
|
6
|
+
|
|
7
|
+
interface AuthUser {
|
|
8
|
+
id: string;
|
|
9
|
+
email: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
userName?: string;
|
|
12
|
+
displayName?: string;
|
|
13
|
+
firstName?: string;
|
|
14
|
+
lastName?: string;
|
|
15
|
+
picture?: string;
|
|
16
|
+
emailVerified?: boolean;
|
|
17
|
+
roles?: string[];
|
|
18
|
+
permissions?: string[];
|
|
19
|
+
metadata?: Record<string, unknown>;
|
|
20
|
+
organizationIds?: string[];
|
|
21
|
+
organizationRoles?: Record<string, string>;
|
|
22
|
+
isOrgAdmin?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface OrganizationContext {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
slug?: string;
|
|
29
|
+
type?: 'personal' | 'team' | 'enterprise';
|
|
30
|
+
role?: string;
|
|
31
|
+
permissions?: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface AuthState {
|
|
35
|
+
user: AuthUser | null;
|
|
36
|
+
accessToken: string | null;
|
|
37
|
+
refreshToken?: string | null;
|
|
38
|
+
idToken?: string | null;
|
|
39
|
+
isAuthenticated: boolean;
|
|
40
|
+
isLoading: boolean;
|
|
41
|
+
error: string | null;
|
|
42
|
+
currentTenantId: string | null;
|
|
43
|
+
currentOrganization: OrganizationContext | null;
|
|
44
|
+
isSuperAdmin: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface LoginRequest {
|
|
48
|
+
username: string;
|
|
49
|
+
password: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface LoginResponse {
|
|
53
|
+
user: AuthUser;
|
|
54
|
+
accessToken: string;
|
|
55
|
+
refreshToken?: string;
|
|
56
|
+
idToken?: string;
|
|
57
|
+
expiresIn?: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface OidcConfig {
|
|
61
|
+
authority: string;
|
|
62
|
+
clientId: string;
|
|
63
|
+
redirectUri: string;
|
|
64
|
+
postLogoutRedirectUri?: string;
|
|
65
|
+
scope?: string;
|
|
66
|
+
responseType?: string;
|
|
67
|
+
silentRedirectUri?: string;
|
|
68
|
+
automaticSilentRenew?: boolean;
|
|
69
|
+
loadUserInfo?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface TokenPayload {
|
|
73
|
+
sub: string;
|
|
74
|
+
iss: string;
|
|
75
|
+
aud: string | string[];
|
|
76
|
+
exp: number;
|
|
77
|
+
iat: number;
|
|
78
|
+
email?: string;
|
|
79
|
+
name?: string;
|
|
80
|
+
roles?: string[];
|
|
81
|
+
permissions?: string[];
|
|
82
|
+
tenant_id?: string;
|
|
83
|
+
[key: string]: unknown;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface AuthRefreshResult {
|
|
87
|
+
isAuthenticated: boolean;
|
|
88
|
+
accessToken: string | null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Implemented by the host app to bridge auth concerns to whatever auth library
|
|
92
|
+
* (angular-auth-oidc-client, MSAL, etc.) is in use. Keeps libs/auth free of
|
|
93
|
+
* library-specific dependencies — swapping providers is a single-file change.
|
|
94
|
+
*/
|
|
95
|
+
declare abstract class AuthProvider {
|
|
96
|
+
abstract readonly isAuthenticated$: Observable<boolean>;
|
|
97
|
+
abstract getAccessToken(): Observable<string | null>;
|
|
98
|
+
abstract forceRefreshSession(): Observable<AuthRefreshResult>;
|
|
99
|
+
/**
|
|
100
|
+
* Refreshes the session into another tenant: the library's new tokens (access and refresh) are for
|
|
101
|
+
* `tenantId`, and later refreshes stay there. Errors when the server refuses the tenant (the user has no
|
|
102
|
+
* access to it); the session stays signed in, where it was.
|
|
103
|
+
*/
|
|
104
|
+
abstract switchTenant(tenantId: string): Observable<AuthRefreshResult>;
|
|
105
|
+
abstract authorize(): void;
|
|
106
|
+
}
|
|
107
|
+
declare class NoOpAuthProvider extends AuthProvider {
|
|
108
|
+
readonly isAuthenticated$: Observable<boolean>;
|
|
109
|
+
getAccessToken(): Observable<string | null>;
|
|
110
|
+
forceRefreshSession(): Observable<AuthRefreshResult>;
|
|
111
|
+
switchTenant(): Observable<AuthRefreshResult>;
|
|
112
|
+
authorize(): void;
|
|
113
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<NoOpAuthProvider, never>;
|
|
114
|
+
static ɵprov: _angular_core.ɵɵInjectableDeclaration<NoOpAuthProvider>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Serialises token refreshes against the auth library's own background renew.
|
|
119
|
+
*
|
|
120
|
+
* With silent renew on, the library refreshes on a timer. If an API call fails
|
|
121
|
+
* with 401 while that renew is still in flight, a second refresh request is
|
|
122
|
+
* declined by the library, which reports "no session" even though the renew is
|
|
123
|
+
* about to succeed. Left alone, the interceptor would treat that as a signed-out
|
|
124
|
+
* user and redirect to login. Instead, wait for the running renew to settle and
|
|
125
|
+
* answer with the token it produced.
|
|
126
|
+
*
|
|
127
|
+
* Library-agnostic: the adapter supplies the "renew started" and "renew settled"
|
|
128
|
+
* streams and the single-shot refresh call.
|
|
129
|
+
*/
|
|
130
|
+
declare abstract class RenewAwareAuthProvider extends AuthProvider {
|
|
131
|
+
/** How long to wait for a running renew before giving up; matches the library's own timeout. */
|
|
132
|
+
protected renewWaitMs: number;
|
|
133
|
+
private renewInFlight;
|
|
134
|
+
private readonly settled$;
|
|
135
|
+
/** One refresh attempt through the library. Reports unauthenticated when the library declines. */
|
|
136
|
+
protected abstract refreshOnce(): Observable<AuthRefreshResult>;
|
|
137
|
+
/**
|
|
138
|
+
* Adapters call this once, in their constructor, with the library's events:
|
|
139
|
+
* `started$` whenever a background renew begins, `settled$` with true/false
|
|
140
|
+
* whenever a renew (or any authentication result) completes.
|
|
141
|
+
*/
|
|
142
|
+
protected trackRenew(started$: Observable<unknown>, settled$: Observable<boolean>): void;
|
|
143
|
+
forceRefreshSession(): Observable<AuthRefreshResult>;
|
|
144
|
+
/**
|
|
145
|
+
* A refresh that switches tenant: {@link requestTenantSwitch} adds the token endpoint's `tenant` parameter to
|
|
146
|
+
* that one refresh request, outside the library, so the library neither keeps it for later refreshes nor sees
|
|
147
|
+
* a refusal (which it would answer by signing the user out). A renew already running is let finish first, so
|
|
148
|
+
* the switch redeems the refresh token it leaves behind. Errors unless the new token is for `tenantId`; the
|
|
149
|
+
* session is left where it was.
|
|
150
|
+
*/
|
|
151
|
+
switchTenant(tenantId: string): Observable<AuthRefreshResult>;
|
|
152
|
+
private awaitRenew;
|
|
153
|
+
}
|
|
154
|
+
/** The tenant an access token is for (its `tenant_id` claim), or null when it names none or is not a JWT. */
|
|
155
|
+
declare function tenantOfToken(accessToken: string): string | null;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Tells the app's other tabs when a refresh token has been spent, and hands them what it was exchanged for.
|
|
159
|
+
*
|
|
160
|
+
* Refresh tokens are single use. Tabs can hold the same one: a duplicated tab starts with a copy of its
|
|
161
|
+
* opener's session, and remembered sign-ins share one store across every tab. When one tab redeems it, the
|
|
162
|
+
* others must take the rotated tokens rather than present the spent one, which the server refuses.
|
|
163
|
+
*
|
|
164
|
+
* The tokens travel encrypted under a key derived from the spent refresh token, so only a tab that already
|
|
165
|
+
* held that token can read them; any other tab of the app (another user's session-only sign-in, say) learns
|
|
166
|
+
* nothing. An answer is kept for {@link ROTATION_ANSWER_TTL_MS}, well inside the 120 s the OIDC client
|
|
167
|
+
* allows between an ID token being issued and accepted.
|
|
168
|
+
*/
|
|
169
|
+
/** The token entries of the OIDC client's stored state, as written after a refresh. */
|
|
170
|
+
type RotatedTokens = Record<string, unknown>;
|
|
171
|
+
declare class TokenRotationChannel {
|
|
172
|
+
private readonly answers;
|
|
173
|
+
private readonly waiters;
|
|
174
|
+
private readonly publications;
|
|
175
|
+
private readonly listeners;
|
|
176
|
+
private readonly channel;
|
|
177
|
+
constructor(channelName?: string);
|
|
178
|
+
/**
|
|
179
|
+
* Records that `spentRefreshToken` was exchanged for `tokens` and tells the other tabs. Resolves once the
|
|
180
|
+
* other tabs have been told.
|
|
181
|
+
*/
|
|
182
|
+
publish(spentRefreshToken: string, tokens: RotatedTokens): Promise<void>;
|
|
183
|
+
/** Resolves once this tab has published the rotation of `refreshToken`, or after `timeoutMs`. */
|
|
184
|
+
awaitPublished(refreshToken: string, timeoutMs: number): Promise<void>;
|
|
185
|
+
/** What `refreshToken` was exchanged for, in this tab or another, while that answer is fresh; else null. */
|
|
186
|
+
answerFor(refreshToken: string): Promise<RotatedTokens | null>;
|
|
187
|
+
/** {@link answerFor}, waiting up to `timeoutMs` for another tab's rotation of `refreshToken` to arrive. */
|
|
188
|
+
waitForAnswer(refreshToken: string, timeoutMs: number): Promise<RotatedTokens | null>;
|
|
189
|
+
/** Calls `listener` with the id of every rotation that arrives from another tab. Returns the unsubscribe. */
|
|
190
|
+
onRotation(listener: (id: string) => void): () => void;
|
|
191
|
+
/** Whether `refreshToken` is the token a rotation with `id` spent. */
|
|
192
|
+
isSpentBy(refreshToken: string, id: string): Promise<boolean>;
|
|
193
|
+
private remember;
|
|
194
|
+
private publication;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Token storage that follows the user's "Remember me" choice at sign-in.
|
|
199
|
+
*
|
|
200
|
+
* The OIDC client hands this one JSON blob per configuration. Until sign-in
|
|
201
|
+
* completes the blob only holds the pending state (nonce, code verifier) and
|
|
202
|
+
* stays in session storage, which the callback in the same tab can read. Once
|
|
203
|
+
* the blob contains a token response, the ID token says whether the sign-in was
|
|
204
|
+
* remembered: remembered sessions move to local storage, so they survive a
|
|
205
|
+
* browser restart and are shared across tabs; session-only sign-ins stay per
|
|
206
|
+
* tab and vanish when it closes. Sign-out removes both copies.
|
|
207
|
+
*
|
|
208
|
+
* Refresh tokens are single use, and tabs can hold the same one (a duplicated
|
|
209
|
+
* tab starts with a copy of its opener's session; remembered sessions share
|
|
210
|
+
* one store). When a write replaces the refresh token, the spent token and the
|
|
211
|
+
* new tokens go to the app's other tabs; a tab still holding the spent token
|
|
212
|
+
* takes the new ones, so it never presents a token the server has retired.
|
|
213
|
+
*
|
|
214
|
+
* Kept free of the OIDC library: it matches the storage contract by shape and
|
|
215
|
+
* is registered by the app's auth provider.
|
|
216
|
+
*/
|
|
217
|
+
declare class RememberMeSecurityStorage {
|
|
218
|
+
private readonly rotations;
|
|
219
|
+
private readonly pendingRotations;
|
|
220
|
+
constructor(rotations?: TokenRotationChannel | null);
|
|
221
|
+
read(key: string): string | null;
|
|
222
|
+
write(key: string, value: string): void;
|
|
223
|
+
private publishRotation;
|
|
224
|
+
/** Takes the tokens another tab rotated to, for every stored session still holding the token it spent. */
|
|
225
|
+
private adoptRotation;
|
|
226
|
+
private authKeys;
|
|
227
|
+
private store;
|
|
228
|
+
remove(key: string): void;
|
|
229
|
+
clear(): void;
|
|
230
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<RememberMeSecurityStorage, [{ optional: true; }]>;
|
|
231
|
+
static ɵprov: _angular_core.ɵɵInjectableDeclaration<RememberMeSecurityStorage>;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The parts of the app's OIDC client the startup check needs; the library's security service fits as is. */
|
|
235
|
+
interface StartupAuthClient<R extends {
|
|
236
|
+
isAuthenticated: boolean;
|
|
237
|
+
}> {
|
|
238
|
+
/** Validates the stored tokens (or completes a sign-in callback) without calling the server. */
|
|
239
|
+
checkAuth(): Observable<R>;
|
|
240
|
+
/** {@link checkAuth}, then, when that finds no valid session, a refresh with the stored refresh token. */
|
|
241
|
+
checkAuthIncludingServer(): Observable<R>;
|
|
242
|
+
getRefreshToken(): Observable<string | null | undefined>;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The app's startup auth check. A stored access token that has expired (or is inside the renew window) makes the
|
|
246
|
+
* plain check report "signed out", and the app would send the user through sign-in again although the refresh
|
|
247
|
+
* token is good for days. When a refresh token is stored this renews with it instead, which also restarts the
|
|
248
|
+
* library's background renew. A sign-in callback is completed by the plain check alone, and a refresh the server
|
|
249
|
+
* refuses (refresh token expired or revoked) leaves the user signed out, as before.
|
|
250
|
+
*
|
|
251
|
+
* The refresh goes through the app's HttpClient, so the token-refresh interceptor still coordinates it with the
|
|
252
|
+
* app's other tabs.
|
|
253
|
+
*/
|
|
254
|
+
declare function checkAuthRenewingSession<R extends {
|
|
255
|
+
isAuthenticated: boolean;
|
|
256
|
+
}>(client: StartupAuthClient<R>, url?: string): Observable<R>;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Auth Store
|
|
260
|
+
*
|
|
261
|
+
* Signal-based authentication state management.
|
|
262
|
+
* This mirrors the Zustand authSlice from the React codebase.
|
|
263
|
+
*
|
|
264
|
+
* Features:
|
|
265
|
+
* - Persistent state via localStorage
|
|
266
|
+
* - Tenant/organization context
|
|
267
|
+
* - Super admin support
|
|
268
|
+
*/
|
|
269
|
+
declare class AuthStore {
|
|
270
|
+
private readonly authProvider;
|
|
271
|
+
private readonly _user;
|
|
272
|
+
private readonly _accessToken;
|
|
273
|
+
private readonly _refreshToken;
|
|
274
|
+
private readonly _idToken;
|
|
275
|
+
private readonly _isLoading;
|
|
276
|
+
private readonly _error;
|
|
277
|
+
private readonly _currentTenantId;
|
|
278
|
+
/** True once the user has picked a tenant in the UI; only then do requests carry X-Tenant-ID. */
|
|
279
|
+
private readonly _tenantSwitched;
|
|
280
|
+
private readonly _currentOrganization;
|
|
281
|
+
private readonly _isSuperAdmin;
|
|
282
|
+
private readonly _hasHydrated;
|
|
283
|
+
/** Read-only computed state */
|
|
284
|
+
readonly user: _angular_core.Signal<AuthUser | null>;
|
|
285
|
+
readonly accessToken: _angular_core.Signal<string | null>;
|
|
286
|
+
readonly refreshToken: _angular_core.Signal<string | null>;
|
|
287
|
+
readonly idToken: _angular_core.Signal<string | null>;
|
|
288
|
+
readonly isLoading: _angular_core.Signal<boolean>;
|
|
289
|
+
readonly error: _angular_core.Signal<string | null>;
|
|
290
|
+
readonly currentTenantId: _angular_core.Signal<string | null>;
|
|
291
|
+
readonly tenantSwitched: _angular_core.Signal<boolean>;
|
|
292
|
+
readonly currentOrganization: _angular_core.Signal<OrganizationContext | null>;
|
|
293
|
+
readonly isSuperAdmin: _angular_core.Signal<boolean>;
|
|
294
|
+
readonly hasHydrated: _angular_core.Signal<boolean>;
|
|
295
|
+
readonly isAuthenticated: _angular_core.Signal<boolean>;
|
|
296
|
+
/** Full state snapshot */
|
|
297
|
+
readonly state: _angular_core.Signal<AuthState>;
|
|
298
|
+
constructor();
|
|
299
|
+
/**
|
|
300
|
+
* Set user and tokens after successful login.
|
|
301
|
+
* Extracts tenant_id from the JWT access token if present.
|
|
302
|
+
*/
|
|
303
|
+
login(user: AuthUser, accessToken: string, refreshToken?: string, idToken?: string): void;
|
|
304
|
+
/**
|
|
305
|
+
* Makes the current tenant the one the token is for. A switch mints a token for its target, so a current
|
|
306
|
+
* tenant naming any other was left by an earlier session (or another app on the origin): it is replaced, and
|
|
307
|
+
* the session counts as not switched, so requests carry no X-Tenant-ID.
|
|
308
|
+
*/
|
|
309
|
+
alignTenantWithToken(accessToken: string): void;
|
|
310
|
+
/**
|
|
311
|
+
* Clear authentication state on logout
|
|
312
|
+
*/
|
|
313
|
+
logout(): void;
|
|
314
|
+
/**
|
|
315
|
+
* Update user information
|
|
316
|
+
*/
|
|
317
|
+
setUser(user: AuthUser | null): void;
|
|
318
|
+
/**
|
|
319
|
+
* Update access token
|
|
320
|
+
*/
|
|
321
|
+
setAccessToken(token: string | null): void;
|
|
322
|
+
/**
|
|
323
|
+
* Update refresh token
|
|
324
|
+
*/
|
|
325
|
+
setRefreshToken(token: string | null): void;
|
|
326
|
+
/**
|
|
327
|
+
* Set current tenant ID (for super admin)
|
|
328
|
+
*/
|
|
329
|
+
setCurrentTenantId(tenantId: string | null): void;
|
|
330
|
+
/**
|
|
331
|
+
* Adopt a tenant switch: the new context and the token minted for it.
|
|
332
|
+
*
|
|
333
|
+
* Both move together deliberately. Roles and permissions are per-tenant, so a
|
|
334
|
+
* moment where the header names one tenant and the token carries another
|
|
335
|
+
* either over-grants or under-grants — and a persist() between the two writes
|
|
336
|
+
* would make that state survive a reload.
|
|
337
|
+
*/
|
|
338
|
+
switchTenant(tenantId: string, accessToken: string): void;
|
|
339
|
+
/**
|
|
340
|
+
* Set current organization context
|
|
341
|
+
*/
|
|
342
|
+
setCurrentOrganization(organization: OrganizationContext | null): void;
|
|
343
|
+
/**
|
|
344
|
+
* Set super admin status
|
|
345
|
+
*/
|
|
346
|
+
setSuperAdmin(isSuperAdmin: boolean): void;
|
|
347
|
+
/**
|
|
348
|
+
* Set loading state
|
|
349
|
+
*/
|
|
350
|
+
setLoading(isLoading: boolean): void;
|
|
351
|
+
/**
|
|
352
|
+
* Set error message
|
|
353
|
+
*/
|
|
354
|
+
setError(error: string | null): void;
|
|
355
|
+
/**
|
|
356
|
+
* Hydrate state from localStorage
|
|
357
|
+
*/
|
|
358
|
+
private hydrate;
|
|
359
|
+
/**
|
|
360
|
+
* Persist state to localStorage
|
|
361
|
+
*/
|
|
362
|
+
private persist;
|
|
363
|
+
/**
|
|
364
|
+
* Clear localStorage
|
|
365
|
+
*/
|
|
366
|
+
private clearStorage;
|
|
367
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<AuthStore, never>;
|
|
368
|
+
static ɵprov: _angular_core.ɵɵInjectableDeclaration<AuthStore>;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Auth Service
|
|
373
|
+
*
|
|
374
|
+
* Handles authentication operations including login, logout, token refresh,
|
|
375
|
+
* and user management.
|
|
376
|
+
*
|
|
377
|
+
* This mirrors the auth services from the React codebase.
|
|
378
|
+
*/
|
|
379
|
+
declare class AuthService {
|
|
380
|
+
private readonly http;
|
|
381
|
+
private readonly authStore;
|
|
382
|
+
private apiBaseUrl;
|
|
383
|
+
/**
|
|
384
|
+
* Configure the API base URL
|
|
385
|
+
*/
|
|
386
|
+
setApiBaseUrl(url: string): void;
|
|
387
|
+
/**
|
|
388
|
+
* Login with email and password
|
|
389
|
+
*/
|
|
390
|
+
login(request: LoginRequest): Observable<LoginResponse>;
|
|
391
|
+
/**
|
|
392
|
+
* Logout the current user
|
|
393
|
+
*/
|
|
394
|
+
logout(): Observable<void>;
|
|
395
|
+
/**
|
|
396
|
+
* Logout locally without API call
|
|
397
|
+
*/
|
|
398
|
+
logoutLocal(): void;
|
|
399
|
+
/**
|
|
400
|
+
* Refresh the access token
|
|
401
|
+
*/
|
|
402
|
+
refreshToken(): Observable<{
|
|
403
|
+
accessToken: string;
|
|
404
|
+
refreshToken?: string;
|
|
405
|
+
}>;
|
|
406
|
+
/**
|
|
407
|
+
* Get the current user profile
|
|
408
|
+
*/
|
|
409
|
+
getProfile(): Observable<AuthUser>;
|
|
410
|
+
/**
|
|
411
|
+
* Update the current user profile
|
|
412
|
+
*/
|
|
413
|
+
updateProfile(data: Partial<AuthUser>): Observable<AuthUser>;
|
|
414
|
+
/**
|
|
415
|
+
* Check if the current access token is expired
|
|
416
|
+
*/
|
|
417
|
+
isTokenExpired(): boolean;
|
|
418
|
+
/**
|
|
419
|
+
* Decode a JWT token
|
|
420
|
+
*/
|
|
421
|
+
decodeToken(token: string): TokenPayload;
|
|
422
|
+
/**
|
|
423
|
+
* Get the current access token
|
|
424
|
+
*/
|
|
425
|
+
getAccessToken(): string | null;
|
|
426
|
+
/**
|
|
427
|
+
* Check if the user is authenticated
|
|
428
|
+
*/
|
|
429
|
+
isAuthenticated(): boolean;
|
|
430
|
+
/**
|
|
431
|
+
* Get the current user
|
|
432
|
+
*/
|
|
433
|
+
getUser(): AuthUser | null;
|
|
434
|
+
/**
|
|
435
|
+
* Check if the user is a super admin
|
|
436
|
+
*/
|
|
437
|
+
isSuperAdmin(): boolean;
|
|
438
|
+
/**
|
|
439
|
+
* Get the current tenant ID
|
|
440
|
+
*/
|
|
441
|
+
getCurrentTenantId(): string | null;
|
|
442
|
+
/**
|
|
443
|
+
* Set the current tenant ID (for super admin)
|
|
444
|
+
*/
|
|
445
|
+
setCurrentTenantId(tenantId: string | null): void;
|
|
446
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<AuthService, never>;
|
|
447
|
+
static ɵprov: _angular_core.ɵɵInjectableDeclaration<AuthService>;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Auth Guard
|
|
452
|
+
*
|
|
453
|
+
* Protects routes that require authentication.
|
|
454
|
+
* Redirects to login if user is not authenticated.
|
|
455
|
+
*/
|
|
456
|
+
declare const authGuard: CanActivateFn;
|
|
457
|
+
/**
|
|
458
|
+
* Guest Guard
|
|
459
|
+
*
|
|
460
|
+
* Protects routes that should only be accessible to non-authenticated users.
|
|
461
|
+
* Redirects to dashboard if user is already authenticated.
|
|
462
|
+
*/
|
|
463
|
+
declare const guestGuard: CanActivateFn;
|
|
464
|
+
/**
|
|
465
|
+
* Super Admin Guard
|
|
466
|
+
*
|
|
467
|
+
* Protects routes that require super admin privileges.
|
|
468
|
+
* Checks PermissionsService which is set during login/permission loading.
|
|
469
|
+
*/
|
|
470
|
+
declare const superAdminGuard: CanActivateFn;
|
|
471
|
+
/**
|
|
472
|
+
* Org Admin Guard
|
|
473
|
+
*
|
|
474
|
+
* Protects routes that require org admin or super admin privileges.
|
|
475
|
+
* Super admins can also access org admin routes.
|
|
476
|
+
* Requires a valid organization context (currentOrganization) in the auth store.
|
|
477
|
+
*/
|
|
478
|
+
declare const orgAdminGuard: CanActivateFn;
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* How long a permission guard waits for the app to load the current user's
|
|
482
|
+
* permissions before denying. Override to tune per app.
|
|
483
|
+
*/
|
|
484
|
+
declare const PERMISSION_GUARD_TIMEOUT_MS: InjectionToken<number>;
|
|
485
|
+
/**
|
|
486
|
+
* Permission Guard
|
|
487
|
+
*
|
|
488
|
+
* Protects routes that require specific permissions.
|
|
489
|
+
* Configure the required permission in route data.
|
|
490
|
+
*
|
|
491
|
+
* Usage:
|
|
492
|
+
* ```typescript
|
|
493
|
+
* {
|
|
494
|
+
* path: 'users',
|
|
495
|
+
* component: UsersPageComponent,
|
|
496
|
+
* canActivate: [permissionGuard],
|
|
497
|
+
* data: { permission: 'users.read' }
|
|
498
|
+
* }
|
|
499
|
+
* ```
|
|
500
|
+
*/
|
|
501
|
+
declare const permissionGuard: CanActivateFn;
|
|
502
|
+
/**
|
|
503
|
+
* Any Permission Guard
|
|
504
|
+
*
|
|
505
|
+
* Allows access if user has any of the specified permissions.
|
|
506
|
+
*
|
|
507
|
+
* Usage:
|
|
508
|
+
* ```typescript
|
|
509
|
+
* {
|
|
510
|
+
* path: 'resources',
|
|
511
|
+
* component: ResourcesPageComponent,
|
|
512
|
+
* canActivate: [anyPermissionGuard],
|
|
513
|
+
* data: { permissions: ['resources.read', 'resources.write'] }
|
|
514
|
+
* }
|
|
515
|
+
* ```
|
|
516
|
+
*/
|
|
517
|
+
declare const anyPermissionGuard: CanActivateFn;
|
|
518
|
+
/**
|
|
519
|
+
* All Permissions Guard
|
|
520
|
+
*
|
|
521
|
+
* Allows access only if user has all of the specified permissions.
|
|
522
|
+
*
|
|
523
|
+
* Usage:
|
|
524
|
+
* ```typescript
|
|
525
|
+
* {
|
|
526
|
+
* path: 'admin',
|
|
527
|
+
* component: AdminPageComponent,
|
|
528
|
+
* canActivate: [allPermissionsGuard],
|
|
529
|
+
* data: { permissions: ['admin.read', 'admin.write'] }
|
|
530
|
+
* }
|
|
531
|
+
* ```
|
|
532
|
+
*/
|
|
533
|
+
declare const allPermissionsGuard: CanActivateFn;
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Helpers for the tenant-aware SSO landing flow shared across host portals.
|
|
537
|
+
*
|
|
538
|
+
* The "Login with SSO" affordance on the landing page resolves a tenant by
|
|
539
|
+
* identifier and persists its issuer URI to sessionStorage. The host's OIDC
|
|
540
|
+
* config picks that up at module load (next page navigation) so the OIDC
|
|
541
|
+
* authority points at the tenant's identity-server subdomain instead of the
|
|
542
|
+
* platform default. After resolve + persist, the landing reloads with
|
|
543
|
+
* `pendingAuthorize` set so the post-load handler can call authorize() with
|
|
544
|
+
* the right authority and the tenant's domain_hint / policy / ui_mode.
|
|
545
|
+
*/
|
|
546
|
+
interface TenantSso {
|
|
547
|
+
identifier: string;
|
|
548
|
+
issuerUri: string;
|
|
549
|
+
defaultIdpScheme?: string;
|
|
550
|
+
defaultSignInPolicyId?: string;
|
|
551
|
+
defaultSsoUiMode?: 'Journey' | 'Standalone';
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Extract tenant identifier from hostname subdomain.
|
|
555
|
+
* e.g., "acme.auth.example.com" → "acme"
|
|
556
|
+
* Returns undefined for localhost, IP addresses, or hostnames with fewer than 3 parts.
|
|
557
|
+
*/
|
|
558
|
+
declare function resolveTenantFromHostname(): string | undefined;
|
|
559
|
+
/**
|
|
560
|
+
* Returns the tenant SSO authority persisted in sessionStorage, or the given
|
|
561
|
+
* default. Safe in SSR / non-browser contexts.
|
|
562
|
+
*/
|
|
563
|
+
declare function resolveTenantSsoAuthority(defaultAuthority: string): string;
|
|
564
|
+
/**
|
|
565
|
+
* Returns the tenant identifier, issuer URI, and any persisted SSO defaults,
|
|
566
|
+
* or null if none is set.
|
|
567
|
+
*/
|
|
568
|
+
declare function readTenantSso(): TenantSso | null;
|
|
569
|
+
/**
|
|
570
|
+
* Persists the tenant SSO context resolved by the landing page. Set
|
|
571
|
+
* `markPendingAuthorize` so the post-reload landing handler kicks off
|
|
572
|
+
* authorize() with the tenant's authority + the stored hints.
|
|
573
|
+
*
|
|
574
|
+
* Wipes angular-auth-oidc-client's cached well-known + state on the way
|
|
575
|
+
* out — the cache is keyed by config id, not authority, so switching
|
|
576
|
+
* tenants would otherwise reuse the previous authority's discovery doc
|
|
577
|
+
* and throw issuer-mismatch on the next authorize call.
|
|
578
|
+
*/
|
|
579
|
+
declare function writeTenantSso(sso: TenantSso, markPendingAuthorize?: boolean): void;
|
|
580
|
+
/**
|
|
581
|
+
* Build the customParams object to pass to oidcSecurityService.authorize()
|
|
582
|
+
* from a remembered tenant SSO. Returns undefined if there's nothing worth
|
|
583
|
+
* passing. Callers feed this into the `customParams` field of the authorize
|
|
584
|
+
* call so domain_hint / policy / ui_mode reach /connect/authorize.
|
|
585
|
+
*/
|
|
586
|
+
declare function tenantSsoAuthorizeParams(sso: TenantSso): Record<string, string> | undefined;
|
|
587
|
+
/**
|
|
588
|
+
* Returns true (and clears the flag) when a pending authorize call was queued
|
|
589
|
+
* by the landing's SSO submit. Use on landing init to auto-trigger
|
|
590
|
+
* oidcSecurityService.authorize() after a reload picked up the new authority.
|
|
591
|
+
*/
|
|
592
|
+
declare function consumePendingAuthorize(): boolean;
|
|
593
|
+
/**
|
|
594
|
+
* Removes all persisted tenant SSO state. Call from logout flows and the
|
|
595
|
+
* "Use a different organisation" affordance on the landing.
|
|
596
|
+
*
|
|
597
|
+
* Also clears angular-auth-oidc-client's sessionStorage cache (well-known
|
|
598
|
+
* discovery doc, in-flight state) — those entries are keyed by config id,
|
|
599
|
+
* not by authority URL, so without wiping them a previously-cached
|
|
600
|
+
* discovery for a different tenant authority survives the switch and
|
|
601
|
+
* throws an "issuer mismatch" on the next authorize call.
|
|
602
|
+
*/
|
|
603
|
+
declare function clearTenantSso(): void;
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Auth Interceptor
|
|
607
|
+
*
|
|
608
|
+
* Adds the Authorization header with the access token to outgoing requests.
|
|
609
|
+
* Handles 401 errors by attempting to refresh the token.
|
|
610
|
+
*/
|
|
611
|
+
declare const authInterceptor: HttpInterceptorFn;
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Token attachment fallback + 401 retry with refresh deduplication. On refresh
|
|
615
|
+
* failure, stashes the current route in sessionStorage under an opaque key so
|
|
616
|
+
* the post-login callback can restore navigation without leaking the route to
|
|
617
|
+
* URL/history/server logs.
|
|
618
|
+
*
|
|
619
|
+
* Skips the OIDC token endpoint to avoid infinite loops if /connect/token 401s.
|
|
620
|
+
*/
|
|
621
|
+
declare const authRefreshInterceptor: HttpInterceptorFn;
|
|
622
|
+
/**
|
|
623
|
+
* Remembers the page the user is on before a sign-in redirect ends their session, so the callback page can
|
|
624
|
+
* bring them back to it ({@link readAndClearReturnUrl}). The callback, renew and error pages, and the root,
|
|
625
|
+
* are not remembered.
|
|
626
|
+
*/
|
|
627
|
+
declare function rememberReturnUrl(): void;
|
|
628
|
+
/**
|
|
629
|
+
* Helper for callback pages to restore the pre-login route.
|
|
630
|
+
*/
|
|
631
|
+
declare function readAndClearReturnUrl(): string | null;
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Makes each refresh token redeem exactly once across all of the app's tabs, and keeps that redemption alive
|
|
635
|
+
* through a brief server outage.
|
|
636
|
+
*
|
|
637
|
+
* Refresh tokens are single use. Three things would otherwise present one twice:
|
|
638
|
+
* - the OIDC client abandons a renew it considers slow (its periodic check cancels the request every few
|
|
639
|
+
* seconds) and later starts another with the same token;
|
|
640
|
+
* - in one tab, a 401 and the background renew can both ask for a refresh;
|
|
641
|
+
* - tabs can hold the same token (a duplicated tab copies its opener's session; remembered sessions share one
|
|
642
|
+
* store), and each renews on its own timer.
|
|
643
|
+
* So one exchange runs per refresh token whether or not its caller still listens, refreshes are taken in turn
|
|
644
|
+
* across tabs under a Web Lock, and a request for a token another exchange already redeemed — here or in
|
|
645
|
+
* another tab ({@link tokenRotationChannel}) — is answered with what it was exchanged for, its lifetime
|
|
646
|
+
* counted from when the server issued it.
|
|
647
|
+
*
|
|
648
|
+
* The client also deletes the session on any HTTP error, so a 502 from the proxy while the API restarts would
|
|
649
|
+
* sign the user out although the refresh token is still good. Transient statuses are retried after the
|
|
650
|
+
* server's Retry-After (up to 30 s), else 1 s doubling to 10 s; a refusal such as invalid_grant still reaches
|
|
651
|
+
* the client.
|
|
652
|
+
*/
|
|
653
|
+
declare const tokenRefreshRetryInterceptor: HttpInterceptorFn;
|
|
654
|
+
/** The refresh token a token request redeems, or null for any other request. */
|
|
655
|
+
declare function refreshTokenOf(req: HttpRequest<unknown>): string | null;
|
|
656
|
+
/**
|
|
657
|
+
* Makes the next refresh a switch to `tenantId` (the token endpoint's `tenant` parameter), or cancels a switch
|
|
658
|
+
* not yet sent. The parameter rides that one request only: the auth library never holds it, so later refreshes
|
|
659
|
+
* carry none (the refresh token remembers the tenant), and a refused switch never reaches the library as a
|
|
660
|
+
* failure — it is answered by a plain refresh instead.
|
|
661
|
+
*/
|
|
662
|
+
declare function requestTenantSwitch(tenantId: string | null): void;
|
|
663
|
+
/** Wait before retry number `attempt` (1-based): the server's Retry-After seconds when it sends them (at most
|
|
664
|
+
* 30 s), else 1 s doubling to 10 s. */
|
|
665
|
+
declare function refreshRetryDelayMs(error: HttpErrorResponse, attempt: number): number;
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Tenant Interceptor
|
|
669
|
+
*
|
|
670
|
+
* Adds the X-Tenant-ID header to outgoing API requests when the user
|
|
671
|
+
* has selected a tenant context.
|
|
672
|
+
*
|
|
673
|
+
* This is used in the Admin UI to allow super admins to manage
|
|
674
|
+
* resources across different tenants. The backend validates
|
|
675
|
+
* whether the user is authorized to access the specified tenant.
|
|
676
|
+
*
|
|
677
|
+
* OIDC protocol endpoints are excluded — they must always use the
|
|
678
|
+
* global (non-tenant) context so the issuer stays consistent.
|
|
679
|
+
*/
|
|
680
|
+
declare const tenantInterceptor: HttpInterceptorFn;
|
|
681
|
+
/**
|
|
682
|
+
* Organization Interceptor
|
|
683
|
+
*
|
|
684
|
+
* Adds the X-Organization-ID header to outgoing requests when the user
|
|
685
|
+
* has selected an organization context.
|
|
686
|
+
*
|
|
687
|
+
* This is used for multi-tenant applications where users can belong
|
|
688
|
+
* to multiple organizations.
|
|
689
|
+
*/
|
|
690
|
+
declare const organizationInterceptor: HttpInterceptorFn;
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Ngrok Interceptor
|
|
694
|
+
*
|
|
695
|
+
* Adds the ngrok-skip-browser-warning header to skip the ngrok interstitial page.
|
|
696
|
+
* This header is required when making API calls through ngrok tunnels.
|
|
697
|
+
*/
|
|
698
|
+
declare const ngrokInterceptor: HttpInterceptorFn;
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Error Interceptor
|
|
702
|
+
*
|
|
703
|
+
* Catches HTTP error responses and displays user-friendly toast notifications.
|
|
704
|
+
* Extracts error messages from backend responses when available.
|
|
705
|
+
* Skips 401 errors (handled by the auth interceptor).
|
|
706
|
+
*/
|
|
707
|
+
declare const errorInterceptor: HttpInterceptorFn;
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* The browser's DPoP key (RFC 9449): an ECDSA P-256 key pair whose private half the page can sign with but
|
|
711
|
+
* never read, so a token bound to it cannot be used anywhere else.
|
|
712
|
+
*
|
|
713
|
+
* The pair is kept in IndexedDB, shared by every tab of the app and surviving reloads, because refresh tokens
|
|
714
|
+
* are bound to it: a remembered session's next refresh needs the same key. Where IndexedDB is unavailable the
|
|
715
|
+
* key lives for the page only, and the session ends with it.
|
|
716
|
+
*/
|
|
717
|
+
/** The public half, as it goes in a proof's <c>jwk</c> header. */
|
|
718
|
+
interface DPoPPublicJwk {
|
|
719
|
+
kty: 'EC';
|
|
720
|
+
crv: 'P-256';
|
|
721
|
+
x: string;
|
|
722
|
+
y: string;
|
|
723
|
+
}
|
|
724
|
+
interface DPoPKey {
|
|
725
|
+
readonly privateKey: CryptoKey;
|
|
726
|
+
readonly publicJwk: DPoPPublicJwk;
|
|
727
|
+
}
|
|
728
|
+
/** Whether this page can hold a DPoP key: Web Crypto, which browsers offer only in secure contexts. */
|
|
729
|
+
declare function dpopSupported(): boolean;
|
|
730
|
+
/** The app's DPoP key, loaded or created once per page. */
|
|
731
|
+
declare function dpopKey(): Promise<DPoPKey>;
|
|
732
|
+
/** Forgets the key held by this page, so the next {@link dpopKey} reads storage again (tests). */
|
|
733
|
+
declare function resetDPoPKeyForTests(): void;
|
|
734
|
+
|
|
735
|
+
/** What a DPoP proof (RFC 9449 §4.2) is for: one HTTP request, and the access token it presents if any. */
|
|
736
|
+
interface DPoPProofTarget {
|
|
737
|
+
method: string;
|
|
738
|
+
url: string;
|
|
739
|
+
accessToken?: string | null;
|
|
740
|
+
nonce?: string | null;
|
|
741
|
+
}
|
|
742
|
+
/** A DPoP proof JWT signed by `key` for one request. */
|
|
743
|
+
declare function createDPoPProof(key: DPoPKey, target: DPoPProofTarget): Promise<string>;
|
|
744
|
+
/** The request URI a proof names: absolute, without query or fragment. */
|
|
745
|
+
declare function proofUri(url: string): string;
|
|
746
|
+
/** The <c>ath</c> claim: the base64url SHA-256 of the access token. */
|
|
747
|
+
declare function accessTokenHash(accessToken: string): Promise<string>;
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Sender-constrains the app's tokens with DPoP (RFC 9449).
|
|
751
|
+
*
|
|
752
|
+
* - Token requests (any grant) carry a proof, so the server binds the tokens it issues — access and refresh —
|
|
753
|
+
* to this browser's key ({@link dpopKey}); a copied token is useless without it.
|
|
754
|
+
* - A request presenting a DPoP-bound access token (one whose <c>cnf.jkt</c> names a key) is sent under the
|
|
755
|
+
* DPoP scheme with a proof for that request and token instead of as a bearer token, which the server refuses.
|
|
756
|
+
* - When a server asks for a nonce (<c>use_dpop_nonce</c>), the request is retried once with it.
|
|
757
|
+
*
|
|
758
|
+
* Register it after the interceptors that attach the access token, so it sees the final request.
|
|
759
|
+
* Pages without Web Crypto (non-secure contexts) send requests unchanged; their tokens are not bound.
|
|
760
|
+
*/
|
|
761
|
+
declare const dpopInterceptor: HttpInterceptorFn;
|
|
762
|
+
/** The access token a request presents, when it is bound to a DPoP key; else null. */
|
|
763
|
+
declare function boundAccessTokenOf(req: HttpRequest<unknown>): string | null;
|
|
764
|
+
/** Whether a JWT access token is bound to a DPoP key (has <c>cnf.jkt</c>). */
|
|
765
|
+
declare function isDPoPBound(token: string): boolean;
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* A `fetch` that sends through the app's HttpClient, so its interceptors apply: the signed-in user's access token,
|
|
769
|
+
* the tenant, DPoP proofs and the refresh-and-retry on an expired token. Give it to the Oluso SDK
|
|
770
|
+
* (`new OlusoClient({ ..., fetch: httpClientFetch(http) })`) to run journeys the app's backend started for the user.
|
|
771
|
+
* Answers with an error status resolve like fetch's do, so the SDK reads the server's message.
|
|
772
|
+
*/
|
|
773
|
+
declare function httpClientFetch(http: HttpClient): typeof fetch;
|
|
774
|
+
|
|
775
|
+
export { AuthProvider, AuthService, AuthStore, NoOpAuthProvider, PERMISSION_GUARD_TIMEOUT_MS, RememberMeSecurityStorage, RenewAwareAuthProvider, accessTokenHash, allPermissionsGuard, anyPermissionGuard, authGuard, authInterceptor, authRefreshInterceptor, boundAccessTokenOf, checkAuthRenewingSession, clearTenantSso, consumePendingAuthorize, createDPoPProof, dpopInterceptor, dpopKey, dpopSupported, errorInterceptor, guestGuard, httpClientFetch, isDPoPBound, ngrokInterceptor, orgAdminGuard, organizationInterceptor, permissionGuard, proofUri, readAndClearReturnUrl, readTenantSso, refreshRetryDelayMs, refreshTokenOf, rememberReturnUrl, requestTenantSwitch, resetDPoPKeyForTests, resolveTenantFromHostname, resolveTenantSsoAuthority, superAdminGuard, tenantInterceptor, tenantOfToken, tenantSsoAuthorizeParams, tokenRefreshRetryInterceptor, writeTenantSso };
|
|
776
|
+
export type { AuthRefreshResult, AuthState, AuthUser, DPoPKey, DPoPProofTarget, DPoPPublicJwk, LoginRequest, LoginResponse, OidcConfig, OrganizationContext, StartupAuthClient, TenantSso, TokenPayload };
|