oidc-spa 7.2.1 → 7.2.2

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.
Files changed (197) hide show
  1. package/backend.js.map +1 -1
  2. package/core/AuthResponse.js.map +1 -1
  3. package/core/Oidc.js.map +1 -1
  4. package/core/OidcInitializationError.js.map +1 -1
  5. package/core/OidcMetadata.js.map +1 -1
  6. package/core/StateData.js.map +1 -1
  7. package/core/configId.js.map +1 -1
  8. package/core/createOidc.js +1 -1
  9. package/core/createOidc.js.map +1 -1
  10. package/core/diagnostic.js.map +1 -1
  11. package/core/evtIsUserActive.js.map +1 -1
  12. package/core/handleOidcCallback.js.map +1 -1
  13. package/core/iframeMessageProtection.js.map +1 -1
  14. package/core/index.js.map +1 -1
  15. package/core/initialLocationHref.js.map +1 -1
  16. package/core/isNewBrowserSession.js.map +1 -1
  17. package/core/loginOrGoToAuthServer.js.map +1 -1
  18. package/core/loginPropagationToOtherTabs.js.map +1 -1
  19. package/core/loginSilent.js.map +1 -1
  20. package/core/logoutPropagationToOtherTabs.js.map +1 -1
  21. package/core/oidcClientTsUserToTokens.js.map +1 -1
  22. package/core/ongoingLoginOrRefreshProcesses.js.map +1 -1
  23. package/core/persistedAuthState.js.map +1 -1
  24. package/entrypoint.js.map +1 -1
  25. package/esm/core/AuthResponse.js.map +1 -1
  26. package/esm/core/Oidc.js.map +1 -1
  27. package/esm/core/OidcInitializationError.js.map +1 -1
  28. package/esm/core/OidcMetadata.js.map +1 -1
  29. package/esm/core/StateData.js.map +1 -1
  30. package/esm/core/configId.js.map +1 -1
  31. package/esm/core/createOidc.js +1 -1
  32. package/esm/core/createOidc.js.map +1 -1
  33. package/esm/core/diagnostic.js.map +1 -1
  34. package/esm/core/evtIsUserActive.js.map +1 -1
  35. package/esm/core/handleOidcCallback.js.map +1 -1
  36. package/esm/core/iframeMessageProtection.js.map +1 -1
  37. package/esm/core/index.js.map +1 -1
  38. package/esm/core/initialLocationHref.js.map +1 -1
  39. package/esm/core/isNewBrowserSession.js.map +1 -1
  40. package/esm/core/loginOrGoToAuthServer.js.map +1 -1
  41. package/esm/core/loginPropagationToOtherTabs.js.map +1 -1
  42. package/esm/core/loginSilent.js.map +1 -1
  43. package/esm/core/logoutPropagationToOtherTabs.js.map +1 -1
  44. package/esm/core/oidcClientTsUserToTokens.js.map +1 -1
  45. package/esm/core/ongoingLoginOrRefreshProcesses.js.map +1 -1
  46. package/esm/core/persistedAuthState.js.map +1 -1
  47. package/esm/entrypoint.js.map +1 -1
  48. package/esm/index.js.map +1 -1
  49. package/esm/keycloak/index.js.map +1 -1
  50. package/esm/keycloak/isKeycloak.js.map +1 -1
  51. package/esm/keycloak/keycloak-js/Keycloak.js.map +1 -1
  52. package/esm/keycloak/keycloak-js/index.js.map +1 -1
  53. package/esm/keycloak/keycloak-js/types.js.map +1 -1
  54. package/esm/keycloak/keycloakIssuerUriParsed.js.map +1 -1
  55. package/esm/keycloak/keycloakUtils.js.map +1 -1
  56. package/esm/keycloak-js.js.map +1 -1
  57. package/esm/mock/index.js.map +1 -1
  58. package/esm/mock/oidc.js.map +1 -1
  59. package/esm/mock/react.js.map +1 -1
  60. package/esm/react/index.js.map +1 -1
  61. package/esm/react/react.js.map +1 -1
  62. package/esm/tools/Deferred.js.map +1 -1
  63. package/esm/tools/EphemeralSessionStorage.js.map +1 -1
  64. package/esm/tools/Evt.js.map +1 -1
  65. package/esm/tools/StatefulEvt.js.map +1 -1
  66. package/esm/tools/ValueOrAsyncGetter.js.map +1 -1
  67. package/esm/tools/asymmetricEncryption.js.map +1 -1
  68. package/esm/tools/base64.js.map +1 -1
  69. package/esm/tools/createObjectThatThrowsIfAccessed.js.map +1 -1
  70. package/esm/tools/decodeJwt.js.map +1 -1
  71. package/esm/tools/generateUrlSafeRandom.js.map +1 -1
  72. package/esm/tools/getDownlinkAndRtt.js.map +1 -1
  73. package/esm/tools/getIsOnline.js.map +1 -1
  74. package/esm/tools/getIsValidRemoteJson.js.map +1 -1
  75. package/esm/tools/getPrUserInteraction.js.map +1 -1
  76. package/esm/tools/getUserEnvironmentInfo.js.map +1 -1
  77. package/esm/tools/haveSharedParentDomain.js.map +1 -1
  78. package/esm/tools/isDev.js.map +1 -1
  79. package/esm/tools/parseKeycloakIssuerUri.js.map +1 -1
  80. package/esm/tools/readExpirationTimeInJwt.js.map +1 -1
  81. package/esm/tools/startCountdown.js.map +1 -1
  82. package/esm/tools/subscribeToUserInteraction.js.map +1 -1
  83. package/esm/tools/toFullyQualifiedUrl.js.map +1 -1
  84. package/esm/tools/toHumanReadableDuration.js.map +1 -1
  85. package/esm/tools/urlSearchParams.js.map +1 -1
  86. package/esm/tools/workerTimers.js.map +1 -1
  87. package/index.js.map +1 -1
  88. package/keycloak/index.js.map +1 -1
  89. package/keycloak/isKeycloak.js.map +1 -1
  90. package/keycloak/keycloak-js/Keycloak.js.map +1 -1
  91. package/keycloak/keycloak-js/index.js.map +1 -1
  92. package/keycloak/keycloak-js/types.js.map +1 -1
  93. package/keycloak/keycloakIssuerUriParsed.js.map +1 -1
  94. package/keycloak/keycloakUtils.js.map +1 -1
  95. package/keycloak-js.js.map +1 -1
  96. package/mock/index.js.map +1 -1
  97. package/mock/oidc.js.map +1 -1
  98. package/mock/react.js.map +1 -1
  99. package/package.json +1 -1
  100. package/react/index.js.map +1 -1
  101. package/react/react.js.map +1 -1
  102. package/src/backend.ts +391 -0
  103. package/src/core/AuthResponse.ts +26 -0
  104. package/src/core/Oidc.ts +140 -0
  105. package/src/core/OidcInitializationError.ts +19 -0
  106. package/src/core/OidcMetadata.ts +271 -0
  107. package/src/core/StateData.ts +118 -0
  108. package/src/core/configId.ts +3 -0
  109. package/src/core/createOidc.ts +1576 -0
  110. package/src/core/diagnostic.ts +267 -0
  111. package/src/core/evtIsUserActive.ts +108 -0
  112. package/src/core/handleOidcCallback.ts +321 -0
  113. package/src/core/iframeMessageProtection.ts +100 -0
  114. package/src/core/index.ts +4 -0
  115. package/src/core/initialLocationHref.ts +5 -0
  116. package/src/core/isNewBrowserSession.ts +37 -0
  117. package/src/core/loginOrGoToAuthServer.ts +324 -0
  118. package/src/core/loginPropagationToOtherTabs.ts +51 -0
  119. package/src/core/loginSilent.ts +242 -0
  120. package/src/core/logoutPropagationToOtherTabs.ts +53 -0
  121. package/src/core/oidcClientTsUserToTokens.ts +229 -0
  122. package/src/core/ongoingLoginOrRefreshProcesses.ts +47 -0
  123. package/src/core/persistedAuthState.ts +122 -0
  124. package/src/entrypoint.ts +69 -0
  125. package/src/index.ts +1 -0
  126. package/src/keycloak/index.ts +8 -0
  127. package/src/keycloak/isKeycloak.ts +23 -0
  128. package/src/keycloak/keycloak-js/Keycloak.ts +1097 -0
  129. package/src/keycloak/keycloak-js/index.ts +2 -0
  130. package/src/keycloak/keycloak-js/types.ts +442 -0
  131. package/src/keycloak/keycloakIssuerUriParsed.ts +29 -0
  132. package/src/keycloak/keycloakUtils.ts +90 -0
  133. package/src/keycloak-js.ts +1 -0
  134. package/src/mock/index.ts +1 -0
  135. package/src/mock/oidc.ts +211 -0
  136. package/src/mock/react.tsx +11 -0
  137. package/src/react/index.ts +1 -0
  138. package/src/react/react.tsx +476 -0
  139. package/src/tools/Deferred.ts +33 -0
  140. package/src/tools/EphemeralSessionStorage.ts +223 -0
  141. package/src/tools/Evt.ts +56 -0
  142. package/src/tools/StatefulEvt.ts +38 -0
  143. package/src/tools/ValueOrAsyncGetter.ts +1 -0
  144. package/src/tools/asymmetricEncryption.ts +184 -0
  145. package/src/tools/base64.ts +7 -0
  146. package/src/tools/createObjectThatThrowsIfAccessed.ts +40 -0
  147. package/src/tools/decodeJwt.ts +95 -0
  148. package/src/tools/generateUrlSafeRandom.ts +26 -0
  149. package/src/tools/getDownlinkAndRtt.ts +22 -0
  150. package/src/tools/getIsOnline.ts +20 -0
  151. package/src/tools/getIsValidRemoteJson.ts +18 -0
  152. package/src/tools/getPrUserInteraction.ts +27 -0
  153. package/src/tools/getUserEnvironmentInfo.ts +42 -0
  154. package/src/tools/haveSharedParentDomain.ts +13 -0
  155. package/src/tools/isDev.ts +30 -0
  156. package/src/tools/parseKeycloakIssuerUri.ts +49 -0
  157. package/src/tools/readExpirationTimeInJwt.ts +16 -0
  158. package/src/tools/startCountdown.ts +36 -0
  159. package/src/tools/subscribeToUserInteraction.ts +33 -0
  160. package/src/tools/toFullyQualifiedUrl.ts +58 -0
  161. package/src/tools/toHumanReadableDuration.ts +21 -0
  162. package/src/tools/urlSearchParams.ts +130 -0
  163. package/src/tools/workerTimers.ts +57 -0
  164. package/src/vendor/backend/evt.ts +2 -0
  165. package/src/vendor/backend/jsonwebtoken.ts +1 -0
  166. package/src/vendor/backend/node-fetch.ts +2 -0
  167. package/src/vendor/backend/node-jose.ts +1 -0
  168. package/src/vendor/backend/tsafe.ts +5 -0
  169. package/src/vendor/backend/zod.ts +1 -0
  170. package/src/vendor/frontend/oidc-client-ts.ts +1 -0
  171. package/src/vendor/frontend/tsafe.ts +6 -0
  172. package/src/vendor/frontend/worker-timers.ts +2 -0
  173. package/tools/Deferred.js.map +1 -1
  174. package/tools/EphemeralSessionStorage.js.map +1 -1
  175. package/tools/Evt.js.map +1 -1
  176. package/tools/StatefulEvt.js.map +1 -1
  177. package/tools/ValueOrAsyncGetter.js.map +1 -1
  178. package/tools/asymmetricEncryption.js.map +1 -1
  179. package/tools/base64.js.map +1 -1
  180. package/tools/createObjectThatThrowsIfAccessed.js.map +1 -1
  181. package/tools/decodeJwt.js.map +1 -1
  182. package/tools/generateUrlSafeRandom.js.map +1 -1
  183. package/tools/getDownlinkAndRtt.js.map +1 -1
  184. package/tools/getIsOnline.js.map +1 -1
  185. package/tools/getIsValidRemoteJson.js.map +1 -1
  186. package/tools/getPrUserInteraction.js.map +1 -1
  187. package/tools/getUserEnvironmentInfo.js.map +1 -1
  188. package/tools/haveSharedParentDomain.js.map +1 -1
  189. package/tools/isDev.js.map +1 -1
  190. package/tools/parseKeycloakIssuerUri.js.map +1 -1
  191. package/tools/readExpirationTimeInJwt.js.map +1 -1
  192. package/tools/startCountdown.js.map +1 -1
  193. package/tools/subscribeToUserInteraction.js.map +1 -1
  194. package/tools/toFullyQualifiedUrl.js.map +1 -1
  195. package/tools/toHumanReadableDuration.js.map +1 -1
  196. package/tools/urlSearchParams.js.map +1 -1
  197. package/tools/workerTimers.js.map +1 -1
