@logto/client 2.3.3 → 3.0.0-alpha.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/lib/adapter/defaults.cjs +28 -0
- package/lib/adapter/defaults.d.ts +10 -0
- package/lib/adapter/defaults.js +25 -0
- package/lib/adapter/index.d.ts +3 -3
- package/lib/adapter/types.d.ts +29 -7
- package/lib/client.cjs +383 -0
- package/lib/client.d.ts +153 -0
- package/lib/client.js +381 -0
- package/lib/index.cjs +11 -381
- package/lib/index.d.ts +6 -149
- package/lib/index.js +10 -382
- package/lib/mock.d.ts +0 -1
- package/lib/shim.cjs +66 -0
- package/lib/shim.d.ts +13 -0
- package/lib/shim.js +7 -0
- package/lib/utils/requester.cjs +1 -0
- package/lib/utils/requester.js +1 -0
- package/package.json +14 -6
- package/lib/remote-jwk-set.cjs +0 -62
- package/lib/remote-jwk-set.d.ts +0 -10
- package/lib/remote-jwk-set.js +0 -60
- /package/lib/{remote-jwk-set.test.d.ts → adapter/defaults.test.d.ts} +0 -0
package/lib/index.js
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DefaultJwtVerifier } from './adapter/defaults.js';
|
|
2
|
+
import { StandardLogtoClient } from './client.js';
|
|
2
3
|
export { LogtoError, LogtoRequestError, OidcError, Prompt, ReservedResource, ReservedScope, UserScope, buildOrganizationUrn, getOrganizationIdFromUrn, organizationUrnPrefix } from '@logto/js';
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
import { CachedRemoteJwkSet } from './remote-jwk-set.js';
|
|
7
|
-
import { normalizeLogtoConfig, isLogtoSignInSessionItem, isLogtoAccessTokenMap } from './types/index.js';
|
|
8
|
-
import { buildAccessTokenKey, getDiscoveryEndpoint } from './utils/index.js';
|
|
9
|
-
import { memoize } from './utils/memoize.js';
|
|
10
|
-
import { once } from './utils/once.js';
|
|
11
|
-
import { PersistKey, CacheKey } from './adapter/types.js';
|
|
4
|
+
export { LogtoClientError } from './errors.js';
|
|
5
|
+
import '@silverhand/essentials';
|
|
6
|
+
export { CacheKey, PersistKey } from './adapter/types.js';
|
|
12
7
|
export { createRequester } from './utils/requester.js';
|
|
8
|
+
export { isLogtoAccessTokenMap, isLogtoSignInSessionItem, normalizeLogtoConfig } from './types/index.js';
|
|
13
9
|
|
|
14
|
-
/* eslint-disable max-lines */
|
|
15
10
|
/**
|
|
16
11
|
* The Logto base client class that provides the essential methods for
|
|
17
12
|
* interacting with the Logto server.
|
|
@@ -19,377 +14,10 @@ export { createRequester } from './utils/requester.js';
|
|
|
19
14
|
* It also provides an adapter object that allows the customizations of the
|
|
20
15
|
* client behavior for different environments.
|
|
21
16
|
*/
|
|
22
|
-
class LogtoClient {
|
|
23
|
-
constructor(logtoConfig, adapter) {
|
|
24
|
-
|
|
25
|
-
* Get the OIDC configuration from the discovery endpoint. This method will
|
|
26
|
-
* only fetch the configuration once and cache the result.
|
|
27
|
-
*/
|
|
28
|
-
this.getOidcConfig = once(this.#getOidcConfig);
|
|
29
|
-
/**
|
|
30
|
-
* Get the access token from the storage with refresh strategy.
|
|
31
|
-
*
|
|
32
|
-
* - If the access token has expired, it will try to fetch a new one using the Refresh Token.
|
|
33
|
-
* - If there's an ongoing Promise to fetch the access token, it will return the Promise.
|
|
34
|
-
*
|
|
35
|
-
* If you want to get the access token claims, use {@link getAccessTokenClaims} instead.
|
|
36
|
-
*
|
|
37
|
-
* @param resource The resource that the access token is granted for. If not
|
|
38
|
-
* specified, the access token will be used for OpenID Connect or the default
|
|
39
|
-
* resource, as specified in the Logto Console.
|
|
40
|
-
* @returns The access token string.
|
|
41
|
-
* @throws LogtoClientError if the user is not authenticated.
|
|
42
|
-
*/
|
|
43
|
-
this.getAccessToken = memoize(this.#getAccessToken);
|
|
44
|
-
/**
|
|
45
|
-
* Get the access token for the specified organization from the storage with refresh strategy.
|
|
46
|
-
*
|
|
47
|
-
* Scope {@link UserScope.Organizations} is required in the config to use organization-related
|
|
48
|
-
* methods.
|
|
49
|
-
*
|
|
50
|
-
* @param organizationId The ID of the organization that the access token is granted for.
|
|
51
|
-
* @returns The access token string.
|
|
52
|
-
* @throws LogtoClientError if the user is not authenticated.
|
|
53
|
-
* @remarks
|
|
54
|
-
* It uses the same refresh strategy as {@link getAccessToken}.
|
|
55
|
-
*/
|
|
56
|
-
this.getOrganizationToken = memoize(this.#getOrganizationToken);
|
|
57
|
-
/**
|
|
58
|
-
* Handle the sign-in callback by parsing the authorization code from the
|
|
59
|
-
* callback URI and exchanging it for the tokens.
|
|
60
|
-
*
|
|
61
|
-
* @param callbackUri The callback URI, including the search params, that the user is redirected to after the sign-in flow is completed.
|
|
62
|
-
* The origin and pathname of this URI must match the origin and pathname of the redirect URI specified in {@link signIn}.
|
|
63
|
-
* In many cases you'll probably end up passing `window.location.href` as the argument to this function.
|
|
64
|
-
* @throws LogtoClientError if the sign-in session is not found.
|
|
65
|
-
*/
|
|
66
|
-
this.handleSignInCallback = memoize(this.#handleSignInCallback);
|
|
67
|
-
this.getJwtVerifyGetKey = once(this.#getJwtVerifyGetKey);
|
|
68
|
-
this.accessTokenMap = new Map();
|
|
69
|
-
this.logtoConfig = normalizeLogtoConfig(logtoConfig);
|
|
70
|
-
this.adapter = new ClientAdapterInstance(adapter);
|
|
71
|
-
void this.loadAccessTokenMap();
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Check if the user is authenticated by checking if the ID token exists.
|
|
75
|
-
*/
|
|
76
|
-
async isAuthenticated() {
|
|
77
|
-
return Boolean(await this.getIdToken());
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Get the Refresh Token from the storage.
|
|
81
|
-
*/
|
|
82
|
-
async getRefreshToken() {
|
|
83
|
-
return this.adapter.storage.getItem('refreshToken');
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Get the ID Token from the storage. If you want to get the ID Token claims,
|
|
87
|
-
* use {@link getIdTokenClaims} instead.
|
|
88
|
-
*/
|
|
89
|
-
async getIdToken() {
|
|
90
|
-
return this.adapter.storage.getItem('idToken');
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Get the ID Token claims.
|
|
94
|
-
*/
|
|
95
|
-
async getIdTokenClaims() {
|
|
96
|
-
const idToken = await this.getIdToken();
|
|
97
|
-
if (!idToken) {
|
|
98
|
-
throw new LogtoClientError('not_authenticated');
|
|
99
|
-
}
|
|
100
|
-
return decodeIdToken(idToken);
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Get the access token claims for the specified resource.
|
|
104
|
-
*
|
|
105
|
-
* @param resource The resource that the access token is granted for. If not
|
|
106
|
-
* specified, the access token will be used for OpenID Connect or the default
|
|
107
|
-
* resource, as specified in the Logto Console.
|
|
108
|
-
*/
|
|
109
|
-
async getAccessTokenClaims(resource) {
|
|
110
|
-
const accessToken = await this.getAccessToken(resource);
|
|
111
|
-
return decodeAccessToken(accessToken);
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Get the organization token claims for the specified organization.
|
|
115
|
-
*
|
|
116
|
-
* @param organizationId The ID of the organization that the access token is granted for.
|
|
117
|
-
*/
|
|
118
|
-
async getOrganizationTokenClaims(organizationId) {
|
|
119
|
-
const accessToken = await this.getOrganizationToken(organizationId);
|
|
120
|
-
return decodeAccessToken(accessToken);
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Get the user information from the Userinfo Endpoint.
|
|
124
|
-
*
|
|
125
|
-
* Note the Userinfo Endpoint will return more claims than the ID Token. See
|
|
126
|
-
* {@link https://docs.logto.io/docs/recipes/integrate-logto/vanilla-js/#fetch-user-information | Fetch user information}
|
|
127
|
-
* for more information.
|
|
128
|
-
*
|
|
129
|
-
* @returns The user information.
|
|
130
|
-
* @throws LogtoClientError if the user is not authenticated.
|
|
131
|
-
*/
|
|
132
|
-
async fetchUserInfo() {
|
|
133
|
-
const { userinfoEndpoint } = await this.getOidcConfig();
|
|
134
|
-
const accessToken = await this.getAccessToken();
|
|
135
|
-
if (!accessToken) {
|
|
136
|
-
throw new LogtoClientError('fetch_user_info_failed');
|
|
137
|
-
}
|
|
138
|
-
return fetchUserInfo(userinfoEndpoint, accessToken, this.adapter.requester);
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Start the sign-in flow with the specified redirect URI. The URI must be
|
|
142
|
-
* registered in the Logto Console.
|
|
143
|
-
*
|
|
144
|
-
* The user will be redirected to that URI after the sign-in flow is completed,
|
|
145
|
-
* and the client will be able to get the authorization code from the URI.
|
|
146
|
-
* To fetch the tokens from the authorization code, use {@link handleSignInCallback}
|
|
147
|
-
* after the user is redirected in the callback URI.
|
|
148
|
-
*
|
|
149
|
-
* @param redirectUri The redirect URI that the user will be redirected to after the sign-in flow is completed.
|
|
150
|
-
* @param interactionMode The interaction mode to be used for the authorization request. Note it's not
|
|
151
|
-
* a part of the OIDC standard, but a Logto-specific extension. Defaults to `signIn`.
|
|
152
|
-
*
|
|
153
|
-
* @see {@link https://docs.logto.io/docs/recipes/integrate-logto/vanilla-js/#sign-in | Sign in} for more information.
|
|
154
|
-
* @see {@link InteractionMode}
|
|
155
|
-
*/
|
|
156
|
-
async signIn(redirectUri, interactionMode) {
|
|
157
|
-
const { appId: clientId, prompt, resources, scopes } = this.logtoConfig;
|
|
158
|
-
const { authorizationEndpoint } = await this.getOidcConfig();
|
|
159
|
-
const codeVerifier = this.adapter.generateCodeVerifier();
|
|
160
|
-
const codeChallenge = await this.adapter.generateCodeChallenge(codeVerifier);
|
|
161
|
-
const state = this.adapter.generateState();
|
|
162
|
-
const signInUri = generateSignInUri({
|
|
163
|
-
authorizationEndpoint,
|
|
164
|
-
clientId,
|
|
165
|
-
redirectUri,
|
|
166
|
-
codeChallenge,
|
|
167
|
-
state,
|
|
168
|
-
scopes,
|
|
169
|
-
resources,
|
|
170
|
-
prompt,
|
|
171
|
-
interactionMode,
|
|
172
|
-
});
|
|
173
|
-
await Promise.all([
|
|
174
|
-
this.setSignInSession({ redirectUri, codeVerifier, state }),
|
|
175
|
-
this.setRefreshToken(null),
|
|
176
|
-
this.setIdToken(null),
|
|
177
|
-
]);
|
|
178
|
-
await this.adapter.navigate(signInUri);
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Check if the user is redirected from the sign-in page by checking if the
|
|
182
|
-
* current URL matches the redirect URI in the sign-in session.
|
|
183
|
-
*
|
|
184
|
-
* If there's no sign-in session, it will return `false`.
|
|
185
|
-
*
|
|
186
|
-
* @param url The current URL.
|
|
187
|
-
*/
|
|
188
|
-
async isSignInRedirected(url) {
|
|
189
|
-
const signInSession = await this.getSignInSession();
|
|
190
|
-
if (!signInSession) {
|
|
191
|
-
return false;
|
|
192
|
-
}
|
|
193
|
-
const { redirectUri } = signInSession;
|
|
194
|
-
const { origin, pathname } = new URL(url);
|
|
195
|
-
return `${origin}${pathname}` === redirectUri;
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Start the sign-out flow with the specified redirect URI. The URI must be
|
|
199
|
-
* registered in the Logto Console.
|
|
200
|
-
*
|
|
201
|
-
* It will also revoke all the tokens and clean up the storage.
|
|
202
|
-
*
|
|
203
|
-
* The user will be redirected that URI after the sign-out flow is completed.
|
|
204
|
-
* If the `postLogoutRedirectUri` is not specified, the user will be redirected
|
|
205
|
-
* to a default page.
|
|
206
|
-
*/
|
|
207
|
-
async signOut(postLogoutRedirectUri) {
|
|
208
|
-
const { appId: clientId } = this.logtoConfig;
|
|
209
|
-
const { endSessionEndpoint, revocationEndpoint } = await this.getOidcConfig();
|
|
210
|
-
const refreshToken = await this.getRefreshToken();
|
|
211
|
-
if (refreshToken) {
|
|
212
|
-
try {
|
|
213
|
-
await revoke(revocationEndpoint, clientId, refreshToken, this.adapter.requester);
|
|
214
|
-
}
|
|
215
|
-
catch {
|
|
216
|
-
// Do nothing at this point, as we don't want to break the sign-out flow even if the revocation is failed
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
const url = generateSignOutUri({
|
|
220
|
-
endSessionEndpoint,
|
|
221
|
-
postLogoutRedirectUri,
|
|
222
|
-
clientId,
|
|
223
|
-
});
|
|
224
|
-
this.accessTokenMap.clear();
|
|
225
|
-
await Promise.all([
|
|
226
|
-
this.setRefreshToken(null),
|
|
227
|
-
this.setIdToken(null),
|
|
228
|
-
this.adapter.storage.removeItem('accessToken'),
|
|
229
|
-
]);
|
|
230
|
-
await this.adapter.navigate(url);
|
|
231
|
-
}
|
|
232
|
-
async getSignInSession() {
|
|
233
|
-
const jsonItem = await this.adapter.storage.getItem('signInSession');
|
|
234
|
-
if (!jsonItem) {
|
|
235
|
-
return null;
|
|
236
|
-
}
|
|
237
|
-
const item = JSON.parse(jsonItem);
|
|
238
|
-
if (!isLogtoSignInSessionItem(item)) {
|
|
239
|
-
throw new LogtoClientError('sign_in_session.invalid');
|
|
240
|
-
}
|
|
241
|
-
return item;
|
|
242
|
-
}
|
|
243
|
-
async setSignInSession(value) {
|
|
244
|
-
return this.adapter.setStorageItem(PersistKey.SignInSession, value && JSON.stringify(value));
|
|
245
|
-
}
|
|
246
|
-
async setIdToken(value) {
|
|
247
|
-
return this.adapter.setStorageItem(PersistKey.IdToken, value);
|
|
248
|
-
}
|
|
249
|
-
async setRefreshToken(value) {
|
|
250
|
-
return this.adapter.setStorageItem(PersistKey.RefreshToken, value);
|
|
251
|
-
}
|
|
252
|
-
async getAccessTokenByRefreshToken(resource, organizationId) {
|
|
253
|
-
const currentRefreshToken = await this.getRefreshToken();
|
|
254
|
-
if (!currentRefreshToken) {
|
|
255
|
-
throw new LogtoClientError('not_authenticated');
|
|
256
|
-
}
|
|
257
|
-
const accessTokenKey = buildAccessTokenKey(resource, organizationId);
|
|
258
|
-
const { appId: clientId } = this.logtoConfig;
|
|
259
|
-
const { tokenEndpoint } = await this.getOidcConfig();
|
|
260
|
-
const requestedAt = Math.round(Date.now() / 1000);
|
|
261
|
-
const { accessToken, refreshToken, idToken, scope, expiresIn } = await fetchTokenByRefreshToken({
|
|
262
|
-
clientId,
|
|
263
|
-
tokenEndpoint,
|
|
264
|
-
refreshToken: currentRefreshToken,
|
|
265
|
-
resource,
|
|
266
|
-
organizationId,
|
|
267
|
-
}, this.adapter.requester);
|
|
268
|
-
this.accessTokenMap.set(accessTokenKey, {
|
|
269
|
-
token: accessToken,
|
|
270
|
-
scope,
|
|
271
|
-
/** The `expiresAt` variable provides an approximate estimation of the actual `exp` property
|
|
272
|
-
* in the token claims. It is utilized by the client to determine if the cached access token
|
|
273
|
-
* has expired and when a new access token should be requested.
|
|
274
|
-
*/
|
|
275
|
-
expiresAt: requestedAt + expiresIn,
|
|
276
|
-
});
|
|
277
|
-
await this.saveAccessTokenMap();
|
|
278
|
-
if (refreshToken) {
|
|
279
|
-
await this.setRefreshToken(refreshToken);
|
|
280
|
-
}
|
|
281
|
-
if (idToken) {
|
|
282
|
-
await this.verifyIdToken(idToken);
|
|
283
|
-
await this.setIdToken(idToken);
|
|
284
|
-
}
|
|
285
|
-
return accessToken;
|
|
286
|
-
}
|
|
287
|
-
async verifyIdToken(idToken) {
|
|
288
|
-
const { appId } = this.logtoConfig;
|
|
289
|
-
const { issuer } = await this.getOidcConfig();
|
|
290
|
-
const jwtVerifyGetKey = await this.getJwtVerifyGetKey();
|
|
291
|
-
await verifyIdToken(idToken, appId, issuer, jwtVerifyGetKey);
|
|
292
|
-
}
|
|
293
|
-
async saveAccessTokenMap() {
|
|
294
|
-
const data = {};
|
|
295
|
-
for (const [key, accessToken] of this.accessTokenMap.entries()) {
|
|
296
|
-
// eslint-disable-next-line @silverhand/fp/no-mutation
|
|
297
|
-
data[key] = accessToken;
|
|
298
|
-
}
|
|
299
|
-
await this.adapter.storage.setItem('accessToken', JSON.stringify(data));
|
|
300
|
-
}
|
|
301
|
-
async loadAccessTokenMap() {
|
|
302
|
-
const raw = await this.adapter.storage.getItem('accessToken');
|
|
303
|
-
if (!raw) {
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
try {
|
|
307
|
-
const json = JSON.parse(raw);
|
|
308
|
-
if (!isLogtoAccessTokenMap(json)) {
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
this.accessTokenMap.clear();
|
|
312
|
-
for (const [key, accessToken] of Object.entries(json)) {
|
|
313
|
-
this.accessTokenMap.set(key, accessToken);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
catch (error) {
|
|
317
|
-
console.warn(error);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
async #getOidcConfig() {
|
|
321
|
-
return this.adapter.getWithCache(CacheKey.OpenidConfig, async () => {
|
|
322
|
-
return fetchOidcConfig(getDiscoveryEndpoint(this.logtoConfig.endpoint), this.adapter.requester);
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
async #getJwtVerifyGetKey() {
|
|
326
|
-
const { jwksUri } = await this.getOidcConfig();
|
|
327
|
-
if (!this.adapter.unstable_cache) {
|
|
328
|
-
return createRemoteJWKSet(new URL(jwksUri));
|
|
329
|
-
}
|
|
330
|
-
const cachedJwkSet = new CachedRemoteJwkSet(new URL(jwksUri), this.adapter);
|
|
331
|
-
return async (...args) => cachedJwkSet.getKey(...args);
|
|
332
|
-
}
|
|
333
|
-
async #getAccessToken(resource, organizationId) {
|
|
334
|
-
if (!(await this.isAuthenticated())) {
|
|
335
|
-
throw new LogtoClientError('not_authenticated');
|
|
336
|
-
}
|
|
337
|
-
const accessTokenKey = buildAccessTokenKey(resource, organizationId);
|
|
338
|
-
const accessToken = this.accessTokenMap.get(accessTokenKey);
|
|
339
|
-
if (accessToken && accessToken.expiresAt > Date.now() / 1000) {
|
|
340
|
-
return accessToken.token;
|
|
341
|
-
}
|
|
342
|
-
// Since the access token has expired, delete it from the map.
|
|
343
|
-
if (accessToken) {
|
|
344
|
-
this.accessTokenMap.delete(accessTokenKey);
|
|
345
|
-
}
|
|
346
|
-
/**
|
|
347
|
-
* Need to fetch a new access token using refresh token.
|
|
348
|
-
*/
|
|
349
|
-
return this.getAccessTokenByRefreshToken(resource, organizationId);
|
|
350
|
-
}
|
|
351
|
-
async #getOrganizationToken(organizationId) {
|
|
352
|
-
if (!this.logtoConfig.scopes?.includes(UserScope.Organizations)) {
|
|
353
|
-
throw new LogtoClientError('missing_scope_organizations');
|
|
354
|
-
}
|
|
355
|
-
return this.getAccessToken(undefined, organizationId);
|
|
356
|
-
}
|
|
357
|
-
async #handleSignInCallback(callbackUri) {
|
|
358
|
-
const { requester } = this.adapter;
|
|
359
|
-
const signInSession = await this.getSignInSession();
|
|
360
|
-
if (!signInSession) {
|
|
361
|
-
throw new LogtoClientError('sign_in_session.not_found');
|
|
362
|
-
}
|
|
363
|
-
const { redirectUri, state, codeVerifier } = signInSession;
|
|
364
|
-
const code = verifyAndParseCodeFromCallbackUri(callbackUri, redirectUri, state);
|
|
365
|
-
// NOTE: Will add scope to accessTokenKey when needed. (Linear issue LOG-1589)
|
|
366
|
-
const accessTokenKey = buildAccessTokenKey();
|
|
367
|
-
const { appId: clientId } = this.logtoConfig;
|
|
368
|
-
const { tokenEndpoint } = await this.getOidcConfig();
|
|
369
|
-
const requestedAt = Math.round(Date.now() / 1000);
|
|
370
|
-
const { idToken, refreshToken, accessToken, scope, expiresIn } = await fetchTokenByAuthorizationCode({
|
|
371
|
-
clientId,
|
|
372
|
-
tokenEndpoint,
|
|
373
|
-
redirectUri,
|
|
374
|
-
codeVerifier,
|
|
375
|
-
code,
|
|
376
|
-
}, requester);
|
|
377
|
-
await this.verifyIdToken(idToken);
|
|
378
|
-
await this.setRefreshToken(refreshToken ?? null);
|
|
379
|
-
await this.setIdToken(idToken);
|
|
380
|
-
this.accessTokenMap.set(accessTokenKey, {
|
|
381
|
-
token: accessToken,
|
|
382
|
-
scope,
|
|
383
|
-
/** The `expiresAt` variable provides an approximate estimation of the actual `exp` property
|
|
384
|
-
* in the token claims. It is utilized by the client to determine if the cached access token
|
|
385
|
-
* has expired and when a new access token should be requested.
|
|
386
|
-
*/
|
|
387
|
-
expiresAt: requestedAt + expiresIn,
|
|
388
|
-
});
|
|
389
|
-
await this.saveAccessTokenMap();
|
|
390
|
-
await this.setSignInSession(null);
|
|
17
|
+
class LogtoClient extends StandardLogtoClient {
|
|
18
|
+
constructor(logtoConfig, adapter, buildJwtVerifier) {
|
|
19
|
+
super(logtoConfig, adapter, buildJwtVerifier ?? ((client) => new DefaultJwtVerifier(client)));
|
|
391
20
|
}
|
|
392
21
|
}
|
|
393
|
-
/* eslint-enable max-lines */
|
|
394
22
|
|
|
395
|
-
export {
|
|
23
|
+
export { StandardLogtoClient, LogtoClient as default };
|
package/lib/mock.d.ts
CHANGED
|
@@ -72,7 +72,6 @@ export declare const createClient: (prompt?: Prompt, storage?: MockedStorage, wi
|
|
|
72
72
|
*/
|
|
73
73
|
export declare class LogtoClientWithAccessors extends LogtoClient {
|
|
74
74
|
runGetOidcConfig(): Promise<OidcConfigResponse>;
|
|
75
|
-
runGetJwtVerifyGetKey(): Promise<import("jose").JWTVerifyGetKey>;
|
|
76
75
|
getLogtoConfig(): Nullable<LogtoConfig>;
|
|
77
76
|
getSignInSession(): Promise<Nullable<LogtoSignInSessionItem>>;
|
|
78
77
|
setSignInSessionItem(item: Nullable<LogtoSignInSessionItem>): Promise<void>;
|
package/lib/shim.cjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var js = require('@logto/js');
|
|
4
|
+
var errors = require('./errors.cjs');
|
|
5
|
+
require('@silverhand/essentials');
|
|
6
|
+
var types = require('./adapter/types.cjs');
|
|
7
|
+
var requester = require('./utils/requester.cjs');
|
|
8
|
+
var index = require('./types/index.cjs');
|
|
9
|
+
var client = require('./client.cjs');
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
Object.defineProperty(exports, "LogtoError", {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
get: function () { return js.LogtoError; }
|
|
16
|
+
});
|
|
17
|
+
Object.defineProperty(exports, "LogtoRequestError", {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
get: function () { return js.LogtoRequestError; }
|
|
20
|
+
});
|
|
21
|
+
Object.defineProperty(exports, "OidcError", {
|
|
22
|
+
enumerable: true,
|
|
23
|
+
get: function () { return js.OidcError; }
|
|
24
|
+
});
|
|
25
|
+
Object.defineProperty(exports, "Prompt", {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
get: function () { return js.Prompt; }
|
|
28
|
+
});
|
|
29
|
+
Object.defineProperty(exports, "ReservedResource", {
|
|
30
|
+
enumerable: true,
|
|
31
|
+
get: function () { return js.ReservedResource; }
|
|
32
|
+
});
|
|
33
|
+
Object.defineProperty(exports, "ReservedScope", {
|
|
34
|
+
enumerable: true,
|
|
35
|
+
get: function () { return js.ReservedScope; }
|
|
36
|
+
});
|
|
37
|
+
Object.defineProperty(exports, "UserScope", {
|
|
38
|
+
enumerable: true,
|
|
39
|
+
get: function () { return js.UserScope; }
|
|
40
|
+
});
|
|
41
|
+
Object.defineProperty(exports, "buildOrganizationUrn", {
|
|
42
|
+
enumerable: true,
|
|
43
|
+
get: function () { return js.buildOrganizationUrn; }
|
|
44
|
+
});
|
|
45
|
+
Object.defineProperty(exports, "getOrganizationIdFromUrn", {
|
|
46
|
+
enumerable: true,
|
|
47
|
+
get: function () { return js.getOrganizationIdFromUrn; }
|
|
48
|
+
});
|
|
49
|
+
Object.defineProperty(exports, "organizationUrnPrefix", {
|
|
50
|
+
enumerable: true,
|
|
51
|
+
get: function () { return js.organizationUrnPrefix; }
|
|
52
|
+
});
|
|
53
|
+
exports.LogtoClientError = errors.LogtoClientError;
|
|
54
|
+
Object.defineProperty(exports, "CacheKey", {
|
|
55
|
+
enumerable: true,
|
|
56
|
+
get: function () { return types.CacheKey; }
|
|
57
|
+
});
|
|
58
|
+
Object.defineProperty(exports, "PersistKey", {
|
|
59
|
+
enumerable: true,
|
|
60
|
+
get: function () { return types.PersistKey; }
|
|
61
|
+
});
|
|
62
|
+
exports.createRequester = requester.createRequester;
|
|
63
|
+
exports.isLogtoAccessTokenMap = index.isLogtoAccessTokenMap;
|
|
64
|
+
exports.isLogtoSignInSessionItem = index.isLogtoSignInSessionItem;
|
|
65
|
+
exports.normalizeLogtoConfig = index.normalizeLogtoConfig;
|
|
66
|
+
exports.StandardLogtoClient = client.StandardLogtoClient;
|
package/lib/shim.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Exports all necessary stuff for the package except the client class with default
|
|
3
|
+
* JWT verifier. It can avoid the use of `jose` package which is useful for certain environments
|
|
4
|
+
* that don't support native modules like `crypto`. (e.g. React Native)
|
|
5
|
+
*/
|
|
6
|
+
export type { IdTokenClaims, LogtoErrorCode, UserInfoResponse, InteractionMode } from '@logto/js';
|
|
7
|
+
export { LogtoError, LogtoRequestError, OidcError, Prompt, ReservedScope, ReservedResource, UserScope, organizationUrnPrefix, buildOrganizationUrn, getOrganizationIdFromUrn, } from '@logto/js';
|
|
8
|
+
export * from './errors.js';
|
|
9
|
+
export type { Storage, StorageKey, ClientAdapter } from './adapter/index.js';
|
|
10
|
+
export { PersistKey, CacheKey } from './adapter/index.js';
|
|
11
|
+
export { createRequester } from './utils/index.js';
|
|
12
|
+
export * from './types/index.js';
|
|
13
|
+
export * from './client.js';
|
package/lib/shim.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { LogtoError, LogtoRequestError, OidcError, Prompt, ReservedResource, ReservedScope, UserScope, buildOrganizationUrn, getOrganizationIdFromUrn, organizationUrnPrefix } from '@logto/js';
|
|
2
|
+
export { LogtoClientError } from './errors.js';
|
|
3
|
+
import '@silverhand/essentials';
|
|
4
|
+
export { CacheKey, PersistKey } from './adapter/types.js';
|
|
5
|
+
export { createRequester } from './utils/requester.js';
|
|
6
|
+
export { isLogtoAccessTokenMap, isLogtoSignInSessionItem, normalizeLogtoConfig } from './types/index.js';
|
|
7
|
+
export { StandardLogtoClient } from './client.js';
|
package/lib/utils/requester.cjs
CHANGED
|
@@ -14,6 +14,7 @@ const createRequester = (fetchFunction) => {
|
|
|
14
14
|
const response = await fetchFunction(...args);
|
|
15
15
|
if (!response.ok) {
|
|
16
16
|
const responseJson = await response.json();
|
|
17
|
+
console.error(`Logto requester error: [status=${response.status}]`, responseJson);
|
|
17
18
|
if (!js.isLogtoRequestError(responseJson)) {
|
|
18
19
|
throw new js.LogtoError('unexpected_response_error', responseJson);
|
|
19
20
|
}
|
package/lib/utils/requester.js
CHANGED
|
@@ -12,6 +12,7 @@ const createRequester = (fetchFunction) => {
|
|
|
12
12
|
const response = await fetchFunction(...args);
|
|
13
13
|
if (!response.ok) {
|
|
14
14
|
const responseJson = await response.json();
|
|
15
|
+
console.error(`Logto requester error: [status=${response.status}]`, responseJson);
|
|
15
16
|
if (!isLogtoRequestError(responseJson)) {
|
|
16
17
|
throw new LogtoError('unexpected_response_error', responseJson);
|
|
17
18
|
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logto/client",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0-alpha.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./lib/index.cjs",
|
|
6
6
|
"module": "./lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"require": "./lib/index.cjs",
|
|
12
|
+
"import": "./lib/index.js",
|
|
13
|
+
"default": "./lib/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./shim": {
|
|
16
|
+
"types": "./lib/shim.d.ts",
|
|
17
|
+
"require": "./lib/shim.cjs",
|
|
18
|
+
"import": "./lib/shim.js",
|
|
19
|
+
"default": "./lib/shim.js"
|
|
20
|
+
}
|
|
13
21
|
},
|
|
14
22
|
"files": [
|
|
15
23
|
"lib"
|
|
@@ -21,7 +29,7 @@
|
|
|
21
29
|
"directory": "packages/client"
|
|
22
30
|
},
|
|
23
31
|
"dependencies": {
|
|
24
|
-
"@logto/js": "^
|
|
32
|
+
"@logto/js": "^4.0.0-alpha.0",
|
|
25
33
|
"@silverhand/essentials": "^2.8.7",
|
|
26
34
|
"camelcase-keys": "^7.0.1",
|
|
27
35
|
"jose": "^5.0.0"
|
package/lib/remote-jwk-set.cjs
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var essentials = require('@silverhand/essentials');
|
|
4
|
-
var jose = require('jose');
|
|
5
|
-
var types = require('./adapter/types.cjs');
|
|
6
|
-
|
|
7
|
-
// Edited from jose's internal util `isJWKSLike`
|
|
8
|
-
function isJwkSetLike(jwkSet) {
|
|
9
|
-
return Boolean(jwkSet &&
|
|
10
|
-
typeof jwkSet === 'object' &&
|
|
11
|
-
'keys' in jwkSet &&
|
|
12
|
-
Array.isArray(jwkSet.keys) &&
|
|
13
|
-
jwkSet.keys.every((element) => essentials.isObject(element)));
|
|
14
|
-
}
|
|
15
|
-
class CachedRemoteJwkSet {
|
|
16
|
-
constructor(url, adapter) {
|
|
17
|
-
this.url = url;
|
|
18
|
-
this.adapter = adapter;
|
|
19
|
-
if (!adapter.unstable_cache) {
|
|
20
|
-
throw new Error("No cache found in the client adapter. Use `createRemoteJWKSet()` from 'jose' instead.");
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
async getKey(...args) {
|
|
24
|
-
if (!this.jwkSet) {
|
|
25
|
-
this.jwkSet = await this.#load();
|
|
26
|
-
}
|
|
27
|
-
try {
|
|
28
|
-
return await this.#getLocalKey(...args);
|
|
29
|
-
}
|
|
30
|
-
catch (error) {
|
|
31
|
-
// Jose does not export the error definition
|
|
32
|
-
// Found in https://github.com/panva/jose/blob/d5b3cb672736112b1e1e31ac4d5e9cd641675206/src/util/errors.ts#L347
|
|
33
|
-
if (error instanceof Error && 'code' in error && error.code === 'ERR_JWKS_NO_MATCHING_KEY') {
|
|
34
|
-
this.jwkSet = await this.#load();
|
|
35
|
-
return this.#getLocalKey(...args);
|
|
36
|
-
}
|
|
37
|
-
throw error;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
async #load() {
|
|
41
|
-
return this.adapter.getWithCache(types.CacheKey.Jwks, async () => {
|
|
42
|
-
const controller = new AbortController();
|
|
43
|
-
const response = await fetch(this.url, { signal: controller.signal, redirect: 'manual' });
|
|
44
|
-
if (!response.ok) {
|
|
45
|
-
throw new Error('Expected OK from the JSON Web Key Set HTTP response');
|
|
46
|
-
}
|
|
47
|
-
const json = await response.json();
|
|
48
|
-
if (!isJwkSetLike(json)) {
|
|
49
|
-
throw new Error('JSON Web Key Set malformed');
|
|
50
|
-
}
|
|
51
|
-
return json;
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
async #getLocalKey(...args) {
|
|
55
|
-
if (!this.jwkSet) {
|
|
56
|
-
throw new Error('No local JWK Set found.');
|
|
57
|
-
}
|
|
58
|
-
return jose.createLocalJWKSet(this.jwkSet)(...args);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
exports.CachedRemoteJwkSet = CachedRemoteJwkSet;
|
package/lib/remote-jwk-set.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { type JSONWebKeySet, type JWTVerifyGetKey } from 'jose';
|
|
2
|
-
import { type ClientAdapterInstance } from './adapter/index.js';
|
|
3
|
-
export declare class CachedRemoteJwkSet {
|
|
4
|
-
#private;
|
|
5
|
-
readonly url: URL;
|
|
6
|
-
private readonly adapter;
|
|
7
|
-
protected jwkSet?: JSONWebKeySet;
|
|
8
|
-
constructor(url: URL, adapter: ClientAdapterInstance);
|
|
9
|
-
getKey(...args: Parameters<JWTVerifyGetKey>): Promise<import("jose").KeyLike>;
|
|
10
|
-
}
|