oidc-spa 8.0.4 → 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 +15 -2
- package/core/createOidc.js.map +1 -1
- package/core/oidcClientTsUserToTokens.js +7 -0
- package/core/oidcClientTsUserToTokens.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 +15 -2
- package/esm/core/createOidc.js.map +1 -1
- package/esm/core/oidcClientTsUserToTokens.js +7 -0
- package/esm/core/oidcClientTsUserToTokens.js.map +1 -1
- package/esm/keycloak/keycloakUtils.js.map +1 -1
- package/esm/mock/oidc.js +2 -1
- package/esm/mock/oidc.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/INFINITY_TIME.d.ts +1 -0
- package/esm/tools/INFINITY_TIME.js +3 -0
- package/esm/tools/INFINITY_TIME.js.map +1 -0
- 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/tools/readExpirationTimeInJwt.js +4 -0
- package/esm/tools/readExpirationTimeInJwt.js.map +1 -1
- 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/mock/oidc.js +2 -1
- package/mock/oidc.js.map +1 -1
- package/package.json +3 -3
- package/src/angular.ts +583 -0
- package/src/core/createOidc.ts +28 -10
- package/src/core/oidcClientTsUserToTokens.ts +9 -0
- package/src/keycloak/keycloakUtils.ts +0 -1
- package/src/mock/oidc.ts +2 -1
- package/src/tools/ConcreteClass.ts +3 -0
- package/src/tools/Deferred.ts +10 -0
- package/src/tools/INFINITY_TIME.ts +2 -0
- package/src/tools/ReadonlyBehaviorSubject.ts +9 -0
- package/src/tools/getBaseHref.ts +7 -0
- package/src/tools/readExpirationTimeInJwt.ts +5 -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/INFINITY_TIME.d.ts +1 -0
- package/tools/INFINITY_TIME.js +6 -0
- package/tools/INFINITY_TIME.js.map +1 -0
- 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/tools/readExpirationTimeInJwt.js +4 -0
- package/tools/readExpirationTimeInJwt.js.map +1 -1
- 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 -253
- 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 -249
- 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 -428
- 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
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
import { createGetIsNewBrowserSession } from "./isNewBrowserSession";
|
|
48
48
|
import { getIsOnline } from "../tools/getIsOnline";
|
|
49
49
|
import { isKeycloak } from "../keycloak/isKeycloak";
|
|
50
|
+
import { INFINITY_TIME } from "../tools/INFINITY_TIME";
|
|
50
51
|
|
|
51
52
|
// NOTE: Replaced at build time
|
|
52
53
|
const VERSION = "{{OIDC_SPA_VERSION}}";
|
|
@@ -55,6 +56,14 @@ export type ParamsOfCreateOidc<
|
|
|
55
56
|
DecodedIdToken extends Record<string, unknown> = Oidc.Tokens.DecodedIdToken_base,
|
|
56
57
|
AutoLogin extends boolean = false
|
|
57
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
|
+
|
|
58
67
|
issuerUri: string;
|
|
59
68
|
clientId: string;
|
|
60
69
|
/**
|
|
@@ -109,14 +118,6 @@ export type ParamsOfCreateOidc<
|
|
|
109
118
|
*/
|
|
110
119
|
postLoginRedirectUrl?: string;
|
|
111
120
|
|
|
112
|
-
/**
|
|
113
|
-
* What should you put in this parameter?
|
|
114
|
-
* - Vite project: `BASE_URL: import.meta.env.BASE_URL`
|
|
115
|
-
* - Create React App project: `BASE_URL: process.env.PUBLIC_URL`
|
|
116
|
-
* - Other: `BASE_URL: "/"` (Usually, or `/dashboard` if your app is not at the root of the domain)
|
|
117
|
-
*/
|
|
118
|
-
homeUrl: string;
|
|
119
|
-
|
|
120
121
|
decodedIdTokenSchema?: {
|
|
121
122
|
parse: (decodedIdToken_original: Oidc.Tokens.DecodedIdToken_base) => DecodedIdToken;
|
|
122
123
|
};
|
|
@@ -1502,6 +1503,14 @@ export async function createOidc_nonMemoized<
|
|
|
1502
1503
|
return;
|
|
1503
1504
|
}
|
|
1504
1505
|
|
|
1506
|
+
if (
|
|
1507
|
+
currentTokens.refreshTokenExpirationTime !== undefined &&
|
|
1508
|
+
currentTokens.refreshTokenExpirationTime >= INFINITY_TIME
|
|
1509
|
+
) {
|
|
1510
|
+
log?.("The refresh_token never expires, disabling auto-renewal mechanism");
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1505
1514
|
const msBeforeExpiration =
|
|
1506
1515
|
(currentTokens.refreshTokenExpirationTime ?? currentTokens.accessTokenExpirationTime) -
|
|
1507
1516
|
currentTokens.getServerDateNow();
|
|
@@ -1612,9 +1621,18 @@ export async function createOidc_nonMemoized<
|
|
|
1612
1621
|
return undefined;
|
|
1613
1622
|
}
|
|
1614
1623
|
|
|
1624
|
+
if (currentTokens.refreshTokenExpirationTime >= INFINITY_TIME) {
|
|
1625
|
+
return 0;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1615
1628
|
return (currentTokens.refreshTokenExpirationTime - currentTokens.issuedAtTime) / 1000;
|
|
1616
1629
|
};
|
|
1617
1630
|
|
|
1631
|
+
if (getCurrentRefreshTokenTtlInSeconds() === 0) {
|
|
1632
|
+
log?.("The refresh_token never expires, disabling auto logout mechanism");
|
|
1633
|
+
break auto_logout;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1618
1636
|
if (getCurrentRefreshTokenTtlInSeconds() === undefined) {
|
|
1619
1637
|
log?.(
|
|
1620
1638
|
`${
|
|
@@ -1635,8 +1653,6 @@ export async function createOidc_nonMemoized<
|
|
|
1635
1653
|
);
|
|
1636
1654
|
};
|
|
1637
1655
|
|
|
1638
|
-
invokeAllCallbacks({ secondsLeft });
|
|
1639
|
-
|
|
1640
1656
|
if (secondsLeft === 0) {
|
|
1641
1657
|
cancel_if_offline: {
|
|
1642
1658
|
const { isOnline, prOnline } = getIsOnline();
|
|
@@ -1675,6 +1691,8 @@ export async function createOidc_nonMemoized<
|
|
|
1675
1691
|
|
|
1676
1692
|
await oidc_loggedIn.logout(autoLogoutParams);
|
|
1677
1693
|
}
|
|
1694
|
+
|
|
1695
|
+
invokeAllCallbacks({ secondsLeft });
|
|
1678
1696
|
}
|
|
1679
1697
|
});
|
|
1680
1698
|
|
|
@@ -3,6 +3,7 @@ import { assert, id } from "../vendor/frontend/tsafe";
|
|
|
3
3
|
import { readExpirationTimeInJwt } from "../tools/readExpirationTimeInJwt";
|
|
4
4
|
import { decodeJwt } from "../tools/decodeJwt";
|
|
5
5
|
import type { Oidc } from "./Oidc";
|
|
6
|
+
import { INFINITY_TIME } from "../tools/INFINITY_TIME";
|
|
6
7
|
|
|
7
8
|
export function oidcClientTsUserToTokens<DecodedIdToken extends Record<string, unknown>>(params: {
|
|
8
9
|
oidcClientTsUser: OidcClientTsUser;
|
|
@@ -187,6 +188,10 @@ export function oidcClientTsUserToTokens<DecodedIdToken extends Record<string, u
|
|
|
187
188
|
|
|
188
189
|
assert(typeof refresh_expires_at === "number", "2033392");
|
|
189
190
|
|
|
191
|
+
if (refresh_expires_at === 0) {
|
|
192
|
+
return INFINITY_TIME;
|
|
193
|
+
}
|
|
194
|
+
|
|
190
195
|
return refresh_expires_at * 1000;
|
|
191
196
|
}
|
|
192
197
|
|
|
@@ -199,6 +204,10 @@ export function oidcClientTsUserToTokens<DecodedIdToken extends Record<string, u
|
|
|
199
204
|
|
|
200
205
|
assert(typeof refresh_expires_in === "number", "2033425330");
|
|
201
206
|
|
|
207
|
+
if (refresh_expires_in === 0) {
|
|
208
|
+
return INFINITY_TIME;
|
|
209
|
+
}
|
|
210
|
+
|
|
202
211
|
return issuedAtTime + refresh_expires_in * 1000;
|
|
203
212
|
}
|
|
204
213
|
|