oidc-spa 8.0.5 → 8.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/angular.d.ts +191 -0
- package/angular.js +351 -0
- package/angular.js.map +1 -0
- package/core/createOidc.d.ts +7 -7
- package/core/createOidc.js +2 -2
- package/core/createOidc.js.map +1 -1
- package/esm/angular.d.ts +191 -0
- package/esm/angular.js +314 -0
- package/esm/angular.js.map +1 -0
- package/esm/core/createOidc.d.ts +7 -7
- package/esm/core/createOidc.js +2 -2
- package/esm/core/createOidc.js.map +1 -1
- package/esm/keycloak/keycloakUtils.js.map +1 -1
- package/esm/tools/ConcreteClass.d.ts +3 -0
- package/esm/tools/ConcreteClass.js +2 -0
- package/esm/tools/ConcreteClass.js.map +1 -0
- package/esm/tools/Deferred.d.ts +7 -0
- package/esm/tools/Deferred.js +5 -0
- package/esm/tools/Deferred.js.map +1 -1
- package/esm/tools/ReadonlyBehaviorSubject.d.ts +8 -0
- package/esm/tools/ReadonlyBehaviorSubject.js +2 -0
- package/esm/tools/ReadonlyBehaviorSubject.js.map +1 -0
- package/esm/tools/getBaseHref.d.ts +1 -0
- package/esm/tools/getBaseHref.js +8 -0
- package/esm/tools/getBaseHref.js.map +1 -0
- package/esm/vendor/frontend/tsafe.d.ts +1 -0
- package/esm/vendor/frontend/tsafe.js +1 -1
- package/keycloak/keycloakUtils.js.map +1 -1
- package/package.json +3 -3
- package/src/angular.ts +583 -0
- package/src/core/createOidc.ts +10 -10
- package/src/keycloak/keycloakUtils.ts +0 -1
- package/src/tools/ConcreteClass.ts +3 -0
- package/src/tools/Deferred.ts +10 -0
- package/src/tools/ReadonlyBehaviorSubject.ts +9 -0
- package/src/tools/getBaseHref.ts +7 -0
- package/src/vendor/frontend/tsafe.ts +1 -0
- package/tools/ConcreteClass.d.ts +3 -0
- package/tools/ConcreteClass.js +3 -0
- package/tools/ConcreteClass.js.map +1 -0
- package/tools/Deferred.d.ts +7 -0
- package/tools/Deferred.js +5 -0
- package/tools/Deferred.js.map +1 -1
- package/tools/ReadonlyBehaviorSubject.d.ts +8 -0
- package/tools/ReadonlyBehaviorSubject.js +3 -0
- package/tools/ReadonlyBehaviorSubject.js.map +1 -0
- package/tools/getBaseHref.d.ts +1 -0
- package/tools/getBaseHref.js +11 -0
- package/tools/getBaseHref.js.map +1 -0
- package/vendor/frontend/tsafe.d.ts +1 -0
- package/vendor/frontend/tsafe.js +1 -1
- package/angular/angular.d.ts +0 -72
- package/angular/angular.js +0 -254
- package/angular/angular.js.map +0 -1
- package/angular/index.d.ts +0 -1
- package/angular/index.js +0 -6
- package/angular/index.js.map +0 -1
- package/esm/angular/angular.d.ts +0 -72
- package/esm/angular/angular.js +0 -250
- package/esm/angular/angular.js.map +0 -1
- package/esm/angular/index.d.ts +0 -1
- package/esm/angular/index.js +0 -2
- package/esm/angular/index.js.map +0 -1
- package/esm/mock/angular.d.ts +0 -41
- package/esm/mock/angular.js +0 -7
- package/esm/mock/angular.js.map +0 -1
- package/mock/angular.d.ts +0 -41
- package/mock/angular.js +0 -10
- package/mock/angular.js.map +0 -1
- package/src/angular/angular.ts +0 -429
- package/src/angular/index.ts +0 -1
- package/src/mock/angular.ts +0 -11
package/src/angular.ts
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import { BehaviorSubject } from "rxjs";
|
|
2
|
+
import type { Oidc, OidcInitializationError, ParamsOfCreateOidc } from "./core";
|
|
3
|
+
import type { OidcMetadata } from "./core/OidcMetadata";
|
|
4
|
+
import { Deferred } from "./tools/Deferred";
|
|
5
|
+
import { assert, type Equals, is } from "./vendor/frontend/tsafe";
|
|
6
|
+
import { createObjectThatThrowsIfAccessed } from "./tools/createObjectThatThrowsIfAccessed";
|
|
7
|
+
import {
|
|
8
|
+
type Signal,
|
|
9
|
+
inject,
|
|
10
|
+
type EnvironmentProviders,
|
|
11
|
+
makeEnvironmentProviders,
|
|
12
|
+
provideAppInitializer
|
|
13
|
+
} from "@angular/core";
|
|
14
|
+
import { toSignal } from "@angular/core/rxjs-interop";
|
|
15
|
+
import type { ReadonlyBehaviorSubject } from "./tools/ReadonlyBehaviorSubject";
|
|
16
|
+
import { Router, type CanActivateFn } from "@angular/router";
|
|
17
|
+
import type { ValueOrAsyncGetter } from "./tools/ValueOrAsyncGetter";
|
|
18
|
+
import { getBaseHref } from "./tools/getBaseHref";
|
|
19
|
+
import type { ConcreteClass } from "./tools/ConcreteClass";
|
|
20
|
+
|
|
21
|
+
export type ParamsOfProvide = {
|
|
22
|
+
issuerUri: string;
|
|
23
|
+
clientId: string;
|
|
24
|
+
/**
|
|
25
|
+
* The scopes being requested from the OIDC/OAuth2 provider (default: `["profile"]`
|
|
26
|
+
* (the scope "openid" is added automatically as it's mandatory)
|
|
27
|
+
**/
|
|
28
|
+
scopes?: string[];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Transform the url (authorization endpoint) before redirecting to the login pages.
|
|
32
|
+
*
|
|
33
|
+
* The isSilent parameter is true when the redirect is initiated in the background iframe for silent signin.
|
|
34
|
+
* This can be used to omit ui related query parameters (like `ui_locales`).
|
|
35
|
+
*/
|
|
36
|
+
transformUrlBeforeRedirect?: (params: { authorizationUrl: string; isSilent: boolean }) => string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extra query params to be added to the authorization endpoint url before redirecting or silent signing in.
|
|
40
|
+
* You can provide a function that returns those extra query params, it will be called
|
|
41
|
+
* when login() is called.
|
|
42
|
+
*
|
|
43
|
+
* Example: extraQueryParams: ()=> ({ ui_locales: "fr" })
|
|
44
|
+
*
|
|
45
|
+
* This parameter can also be passed to login() directly.
|
|
46
|
+
*/
|
|
47
|
+
extraQueryParams?:
|
|
48
|
+
| Record<string, string | undefined>
|
|
49
|
+
| ((params: { isSilent: boolean; url: string }) => Record<string, string | undefined>);
|
|
50
|
+
/**
|
|
51
|
+
* Extra body params to be added to the /token POST request.
|
|
52
|
+
*
|
|
53
|
+
* It will be used when for the initial request, whenever the token is getting refreshed and if you call `renewTokens()`.
|
|
54
|
+
* You can also provide this parameter directly to the `renewTokens()` method.
|
|
55
|
+
*
|
|
56
|
+
* It can be either a string to string record or a function that returns a string to string record.
|
|
57
|
+
*
|
|
58
|
+
* Example: extraTokenParams: ()=> ({ selectedCustomer: "xxx" })
|
|
59
|
+
* extraTokenParams: { selectedCustomer: "xxx" }
|
|
60
|
+
*/
|
|
61
|
+
extraTokenParams?: Record<string, string | undefined> | (() => Record<string, string | undefined>);
|
|
62
|
+
/**
|
|
63
|
+
* Usage discouraged, it's here because we don't want to assume too much on your
|
|
64
|
+
* usecase but I can't think of a scenario where you would want anything
|
|
65
|
+
* other than the current page.
|
|
66
|
+
*
|
|
67
|
+
* Where to redirect after successful login.
|
|
68
|
+
* Default: window.location.href (here)
|
|
69
|
+
*
|
|
70
|
+
* It does not need to include the origin, eg: "/dashboard"
|
|
71
|
+
*
|
|
72
|
+
* This parameter can also be passed to login() directly as `redirectUrl`.
|
|
73
|
+
*/
|
|
74
|
+
postLoginRedirectUrl?: string;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* This parameter defines after how many seconds of inactivity the user should be
|
|
78
|
+
* logged out automatically.
|
|
79
|
+
*
|
|
80
|
+
* WARNING: It should be configured on the identity server side
|
|
81
|
+
* as it's the authoritative source for security policies and not the client.
|
|
82
|
+
* If you don't provide this parameter it will be inferred from the refresh token expiration time.
|
|
83
|
+
* */
|
|
84
|
+
idleSessionLifetimeInSeconds?: number;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Usage discouraged, this parameter exists because we don't want to assume
|
|
88
|
+
* too much about your usecase but I can't think of a scenario where you would
|
|
89
|
+
* want anything other than the current page.
|
|
90
|
+
*
|
|
91
|
+
* Default: { redirectTo: "current page" }
|
|
92
|
+
*/
|
|
93
|
+
autoLogoutParams?: Parameters<Oidc.LoggedIn<any>["logout"]>[0];
|
|
94
|
+
autoLogin?: boolean;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Default: false
|
|
98
|
+
*
|
|
99
|
+
* See: https://docs.oidc-spa.dev/v/v8/resources/iframe-related-issues
|
|
100
|
+
*/
|
|
101
|
+
noIframe?: boolean;
|
|
102
|
+
|
|
103
|
+
debugLogs?: boolean;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* WARNING: This option exists solely as a workaround
|
|
107
|
+
* for limitations in the Google OAuth API.
|
|
108
|
+
* See: https://docs.oidc-spa.dev/providers-configuration/google-oauth
|
|
109
|
+
*
|
|
110
|
+
* Do not use this for other providers.
|
|
111
|
+
* If you think you need a client secret in a SPA, you are likely
|
|
112
|
+
* trying to use a confidential (private) client in the browser,
|
|
113
|
+
* which is insecure and not supported.
|
|
114
|
+
*/
|
|
115
|
+
__unsafe_clientSecret?: string;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* WARNING: Setting this to true is a workaround for provider
|
|
119
|
+
* like Google OAuth that don't support JWT access token.
|
|
120
|
+
* Use at your own risk, this is a hack.
|
|
121
|
+
*/
|
|
122
|
+
__unsafe_useIdTokenAsAccessToken?: boolean;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* This option should only be used as a last resort.
|
|
126
|
+
*
|
|
127
|
+
* If your OIDC provider is correctly configured, this should not be necessary.
|
|
128
|
+
*
|
|
129
|
+
* The metadata is normally retrieved automatically from:
|
|
130
|
+
* `${issuerUri}/.well-known/openid-configuration`
|
|
131
|
+
*
|
|
132
|
+
* Use this only if that endpoint is not accessible (e.g. due to missing CORS headers
|
|
133
|
+
* or non-standard deployments), and you cannot fix the server-side configuration.
|
|
134
|
+
*/
|
|
135
|
+
__metadata?: Partial<OidcMetadata>;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* You can use oidc.$secondsLeftBeforeAutoLogout to display an overlay/update the tab title
|
|
139
|
+
* to indicate to your user that they are going to be logged out if they don't interact
|
|
140
|
+
* with the app.
|
|
141
|
+
* This value let you define how long before how long before auto logout this warning should
|
|
142
|
+
* start showing.
|
|
143
|
+
* Default is 45 seconds.
|
|
144
|
+
*/
|
|
145
|
+
autoLogoutWarningDurationSeconds?: number;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
assert<
|
|
149
|
+
Equals<
|
|
150
|
+
Omit<ParamsOfProvide, "autoLogoutWarningDurationSeconds">,
|
|
151
|
+
Omit<ParamsOfCreateOidc<any, boolean>, "homeUrl" | "decodedIdTokenSchema">
|
|
152
|
+
>
|
|
153
|
+
>;
|
|
154
|
+
|
|
155
|
+
export type ParamsOfProvideMock = {
|
|
156
|
+
mockIssuerUri?: string;
|
|
157
|
+
mockClientId?: string;
|
|
158
|
+
mockAccessToken?: string;
|
|
159
|
+
isUserInitiallyLoggedIn?: boolean;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export abstract class AbstractOidcService<
|
|
163
|
+
T_DecodedIdToken extends Record<string, unknown> = Oidc.Tokens.DecodedIdToken_base
|
|
164
|
+
> {
|
|
165
|
+
protected autoLogin: boolean = false;
|
|
166
|
+
protected providerAwaitsInitialization: boolean = true;
|
|
167
|
+
protected decodedIdTokenSchema:
|
|
168
|
+
| {
|
|
169
|
+
parse: (decodedIdToken_original: Oidc.Tokens.DecodedIdToken_base) => T_DecodedIdToken;
|
|
170
|
+
}
|
|
171
|
+
| undefined = undefined;
|
|
172
|
+
|
|
173
|
+
protected mockDecodedIdToken: (() => Promise<T_DecodedIdToken>) | T_DecodedIdToken | undefined =
|
|
174
|
+
undefined;
|
|
175
|
+
|
|
176
|
+
#autoLogoutWarningDurationSeconds = 45;
|
|
177
|
+
|
|
178
|
+
static provide(params: ValueOrAsyncGetter<ParamsOfProvide>): EnvironmentProviders {
|
|
179
|
+
const paramsOrGetParams = params;
|
|
180
|
+
|
|
181
|
+
assert(is<ConcreteClass<typeof AbstractOidcService>>(this));
|
|
182
|
+
|
|
183
|
+
return makeEnvironmentProviders([
|
|
184
|
+
this,
|
|
185
|
+
provideAppInitializer(async () => {
|
|
186
|
+
const instance = inject(this);
|
|
187
|
+
|
|
188
|
+
instance.#initialize({
|
|
189
|
+
prOidcOrInitializationError: (async () => {
|
|
190
|
+
const [{ createOidc }, { autoLogoutWarningDurationSeconds, ...params }] =
|
|
191
|
+
await Promise.all([
|
|
192
|
+
import("./core"),
|
|
193
|
+
typeof paramsOrGetParams === "function"
|
|
194
|
+
? paramsOrGetParams()
|
|
195
|
+
: paramsOrGetParams
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
if (autoLogoutWarningDurationSeconds !== undefined) {
|
|
199
|
+
instance.#autoLogoutWarningDurationSeconds =
|
|
200
|
+
autoLogoutWarningDurationSeconds;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
return createOidc({
|
|
205
|
+
homeUrl: getBaseHref(),
|
|
206
|
+
autoLogin: instance.autoLogin,
|
|
207
|
+
decodedIdTokenSchema: instance.decodedIdTokenSchema,
|
|
208
|
+
...params
|
|
209
|
+
});
|
|
210
|
+
} catch (initializationError) {
|
|
211
|
+
assert(initializationError instanceof Error);
|
|
212
|
+
assert(is<OidcInitializationError>(initializationError));
|
|
213
|
+
return initializationError;
|
|
214
|
+
}
|
|
215
|
+
})()
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
if (instance.providerAwaitsInitialization) {
|
|
219
|
+
await instance.prInitialized;
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
]);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
static provideMock(params: ParamsOfProvideMock = {}): EnvironmentProviders {
|
|
226
|
+
assert(is<ConcreteClass<typeof AbstractOidcService>>(this));
|
|
227
|
+
|
|
228
|
+
return makeEnvironmentProviders([
|
|
229
|
+
this,
|
|
230
|
+
provideAppInitializer(async () => {
|
|
231
|
+
const instance = inject(this);
|
|
232
|
+
|
|
233
|
+
instance.#initialize({
|
|
234
|
+
prOidcOrInitializationError: (async () => {
|
|
235
|
+
const { createMockOidc } = await import("./mock");
|
|
236
|
+
|
|
237
|
+
return createMockOidc<Record<string, unknown>, boolean>({
|
|
238
|
+
homeUrl: getBaseHref(),
|
|
239
|
+
autoLogin: instance.autoLogin,
|
|
240
|
+
isUserInitiallyLoggedIn: instance.autoLogin
|
|
241
|
+
? true
|
|
242
|
+
: params.isUserInitiallyLoggedIn,
|
|
243
|
+
mockedParams: {
|
|
244
|
+
issuerUri: params.mockIssuerUri,
|
|
245
|
+
clientId: params.mockClientId
|
|
246
|
+
},
|
|
247
|
+
mockedTokens: {
|
|
248
|
+
accessToken: params.mockAccessToken,
|
|
249
|
+
decodedIdToken: await (() => {
|
|
250
|
+
if (instance.mockDecodedIdToken === undefined) {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
if (typeof instance.mockDecodedIdToken === "function") {
|
|
254
|
+
return instance.mockDecodedIdToken();
|
|
255
|
+
}
|
|
256
|
+
})()
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
})()
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await instance.prInitialized;
|
|
263
|
+
})
|
|
264
|
+
]);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
static enforceLoginGuard() {
|
|
268
|
+
const canActivateFn = (async route => {
|
|
269
|
+
const instance = inject(this);
|
|
270
|
+
const router = inject(Router);
|
|
271
|
+
|
|
272
|
+
await instance.prInitialized;
|
|
273
|
+
|
|
274
|
+
const oidc = instance.#getOidc({ callerName: "enforceLoginGuard" });
|
|
275
|
+
|
|
276
|
+
if (!oidc.isUserLoggedIn) {
|
|
277
|
+
const redirectUrl = router.serializeUrl(
|
|
278
|
+
router.createUrlTree(
|
|
279
|
+
route.url.map(u => u.path),
|
|
280
|
+
{
|
|
281
|
+
queryParams: route.queryParams,
|
|
282
|
+
fragment: route.fragment ?? undefined
|
|
283
|
+
}
|
|
284
|
+
)
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
const doesCurrentHrefRequiresAuth =
|
|
288
|
+
location.href.replace(/\/$/, "") === redirectUrl.replace(/\/$/, "");
|
|
289
|
+
|
|
290
|
+
await oidc.login({
|
|
291
|
+
doesCurrentHrefRequiresAuth,
|
|
292
|
+
redirectUrl
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return true;
|
|
297
|
+
}) satisfies CanActivateFn;
|
|
298
|
+
return canActivateFn;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
#dState = new Deferred<{
|
|
302
|
+
oidc: Oidc<T_DecodedIdToken> | undefined;
|
|
303
|
+
initializationError: OidcInitializationError | undefined;
|
|
304
|
+
}>();
|
|
305
|
+
|
|
306
|
+
readonly prInitialized: Promise<true> = this.#dState.pr.then(() => true);
|
|
307
|
+
|
|
308
|
+
#initialize(params: {
|
|
309
|
+
prOidcOrInitializationError: Promise<Oidc<T_DecodedIdToken> | OidcInitializationError>;
|
|
310
|
+
}): void {
|
|
311
|
+
const { prOidcOrInitializationError } = params;
|
|
312
|
+
|
|
313
|
+
prOidcOrInitializationError.then(oidcOrInitializationError => {
|
|
314
|
+
let initializationError: OidcInitializationError | undefined = undefined;
|
|
315
|
+
let oidc: Oidc<T_DecodedIdToken> | undefined = undefined;
|
|
316
|
+
|
|
317
|
+
if (oidcOrInitializationError instanceof Error) {
|
|
318
|
+
initializationError = oidcOrInitializationError;
|
|
319
|
+
} else {
|
|
320
|
+
oidc = oidcOrInitializationError;
|
|
321
|
+
initializationError = oidc.isUserLoggedIn ? undefined : oidc.initializationError;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
this.#dState.resolve({
|
|
325
|
+
oidc,
|
|
326
|
+
initializationError
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
#getPrInitializedNotResolvedErrorMessage(params: { callerName: string }) {
|
|
332
|
+
const { callerName } = params;
|
|
333
|
+
return [
|
|
334
|
+
`oidc-spa: ${callerName} called/accessed before`,
|
|
335
|
+
"`oidc.prInitialized` resolved.",
|
|
336
|
+
"You are using `awaitInitialization: false`.",
|
|
337
|
+
"In your template you should wrap your usage of",
|
|
338
|
+
"oidc.isUserLoggedIn, oidc.$decodedIdToken() ect. into",
|
|
339
|
+
"@defer (when oidc.prInitialized | async) { } @placeholder { Loading... }"
|
|
340
|
+
].join(" ");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
#getState(params: { callerName: string }) {
|
|
344
|
+
const { callerName } = params;
|
|
345
|
+
const { hasResolved, value } = this.#dState.getState();
|
|
346
|
+
if (!hasResolved) {
|
|
347
|
+
throw new Error(this.#getPrInitializedNotResolvedErrorMessage({ callerName }));
|
|
348
|
+
}
|
|
349
|
+
return value;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
get initializationError(): OidcInitializationError | undefined {
|
|
353
|
+
const state = this.#getState({ callerName: "initializationError" });
|
|
354
|
+
return state.initializationError;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
#getAutoLoginAndInitializationErrorAccessErrorMessage(params: { callerName: string }) {
|
|
358
|
+
const { callerName } = params;
|
|
359
|
+
|
|
360
|
+
return [
|
|
361
|
+
`oidc-spa: ${callerName} was accessed but initialization failed.`,
|
|
362
|
+
"You are using `autoLogin: true`, so there is no anonymous state.",
|
|
363
|
+
"Handle this by gating your UI:",
|
|
364
|
+
"if (oidc.initializationError) show an error/fallback."
|
|
365
|
+
].join(" ");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
#getOidc(params: { callerName: string }) {
|
|
369
|
+
const { callerName } = params;
|
|
370
|
+
const state = this.#getState({ callerName });
|
|
371
|
+
if (state.oidc === undefined) {
|
|
372
|
+
// initialization failed
|
|
373
|
+
assert(state.initializationError !== undefined);
|
|
374
|
+
throw new Error(this.#getAutoLoginAndInitializationErrorAccessErrorMessage({ callerName }));
|
|
375
|
+
}
|
|
376
|
+
return state.oidc;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
get issuerUri() {
|
|
380
|
+
return this.#getOidc({ callerName: "issuerUri" }).params.issuerUri;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
get clientId() {
|
|
384
|
+
return this.#getOidc({ callerName: "clientId" }).params.clientId;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
get isUserLoggedIn() {
|
|
388
|
+
return this.#getOidc({ callerName: "isUserLoggedIn" }).isUserLoggedIn;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async login(params?: {
|
|
392
|
+
/**
|
|
393
|
+
* Add extra query parameters to the url before redirecting to the login pages.
|
|
394
|
+
*/
|
|
395
|
+
extraQueryParams?: Record<string, string | undefined>;
|
|
396
|
+
/**
|
|
397
|
+
* Where to redirect after successful login.
|
|
398
|
+
* Default: window.location.href (here)
|
|
399
|
+
*
|
|
400
|
+
* It does not need to include the origin, eg: "/dashboard"
|
|
401
|
+
*/
|
|
402
|
+
redirectUrl?: string;
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Transform the url before redirecting to the login pages.
|
|
406
|
+
* Prefer using the extraQueryParams parameter if you're only adding query parameters.
|
|
407
|
+
*/
|
|
408
|
+
transformUrlBeforeRedirect?: (url: string) => string;
|
|
409
|
+
}): Promise<never> {
|
|
410
|
+
await this.prInitialized;
|
|
411
|
+
|
|
412
|
+
const oidc = this.#getOidc({ callerName: "login" });
|
|
413
|
+
|
|
414
|
+
if (oidc.isUserLoggedIn) {
|
|
415
|
+
throw new Error(
|
|
416
|
+
[
|
|
417
|
+
"oidc-spa: login() called but the user is already logged in.",
|
|
418
|
+
"If you wish to send the user to the login page for some update",
|
|
419
|
+
"use oidc.goToAuthServer() instead"
|
|
420
|
+
].join(" ")
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return oidc.login({
|
|
425
|
+
...params,
|
|
426
|
+
doesCurrentHrefRequiresAuth: false
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async renewTokens(params?: {
|
|
431
|
+
extraTokenParams?: Record<string, string | undefined>;
|
|
432
|
+
}): Promise<void> {
|
|
433
|
+
await this.prInitialized;
|
|
434
|
+
|
|
435
|
+
const oidc = this.#getOidc({ callerName: "renewTokens" });
|
|
436
|
+
|
|
437
|
+
if (!oidc.isUserLoggedIn) {
|
|
438
|
+
throw new Error("oidc-spa: renewTokens() called but the user is not logged in.");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return oidc.renewTokens(params);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async logout(
|
|
445
|
+
params: { redirectTo: "home" | "current page" } | { redirectTo: "specific url"; url: string }
|
|
446
|
+
): Promise<never> {
|
|
447
|
+
await this.prInitialized;
|
|
448
|
+
|
|
449
|
+
const oidc = this.#getOidc({ callerName: "logout" });
|
|
450
|
+
|
|
451
|
+
if (!oidc.isUserLoggedIn) {
|
|
452
|
+
throw new Error("oidc-spa: logout() called but the user is not logged in.");
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return oidc.logout(params);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async goToAuthServer(params: {
|
|
459
|
+
extraQueryParams?: Record<string, string | undefined>;
|
|
460
|
+
redirectUrl?: string;
|
|
461
|
+
transformUrlBeforeRedirect?: (url: string) => string;
|
|
462
|
+
}): Promise<never> {
|
|
463
|
+
await this.prInitialized;
|
|
464
|
+
|
|
465
|
+
const oidc = this.#getOidc({ callerName: "goToAuthServer" });
|
|
466
|
+
|
|
467
|
+
if (!oidc.isUserLoggedIn) {
|
|
468
|
+
throw new Error("oidc-spa: goToAuthServer() called but the user is not logged in.");
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return oidc.goToAuthServer(params);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
readonly decodedIdToken$: ReadonlyBehaviorSubject<T_DecodedIdToken> = (() => {
|
|
475
|
+
const decodedIdToken$ = new BehaviorSubject<T_DecodedIdToken>(
|
|
476
|
+
createObjectThatThrowsIfAccessed({
|
|
477
|
+
debugMessage: this.#getPrInitializedNotResolvedErrorMessage({
|
|
478
|
+
callerName: "decodedIdToken"
|
|
479
|
+
})
|
|
480
|
+
})
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
(async () => {
|
|
484
|
+
const { initializationError, oidc } = await this.#dState.pr;
|
|
485
|
+
|
|
486
|
+
if (initializationError !== undefined) {
|
|
487
|
+
decodedIdToken$.next(
|
|
488
|
+
createObjectThatThrowsIfAccessed({
|
|
489
|
+
debugMessage: this.#getAutoLoginAndInitializationErrorAccessErrorMessage({
|
|
490
|
+
callerName: "decodedIdToken"
|
|
491
|
+
})
|
|
492
|
+
})
|
|
493
|
+
);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
assert(oidc !== undefined);
|
|
498
|
+
|
|
499
|
+
if (!oidc.isUserLoggedIn) {
|
|
500
|
+
decodedIdToken$.next(
|
|
501
|
+
createObjectThatThrowsIfAccessed({
|
|
502
|
+
debugMessage: [
|
|
503
|
+
`oidc-spa: Trying to read properties of decodedIdToken, the user`,
|
|
504
|
+
`isn't currently logged in, this does not make sense.`,
|
|
505
|
+
`You are responsible for controlling the flow of your app and`,
|
|
506
|
+
`not try to read the decodedIdToken when oidc.isUserLoggedIn is false.`
|
|
507
|
+
].join(" ")
|
|
508
|
+
})
|
|
509
|
+
);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
decodedIdToken$.next(oidc.getDecodedIdToken());
|
|
514
|
+
|
|
515
|
+
oidc.subscribeToTokensChange(() => {
|
|
516
|
+
const value_new = oidc.getDecodedIdToken();
|
|
517
|
+
const value_current = decodedIdToken$.getValue();
|
|
518
|
+
|
|
519
|
+
if (value_new === value_current) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
decodedIdToken$.next(value_new);
|
|
524
|
+
});
|
|
525
|
+
})();
|
|
526
|
+
|
|
527
|
+
return decodedIdToken$;
|
|
528
|
+
})();
|
|
529
|
+
|
|
530
|
+
readonly $decodedIdToken = toSignal(this.decodedIdToken$, { requireSync: true });
|
|
531
|
+
|
|
532
|
+
async getAccessToken(): Promise<
|
|
533
|
+
{ isUserLoggedIn: false; accessToken?: never } | { isUserLoggedIn: true; accessToken: string }
|
|
534
|
+
> {
|
|
535
|
+
await this.prInitialized;
|
|
536
|
+
|
|
537
|
+
const oidc = this.#getOidc({ callerName: "getAccessToken" });
|
|
538
|
+
|
|
539
|
+
return oidc.isUserLoggedIn
|
|
540
|
+
? { isUserLoggedIn: true, accessToken: (await oidc.getTokens()).accessToken }
|
|
541
|
+
: {
|
|
542
|
+
isUserLoggedIn: false
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
readonly $secondsLeftBeforeAutoLogout: Signal<number | null> = (() => {
|
|
547
|
+
const secondsLeftBeforeAutoLogout$ = new BehaviorSubject<number | null>(null);
|
|
548
|
+
|
|
549
|
+
(async () => {
|
|
550
|
+
const { oidc } = await this.#dState.pr;
|
|
551
|
+
|
|
552
|
+
if (oidc === undefined) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (!oidc.isUserLoggedIn) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
oidc.subscribeToAutoLogoutCountdown(({ secondsLeft }) => {
|
|
561
|
+
if (secondsLeft === undefined || secondsLeft > this.#autoLogoutWarningDurationSeconds) {
|
|
562
|
+
if (secondsLeftBeforeAutoLogout$.getValue() !== null) {
|
|
563
|
+
secondsLeftBeforeAutoLogout$.next(null);
|
|
564
|
+
}
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
secondsLeftBeforeAutoLogout$.next(secondsLeft);
|
|
568
|
+
});
|
|
569
|
+
})();
|
|
570
|
+
|
|
571
|
+
return toSignal(secondsLeftBeforeAutoLogout$, { requireSync: true });
|
|
572
|
+
})();
|
|
573
|
+
|
|
574
|
+
get isNewBrowserSession() {
|
|
575
|
+
const oidc = this.#getOidc({ callerName: "isNewBrowserSession" });
|
|
576
|
+
|
|
577
|
+
if (!oidc.isUserLoggedIn) {
|
|
578
|
+
throw new Error("oidc-spa: isNewBrowserSession was used but the used is not logged in");
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return oidc.isNewBrowserSession;
|
|
582
|
+
}
|
|
583
|
+
}
|
package/src/core/createOidc.ts
CHANGED
|
@@ -56,6 +56,14 @@ export type ParamsOfCreateOidc<
|
|
|
56
56
|
DecodedIdToken extends Record<string, unknown> = Oidc.Tokens.DecodedIdToken_base,
|
|
57
57
|
AutoLogin extends boolean = false
|
|
58
58
|
> = {
|
|
59
|
+
/**
|
|
60
|
+
* What should you put in this parameter?
|
|
61
|
+
* - Vite project: `BASE_URL: import.meta.env.BASE_URL`
|
|
62
|
+
* - Create React App project: `BASE_URL: process.env.PUBLIC_URL`
|
|
63
|
+
* - Other: `BASE_URL: "/"` (Usually, or `/dashboard` if your app is not at the root of the domain)
|
|
64
|
+
*/
|
|
65
|
+
homeUrl: string;
|
|
66
|
+
|
|
59
67
|
issuerUri: string;
|
|
60
68
|
clientId: string;
|
|
61
69
|
/**
|
|
@@ -110,14 +118,6 @@ export type ParamsOfCreateOidc<
|
|
|
110
118
|
*/
|
|
111
119
|
postLoginRedirectUrl?: string;
|
|
112
120
|
|
|
113
|
-
/**
|
|
114
|
-
* What should you put in this parameter?
|
|
115
|
-
* - Vite project: `BASE_URL: import.meta.env.BASE_URL`
|
|
116
|
-
* - Create React App project: `BASE_URL: process.env.PUBLIC_URL`
|
|
117
|
-
* - Other: `BASE_URL: "/"` (Usually, or `/dashboard` if your app is not at the root of the domain)
|
|
118
|
-
*/
|
|
119
|
-
homeUrl: string;
|
|
120
|
-
|
|
121
121
|
decodedIdTokenSchema?: {
|
|
122
122
|
parse: (decodedIdToken_original: Oidc.Tokens.DecodedIdToken_base) => DecodedIdToken;
|
|
123
123
|
};
|
|
@@ -1653,8 +1653,6 @@ export async function createOidc_nonMemoized<
|
|
|
1653
1653
|
);
|
|
1654
1654
|
};
|
|
1655
1655
|
|
|
1656
|
-
invokeAllCallbacks({ secondsLeft });
|
|
1657
|
-
|
|
1658
1656
|
if (secondsLeft === 0) {
|
|
1659
1657
|
cancel_if_offline: {
|
|
1660
1658
|
const { isOnline, prOnline } = getIsOnline();
|
|
@@ -1693,6 +1691,8 @@ export async function createOidc_nonMemoized<
|
|
|
1693
1691
|
|
|
1694
1692
|
await oidc_loggedIn.logout(autoLogoutParams);
|
|
1695
1693
|
}
|
|
1694
|
+
|
|
1695
|
+
invokeAllCallbacks({ secondsLeft });
|
|
1696
1696
|
}
|
|
1697
1697
|
});
|
|
1698
1698
|
|
package/src/tools/Deferred.ts
CHANGED
|
@@ -4,13 +4,19 @@ export class Deferred<T> {
|
|
|
4
4
|
/** NOTE: Does not need to be called bound to instance*/
|
|
5
5
|
public readonly resolve: (value: T) => void;
|
|
6
6
|
public readonly reject: (error: any) => void;
|
|
7
|
+
public readonly getState: () =>
|
|
8
|
+
| { hasResolved: true; value: T }
|
|
9
|
+
| { hasResolved: false; value?: never };
|
|
7
10
|
|
|
8
11
|
constructor() {
|
|
9
12
|
let resolve!: (value: T) => void;
|
|
10
13
|
let reject!: (error: any) => void;
|
|
11
14
|
|
|
15
|
+
let valueWrap: [T] | undefined = undefined;
|
|
16
|
+
|
|
12
17
|
this.pr = new Promise<T>((resolve_, reject_) => {
|
|
13
18
|
resolve = value => {
|
|
19
|
+
valueWrap = [value];
|
|
14
20
|
resolve_(value);
|
|
15
21
|
};
|
|
16
22
|
|
|
@@ -21,6 +27,10 @@ export class Deferred<T> {
|
|
|
21
27
|
|
|
22
28
|
this.resolve = resolve;
|
|
23
29
|
this.reject = reject;
|
|
30
|
+
this.getState = () =>
|
|
31
|
+
valueWrap === undefined
|
|
32
|
+
? { hasResolved: false }
|
|
33
|
+
: { hasResolved: true, value: valueWrap[0] };
|
|
24
34
|
}
|
|
25
35
|
}
|
|
26
36
|
|