package/src/backend.ts ADDED
@@ -0,0 +1,391 @@
1
+ import { fetch } from "./vendor/backend/node-fetch";
2
+ import { assert, isAmong, id, type Equals, is, exclude } from "./vendor/backend/tsafe";
3
+ import { JWK } from "./vendor/backend/node-jose";
4
+ import * as jwt from "./vendor/backend/jsonwebtoken";
5
+ import { z } from "./vendor/backend/zod";
6
+ import { Evt } from "./vendor/backend/evt";
7
+ import { throttleTime } from "./vendor/backend/evt";
8
+
9
+ export type ParamsOfCreateOidcBackend<DecodedAccessToken extends Record<string, unknown>> = {
10
+ issuerUri: string;
11
+ decodedAccessTokenSchema?: { parse: (data: unknown) => DecodedAccessToken };
12
+ };
13
+
14
+ export type OidcBackend<DecodedAccessToken extends Record<string, unknown>> = {
15
+ verifyAndDecodeAccessToken(params: {
16
+ accessToken: string;
17
+ }): ResultOfAccessTokenVerify<DecodedAccessToken>;
18
+ };
19
+
20
+ export type ResultOfAccessTokenVerify<DecodedAccessToken> =
21
+ | ResultOfAccessTokenVerify.Valid<DecodedAccessToken>
22
+ | ResultOfAccessTokenVerify.Invalid;
23
+
24
+ export namespace ResultOfAccessTokenVerify {
25
+ export type Valid<DecodedAccessToken> = {
26
+ isValid: true;
27
+ decodedAccessToken: DecodedAccessToken;
28
+
29
+ errorCase?: never;
30
+ errorMessage?: never;
31
+ };
32
+
33
+ export type Invalid = {
34
+ isValid: false;
35
+ errorCase: "expired" | "invalid signature" | "does not respect schema";
36
+ errorMessage: string;
37
+
38
+ decodedAccessToken?: never;
39
+ };
40
+ }
41
+
42
+ export async function createOidcBackend<DecodedAccessToken extends Record<string, unknown>>(
43
+ params: ParamsOfCreateOidcBackend<DecodedAccessToken>
44
+ ): Promise<OidcBackend<DecodedAccessToken>> {
45
+ const { issuerUri, decodedAccessTokenSchema = z.record(z.unknown()) } = params;
46
+
47
+ let publicSigningKeys = await fetchPublicSigningKeys({ issuerUri });
48
+
49
+ const evtInvalidSignature = Evt.create<void>();
50
+
51
+ evtInvalidSignature.pipe(throttleTime(3600_000)).attach(async () => {
52
+ const publicSigningKeys_new = await (async function callee(
53
+ count: number
54
+ ): Promise<PublicSigningKey[] | undefined> {
55
+ let wrap;
56
+
57
+ try {
58
+ wrap = await fetchPublicSigningKeys({ issuerUri });
59
+ } catch (error) {
60
+ if (count === 9) {
61
+ console.warn(
62
+ `Failed to refresh public key and signing algorithm after ${count + 1} attempts`
63
+ );
64
+
65
+ return undefined;
66
+ }
67
+
68
+ const delayMs = 1000 * Math.pow(2, count);
69
+
70
+ console.warn(
71
+ `Failed to refresh public key and signing algorithm: ${String(
72
+ error
73
+ )}, retrying in ${delayMs}ms`
74
+ );
75
+
76
+ await new Promise(resolve => setTimeout(resolve, delayMs));
77
+
78
+ return callee(count + 1);
79
+ }
80
+
81
+ return wrap;
82
+ })(0);
83
+
84
+ if (publicSigningKeys_new === undefined) {
85
+ return;
86
+ }
87
+
88
+ publicSigningKeys = publicSigningKeys_new;
89
+ });
90
+
91
+ return {
92
+ verifyAndDecodeAccessToken: ({ accessToken }) => {
93
+ let kid: string;
94
+ let alg: jwt.Algorithm;
95
+
96
+ {
97
+ const jwtHeader_b64 = accessToken.split(".")[0];
98
+
99
+ let jwtHeader: string;
100
+
101
+ try {
102
+ jwtHeader = Buffer.from(jwtHeader_b64, "base64").toString("utf8");
103
+ } catch {
104
+ return {
105
+ isValid: false,
106
+ errorCase: "invalid signature",
107
+ errorMessage: "Failed to decode the JWT header as a base64 string"
108
+ };
109
+ }
110
+
111
+ let decodedHeader: unknown;
112
+
113
+ try {
114
+ decodedHeader = JSON.parse(jwtHeader);
115
+ } catch {
116
+ return {
117
+ isValid: false,
118
+ errorCase: "invalid signature",
119
+ errorMessage: "Failed to parse the JWT header as a JSON"
120
+ };
121
+ }
122
+
123
+ type DecodedHeader = {
124
+ kid: string;
125
+ alg: string;
126
+ };
127
+
128
+ const zDecodedHeader = z.object({
129
+ kid: z.string(),
130
+ alg: z.string()
131
+ });
132
+
133
+ assert<Equals<DecodedHeader, z.infer<typeof zDecodedHeader>>>();
134
+
135
+ try {
136
+ zDecodedHeader.parse(decodedHeader);
137
+ } catch {
138
+ return {
139
+ isValid: false,
140
+ errorCase: "invalid signature",
141
+ errorMessage: "The decoded JWT header does not have a kid property"
142
+ };
143
+ }
144
+
145
+ assert(is<DecodedHeader>(decodedHeader));
146
+
147
+ {
148
+ const supportedAlgs = [
149
+ "RS256",
150
+ "RS384",
151
+ "RS512",
152
+ "ES256",
153
+ "ES384",
154
+ "ES512",
155
+ "PS256",
156
+ "PS384",
157
+ "PS512"
158
+ ] as const;
159
+
160
+ assert<
161
+ Equals<
162
+ (typeof supportedAlgs)[number] | "none" | "HS256" | "HS384" | "HS512",
163
+ jwt.Algorithm
164
+ >
165
+ >();
166
+
167
+ if (!isAmong(supportedAlgs, decodedHeader.alg)) {
168
+ return {
169
+ isValid: false,
170
+ errorCase: "invalid signature",
171
+ errorMessage: `Unsupported or too week algorithm ${decodedHeader.alg}`
172
+ };
173
+ }
174
+ }
175
+
176
+ kid = decodedHeader.kid;
177
+ alg = decodedHeader.alg;
178
+ }
179
+
180
+ const publicSigningKey = publicSigningKeys.find(
181
+ publicSigningKey => publicSigningKey.kid === kid
182
+ );
183
+
184
+ if (publicSigningKey === undefined) {
185
+ return {
186
+ isValid: false,
187
+ errorCase: "invalid signature",
188
+ errorMessage: `No public signing key found with kid ${kid}`
189
+ };
190
+ }
191
+
192
+ let result = id<ResultOfAccessTokenVerify<DecodedAccessToken> | undefined>(undefined);
193
+
194
+ jwt.verify(
195
+ accessToken,
196
+ publicSigningKey.publicKey,
197
+ { algorithms: [alg] },
198
+ (err, decoded) => {
199
+ invalid: {
200
+ if (!err) {
201
+ break invalid;
202
+ }
203
+
204
+ if (err.name === "TokenExpiredError") {
205
+ result = id<ResultOfAccessTokenVerify.Invalid>({
206
+ isValid: false,
207
+ errorCase: "expired",
208
+ errorMessage: err.message
209
+ });
210
+ return;
211
+ }
212
+
213
+ evtInvalidSignature.post();
214
+
215
+ result = id<ResultOfAccessTokenVerify.Invalid>({
216
+ isValid: false,
217
+ errorCase: "invalid signature",
218
+ errorMessage: err.message
219
+ });
220
+
221
+ return;
222
+ }
223
+
224
+ let decodedAccessToken: DecodedAccessToken;
225
+
226
+ try {
227
+ decodedAccessToken = decodedAccessTokenSchema.parse(
228
+ decoded
229
+ ) as DecodedAccessToken;
230
+ } catch (error) {
231
+ result = id<ResultOfAccessTokenVerify.Invalid>({
232
+ isValid: false,
233
+ errorCase: "does not respect schema",
234
+ errorMessage: String(error)
235
+ });
236
+
237
+ return;
238
+ }
239
+
240
+ result = id<ResultOfAccessTokenVerify.Valid<DecodedAccessToken>>({
241
+ isValid: true,
242
+ decodedAccessToken: decodedAccessToken
243
+ });
244
+ }
245
+ );
246
+
247
+ assert(result !== undefined, "0522e6");
248
+
249
+ return result;
250
+ }
251
+ };
252
+ }
253
+
254
+ type PublicSigningKey = {
255
+ kid: string;
256
+ publicKey: string;
257
+ };
258
+
259
+ async function fetchPublicSigningKeys(params: { issuerUri: string }): Promise<PublicSigningKey[]> {
260
+ const { issuerUri } = params;
261
+
262
+ const { jwks_uri } = await (async () => {
263
+ const url = `${issuerUri.replace(/\/$/, "")}/.well-known/openid-configuration`;
264
+
265
+ const response = await fetch(url);
266
+
267
+ if (!response.ok) {
268
+ throw new Error(
269
+ `Failed to fetch openid configuration of the issuerUri: ${issuerUri} (${url}): ${response.statusText}`
270
+ );
271
+ }
272
+
273
+ let data: unknown;
274
+
275
+ try {
276
+ data = await response.json();
277
+ } catch (error) {
278
+ throw new Error(`Failed to parse json from ${url}: ${String(error)}`);
279
+ }
280
+
281
+ {
282
+ type WellKnownConfiguration = {
283
+ jwks_uri: string;
284
+ };
285
+
286
+ const zWellKnownConfiguration = z.object({
287
+ jwks_uri: z.string()
288
+ });
289
+
290
+ assert<Equals<WellKnownConfiguration, z.infer<typeof zWellKnownConfiguration>>>();
291
+
292
+ try {
293
+ zWellKnownConfiguration.parse(data);
294
+ } catch {
295
+ throw new Error(`${url} does not have a jwks_uri property`);
296
+ }
297
+
298
+ assert(is<WellKnownConfiguration>(data));
299
+ }
300
+
301
+ const { jwks_uri } = data;
302
+
303
+ return { jwks_uri };
304
+ })();
305
+
306
+ const { jwks } = await (async () => {
307
+ const response = await fetch(jwks_uri);
308
+
309
+ if (!response.ok) {
310
+ throw new Error(
311
+ `Failed to fetch public key and algorithm from ${jwks_uri}: ${response.statusText}`
312
+ );
313
+ }
314
+
315
+ let jwks: unknown;
316
+
317
+ try {
318
+ jwks = await response.json();
319
+ } catch (error) {
320
+ throw new Error(`Failed to parse json from ${jwks_uri}: ${String(error)}`);
321
+ }
322
+
323
+ {
324
+ type Jwks = {
325
+ keys: {
326
+ kid: string;
327
+ kty: string;
328
+ e?: string;
329
+ n?: string;
330
+ use: string;
331
+ }[];
332
+ };
333
+
334
+ const zJwks = z.object({
335
+ keys: z.array(
336
+ z.object({
337
+ kid: z.string(),
338
+ kty: z.string(),
339
+ e: z.string().optional(),
340
+ n: z.string().optional(),
341
+ use: z.string()
342
+ })
343
+ )
344
+ });
345
+
346
+ assert<Equals<Jwks, z.infer<typeof zJwks>>>();
347
+
348
+ try {
349
+ zJwks.parse(jwks);
350
+ } catch {
351
+ throw new Error(`${jwks_uri} does not have the expected shape`);
352
+ }
353
+
354
+ assert(is<Jwks>(jwks));
355
+ }
356
+
357
+ return { jwks };
358
+ })();
359
+
360
+ const publicSigningKeys: PublicSigningKey[] = await Promise.all(
361
+ jwks.keys
362
+ .filter(({ use }) => use === "sig")
363
+ .map(({ kid, kty, e, n }) => {
364
+ if (kty !== "RSA") {
365
+ return undefined;
366
+ }
367
+
368
+ assert(e !== undefined, "e is undefined");
369
+ assert(n !== undefined, "n is undefined");
370
+
371
+ return { kid, e, n };
372
+ })
373
+ .filter(exclude(undefined))
374
+ .map(async ({ kid, e, n }) => {
375
+ const key = await JWK.asKey({ kty: "RSA", e, n });
376
+ const publicKey = key.toPEM(false);
377
+
378
+ return {
379
+ kid,
380
+ publicKey
381
+ };
382
+ })
383
+ );
384
+
385
+ assert(
386
+ publicSigningKeys.length !== 0,
387
+ `No public signing key found at ${jwks_uri}, ${JSON.stringify(jwks, null, 2)}`
388
+ );
389
+
390
+ return publicSigningKeys;
391
+ }
@@ -0,0 +1,26 @@
1
+ import { addOrUpdateSearchParam } from "../tools/urlSearchParams";
2
+
3
+ export type AuthResponse = {
4
+ state: string;
5
+ [key: string]: string | undefined;
6
+ };
7
+
8
+ export function authResponseToUrl(authResponse: AuthResponse): string {
9
+ let authResponseUrl = "https://dummy.com";
10
+
11
+ for (const [name, value] of Object.entries(authResponse)) {
12
+ if (value === undefined) {
13
+ continue;
14
+ }
15
+ authResponseUrl = addOrUpdateSearchParam({
16
+ url: authResponseUrl,
17
+ name,
18
+ value,
19
+ encodeMethod: "www-form"
20
+ });
21
+ }
22
+
23
+ authResponseUrl = `${authResponseUrl}#${authResponseUrl.split("?")[1]}`;
24
+
25
+ return authResponseUrl;
26
+ }
@@ -0,0 +1,140 @@
1
+ import type { OidcInitializationError } from "./OidcInitializationError";
2
+
3
+ export declare type Oidc<
4
+ DecodedIdToken extends Record<string, unknown> = Oidc.Tokens.DecodedIdToken_base
5
+ > = Oidc.LoggedIn<DecodedIdToken> | Oidc.NotLoggedIn;
6
+
7
+ export declare namespace Oidc {
8
+ export type Common = {
9
+ params: {
10
+ issuerUri: string;
11
+ clientId: string;
12
+ };
13
+ };
14
+
15
+ export type NotLoggedIn = Common & {
16
+ isUserLoggedIn: false;
17
+ login: (params: {
18
+ doesCurrentHrefRequiresAuth: boolean;
19
+ /**
20
+ * Add extra query parameters to the url before redirecting to the login pages.
21
+ */
22
+ extraQueryParams?: Record<string, string | undefined>;
23
+ /**
24
+ * Where to redirect after successful login.
25
+ * Default: window.location.href (here)
26
+ *
27
+ * It does not need to include the origin, eg: "/dashboard"
28
+ */
29
+ redirectUrl?: string;
30
+
31
+ /**
32
+ * Transform the url before redirecting to the login pages.
33
+ * Prefer using the extraQueryParams parameter if you're only adding query parameters.
34
+ */
35
+ transformUrlBeforeRedirect?: (url: string) => string;
36
+ }) => Promise<never>;
37
+ initializationError: OidcInitializationError | undefined;
38
+ };
39
+
40
+ export type LoggedIn<DecodedIdToken extends Record<string, unknown> = Record<string, unknown>> =
41
+ Common & {
42
+ isUserLoggedIn: true;
43
+ renewTokens(params?: {
44
+ extraTokenParams?: Record<string, string | undefined>;
45
+ }): Promise<void>;
46
+ getTokens: () => Promise<Tokens<DecodedIdToken>>;
47
+ subscribeToTokensChange: (onTokenChange: (tokens: Tokens<DecodedIdToken>) => void) => {
48
+ unsubscribe: () => void;
49
+ };
50
+ getDecodedIdToken: () => DecodedIdToken;
51
+ logout: (
52
+ params:
53
+ | { redirectTo: "home" | "current page" }
54
+ | { redirectTo: "specific url"; url: string }
55
+ ) => Promise<never>;
56
+ goToAuthServer: (params: {
57
+ extraQueryParams?: Record<string, string | undefined>;
58
+ redirectUrl?: string;
59
+ transformUrlBeforeRedirect?: (url: string) => string;
60
+ }) => Promise<never>;
61
+ subscribeToAutoLogoutCountdown: (
62
+ tickCallback: (params: { secondsLeft: number | undefined }) => void
63
+ ) => { unsubscribeFromAutoLogoutCountdown: () => void };
64
+ /**
65
+ * If you called `goToAuthServer` or `login` with extraQueryParams, this object let you know the outcome of the
66
+ * of the action that was intended.
67
+ *
68
+ * For example, on a Keycloak server, if you called `goToAuthServer({ extraQueryParams: { kc_action: "UPDATE_PASSWORD" } })`
69
+ * you'll get back: `{ extraQueryParams: { kc_action: "UPDATE_PASSWORD" }, result: { kc_action_status: "success" } }` (or "cancelled")
70
+ */
71
+ backFromAuthServer:
72
+ | {
73
+ extraQueryParams: Record<string, string>;
74
+ result: Record<string, string>;
75
+ }
76
+ | undefined;
77
+ /**
78
+ * This is true when the user has just returned from the login pages.
79
+ * This is also true when the user navigate to your app and was able to be silently signed in because there was still a valid session.
80
+ * This false however when the use just reload the page.
81
+ *
82
+ * This can be used to perform some action related to session initialization
83
+ * but avoiding doing it repeatedly every time the user reload the page.
84
+ *
85
+ * Note that this is referring to the browser session and not the OIDC session
86
+ * on the server side.
87
+ *
88
+ * If you want to perform an action only when a new OIDC session is created
89
+ * you can test oidc.isNewBrowserSession && oidc.backFromAuthServer !== undefined
90
+ */
91
+ isNewBrowserSession: boolean;
92
+ };
93
+
94
+ export type Tokens<DecodedIdToken extends Record<string, unknown> = Tokens.DecodedIdToken_base> =
95
+ | Tokens.WithRefreshToken<DecodedIdToken>
96
+ | Tokens.WithoutRefreshToken<DecodedIdToken>;
97
+
98
+ export namespace Tokens {
99
+ export type Common<DecodedIdToken> = {
100
+ accessToken: string;
101
+ accessTokenExpirationTime: number;
102
+ idToken: string;
103
+ decodedIdToken: DecodedIdToken;
104
+ /**
105
+ * decodedIdToken_original = decodeJwt(idToken);
106
+ * decodedIdToken = decodedIdTokenSchema.parse(decodedIdToken_original)
107
+ *
108
+ * The idea here is that if you have provided a zod schema as `decodedIdTokenSchema`
109
+ * it will strip out every claim that you haven't specified.
110
+ * You might even be applying some transformation.
111
+ *
112
+ * `decodedIdToken_original` is the actual decoded payload of the id_token, untransformed.
113
+ * */
114
+ decodedIdToken_original: DecodedIdToken_base;
115
+ /** Read from id_token's JWT, iat claim value, it's a JavaScript timestamp (millisecond epoch) */
116
+ issuedAtTime: number;
117
+ };
118
+
119
+ export type WithRefreshToken<DecodedIdToken> = Common<DecodedIdToken> & {
120
+ hasRefreshToken: true;
121
+ refreshToken: string;
122
+ refreshTokenExpirationTime: number | undefined;
123
+ };
124
+
125
+ export type WithoutRefreshToken<DecodedIdToken> = Common<DecodedIdToken> & {
126
+ hasRefreshToken: false;
127
+ refreshToken?: never;
128
+ refreshTokenExpirationTime?: never;
129
+ };
130
+
131
+ export type DecodedIdToken_base = {
132
+ iss: string;
133
+ sub: string;
134
+ aud: string | string[];
135
+ exp: number;
136
+ iat: number;
137
+ [claimName: string]: unknown;
138
+ };
139
+ }
140
+ }
@@ -0,0 +1,19 @@
1
+ export class OidcInitializationError extends Error {
2
+ public readonly isAuthServerLikelyDown: boolean;
3
+
4
+ constructor(params: { messageOrCause: string | Error; isAuthServerLikelyDown: boolean }) {
5
+ super(
6
+ (() => {
7
+ if (typeof params.messageOrCause === "string") {
8
+ return params.messageOrCause;
9
+ } else {
10
+ return `Unknown initialization error: ${params.messageOrCause.message}`;
11
+ }
12
+ })(),
13
+ // @ts-expect-error
14
+ { cause: typeof params.messageOrCause === "string" ? undefined : params.messageOrCause }
15
+ );
16
+ this.isAuthServerLikelyDown = params.isAuthServerLikelyDown;
17
+ Object.setPrototypeOf(this, new.target.prototype);
18
+ }
19
+ }