@logto/client 2.2.3 → 2.3.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/errors.cjs +3 -0
- package/lib/errors.d.ts +1 -0
- package/lib/errors.js +3 -0
- package/lib/index.cjs +87 -40
- package/lib/index.d.ts +40 -19
- package/lib/index.js +74 -44
- package/lib/mock.d.ts +1 -1
- package/lib/types/index.cjs +22 -0
- package/lib/types/index.d.ts +11 -1
- package/lib/types/index.js +23 -2
- package/lib/types/index.test.d.ts +1 -0
- package/lib/utils/index.cjs +2 -1
- package/lib/utils/index.d.ts +1 -1
- package/lib/utils/index.js +2 -1
- package/package.json +4 -4
package/lib/errors.cjs
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var js = require('@logto/js');
|
|
4
|
+
|
|
3
5
|
const logtoClientErrorCodes = Object.freeze({
|
|
4
6
|
'sign_in_session.invalid': 'Invalid sign-in session.',
|
|
5
7
|
'sign_in_session.not_found': 'Sign-in session not found.',
|
|
6
8
|
not_authenticated: 'Not authenticated.',
|
|
7
9
|
fetch_user_info_failed: 'Unable to fetch user info. The access token may be invalid.',
|
|
8
10
|
user_cancelled: 'The user cancelled the action.',
|
|
11
|
+
missing_scope_organizations: `The \`${js.UserScope.Organizations}\` scope is required`,
|
|
9
12
|
});
|
|
10
13
|
class LogtoClientError extends Error {
|
|
11
14
|
constructor(code, data) {
|
package/lib/errors.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ declare const logtoClientErrorCodes: Readonly<{
|
|
|
4
4
|
not_authenticated: "Not authenticated.";
|
|
5
5
|
fetch_user_info_failed: "Unable to fetch user info. The access token may be invalid.";
|
|
6
6
|
user_cancelled: "The user cancelled the action.";
|
|
7
|
+
missing_scope_organizations: "The `urn:logto:scope:organizations` scope is required";
|
|
7
8
|
}>;
|
|
8
9
|
export type LogtoClientErrorCode = keyof typeof logtoClientErrorCodes;
|
|
9
10
|
export declare class LogtoClientError extends Error {
|
package/lib/errors.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { UserScope } from '@logto/js';
|
|
2
|
+
|
|
1
3
|
const logtoClientErrorCodes = Object.freeze({
|
|
2
4
|
'sign_in_session.invalid': 'Invalid sign-in session.',
|
|
3
5
|
'sign_in_session.not_found': 'Sign-in session not found.',
|
|
4
6
|
not_authenticated: 'Not authenticated.',
|
|
5
7
|
fetch_user_info_failed: 'Unable to fetch user info. The access token may be invalid.',
|
|
6
8
|
user_cancelled: 'The user cancelled the action.',
|
|
9
|
+
missing_scope_organizations: `The \`${UserScope.Organizations}\` scope is required`,
|
|
7
10
|
});
|
|
8
11
|
class LogtoClientError extends Error {
|
|
9
12
|
constructor(code, data) {
|
package/lib/index.cjs
CHANGED
|
@@ -14,6 +14,7 @@ var once = require('./utils/once.cjs');
|
|
|
14
14
|
var types = require('./adapter/types.cjs');
|
|
15
15
|
var requester = require('./utils/requester.cjs');
|
|
16
16
|
|
|
17
|
+
/* eslint-disable max-lines */
|
|
17
18
|
/**
|
|
18
19
|
* The Logto base client class that provides the essential methods for
|
|
19
20
|
* interacting with the Logto server.
|
|
@@ -24,13 +25,37 @@ var requester = require('./utils/requester.cjs');
|
|
|
24
25
|
class LogtoClient {
|
|
25
26
|
constructor(logtoConfig, adapter) {
|
|
26
27
|
this.getOidcConfig = memoize.memoize(this.#getOidcConfig);
|
|
28
|
+
/**
|
|
29
|
+
* Get the access token from the storage with refresh strategy.
|
|
30
|
+
*
|
|
31
|
+
* - If the access token has expired, it will try to fetch a new one using the Refresh Token.
|
|
32
|
+
* - If there's an ongoing Promise to fetch the access token, it will return the Promise.
|
|
33
|
+
*
|
|
34
|
+
* If you want to get the access token claims, use {@link getAccessTokenClaims} instead.
|
|
35
|
+
*
|
|
36
|
+
* @param resource The resource that the access token is granted for. If not
|
|
37
|
+
* specified, the access token will be used for OpenID Connect or the default
|
|
38
|
+
* resource, as specified in the Logto Console.
|
|
39
|
+
* @returns The access token string.
|
|
40
|
+
* @throws LogtoClientError if the user is not authenticated.
|
|
41
|
+
*/
|
|
42
|
+
this.getAccessToken = memoize.memoize(this.#getAccessToken);
|
|
43
|
+
/**
|
|
44
|
+
* Get the access token for the specified organization from the storage with refresh strategy.
|
|
45
|
+
*
|
|
46
|
+
* Scope {@link UserScope.Organizations} is required in the config to use organization-related
|
|
47
|
+
* methods.
|
|
48
|
+
*
|
|
49
|
+
* @param organizationId The ID of the organization that the access token is granted for.
|
|
50
|
+
* @returns The access token string.
|
|
51
|
+
* @throws LogtoClientError if the user is not authenticated.
|
|
52
|
+
* @remarks
|
|
53
|
+
* It uses the same refresh strategy as {@link getAccessToken}.
|
|
54
|
+
*/
|
|
55
|
+
this.getOrganizationToken = memoize.memoize(this.#getOrganizationToken);
|
|
27
56
|
this.getJwtVerifyGetKey = once.once(this.#getJwtVerifyGetKey);
|
|
28
57
|
this.accessTokenMap = new Map();
|
|
29
|
-
this.logtoConfig =
|
|
30
|
-
...logtoConfig,
|
|
31
|
-
prompt: logtoConfig.prompt ?? js.Prompt.Consent,
|
|
32
|
-
scopes: js.withDefaultScopes(logtoConfig.scopes).split(' '),
|
|
33
|
-
};
|
|
58
|
+
this.logtoConfig = index.normalizeLogtoConfig(logtoConfig);
|
|
34
59
|
this.adapter = new index$1.ClientAdapterInstance(adapter);
|
|
35
60
|
void this.loadAccessTokenMap();
|
|
36
61
|
}
|
|
@@ -53,36 +78,6 @@ class LogtoClient {
|
|
|
53
78
|
async getIdToken() {
|
|
54
79
|
return this.adapter.storage.getItem('idToken');
|
|
55
80
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Get the Access Token from the storage. If the Access Token has expired, it
|
|
58
|
-
* will try to fetch a new one using the Refresh Token.
|
|
59
|
-
*
|
|
60
|
-
* If you want to get the Access Token claims, use {@link getAccessTokenClaims} instead.
|
|
61
|
-
*
|
|
62
|
-
* @param resource The resource that the Access Token is granted for. If not
|
|
63
|
-
* specified, the Access Token will be used for OpenID Connect or the default
|
|
64
|
-
* resource, as specified in the Logto Console.
|
|
65
|
-
* @returns The Access Token string.
|
|
66
|
-
* @throws LogtoClientError if the user is not authenticated.
|
|
67
|
-
*/
|
|
68
|
-
async getAccessToken(resource) {
|
|
69
|
-
if (!(await this.getIdToken())) {
|
|
70
|
-
throw new errors.LogtoClientError('not_authenticated');
|
|
71
|
-
}
|
|
72
|
-
const accessTokenKey = index$2.buildAccessTokenKey(resource);
|
|
73
|
-
const accessToken = this.accessTokenMap.get(accessTokenKey);
|
|
74
|
-
if (accessToken && accessToken.expiresAt > Date.now() / 1000) {
|
|
75
|
-
return accessToken.token;
|
|
76
|
-
}
|
|
77
|
-
// Since the access token has expired, delete it from the map.
|
|
78
|
-
if (accessToken) {
|
|
79
|
-
this.accessTokenMap.delete(accessTokenKey);
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Need to fetch a new access token using refresh token.
|
|
83
|
-
*/
|
|
84
|
-
return this.getAccessTokenByRefreshToken(resource);
|
|
85
|
-
}
|
|
86
81
|
/**
|
|
87
82
|
* Get the ID Token claims.
|
|
88
83
|
*/
|
|
@@ -94,16 +89,25 @@ class LogtoClient {
|
|
|
94
89
|
return js.decodeIdToken(idToken);
|
|
95
90
|
}
|
|
96
91
|
/**
|
|
97
|
-
* Get the
|
|
92
|
+
* Get the access token claims for the specified resource.
|
|
98
93
|
*
|
|
99
|
-
* @param resource The resource that the
|
|
100
|
-
* specified, the
|
|
94
|
+
* @param resource The resource that the access token is granted for. If not
|
|
95
|
+
* specified, the access token will be used for OpenID Connect or the default
|
|
101
96
|
* resource, as specified in the Logto Console.
|
|
102
97
|
*/
|
|
103
98
|
async getAccessTokenClaims(resource) {
|
|
104
99
|
const accessToken = await this.getAccessToken(resource);
|
|
105
100
|
return js.decodeAccessToken(accessToken);
|
|
106
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Get the organization token claims for the specified organization.
|
|
104
|
+
*
|
|
105
|
+
* @param organizationId The ID of the organization that the access token is granted for.
|
|
106
|
+
*/
|
|
107
|
+
async getOrganizationTokenClaims(organizationId) {
|
|
108
|
+
const accessToken = await this.getOrganizationToken(organizationId);
|
|
109
|
+
return js.decodeAccessToken(accessToken);
|
|
110
|
+
}
|
|
107
111
|
/**
|
|
108
112
|
* Get the user information from the Userinfo Endpoint.
|
|
109
113
|
*
|
|
@@ -278,12 +282,12 @@ class LogtoClient {
|
|
|
278
282
|
async setRefreshToken(value) {
|
|
279
283
|
return this.adapter.setStorageItem(types.PersistKey.RefreshToken, value);
|
|
280
284
|
}
|
|
281
|
-
async getAccessTokenByRefreshToken(resource) {
|
|
285
|
+
async getAccessTokenByRefreshToken(resource, organizationId) {
|
|
282
286
|
const currentRefreshToken = await this.getRefreshToken();
|
|
283
287
|
if (!currentRefreshToken) {
|
|
284
288
|
throw new errors.LogtoClientError('not_authenticated');
|
|
285
289
|
}
|
|
286
|
-
const accessTokenKey = index$2.buildAccessTokenKey(resource);
|
|
290
|
+
const accessTokenKey = index$2.buildAccessTokenKey(resource, organizationId);
|
|
287
291
|
const { appId: clientId } = this.logtoConfig;
|
|
288
292
|
const { tokenEndpoint } = await this.getOidcConfig();
|
|
289
293
|
const requestedAt = Math.round(Date.now() / 1000);
|
|
@@ -292,6 +296,7 @@ class LogtoClient {
|
|
|
292
296
|
tokenEndpoint,
|
|
293
297
|
refreshToken: currentRefreshToken,
|
|
294
298
|
resource,
|
|
299
|
+
organizationId,
|
|
295
300
|
}, this.adapter.requester);
|
|
296
301
|
this.accessTokenMap.set(accessTokenKey, {
|
|
297
302
|
token: accessToken,
|
|
@@ -358,7 +363,32 @@ class LogtoClient {
|
|
|
358
363
|
const cachedJwkSet = new remoteJwkSet.CachedRemoteJwkSet(new URL(jwksUri), this.adapter);
|
|
359
364
|
return async (...args) => cachedJwkSet.getKey(...args);
|
|
360
365
|
}
|
|
366
|
+
async #getAccessToken(resource, organizationId) {
|
|
367
|
+
if (!(await this.isAuthenticated())) {
|
|
368
|
+
throw new errors.LogtoClientError('not_authenticated');
|
|
369
|
+
}
|
|
370
|
+
const accessTokenKey = index$2.buildAccessTokenKey(resource, organizationId);
|
|
371
|
+
const accessToken = this.accessTokenMap.get(accessTokenKey);
|
|
372
|
+
if (accessToken && accessToken.expiresAt > Date.now() / 1000) {
|
|
373
|
+
return accessToken.token;
|
|
374
|
+
}
|
|
375
|
+
// Since the access token has expired, delete it from the map.
|
|
376
|
+
if (accessToken) {
|
|
377
|
+
this.accessTokenMap.delete(accessTokenKey);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Need to fetch a new access token using refresh token.
|
|
381
|
+
*/
|
|
382
|
+
return this.getAccessTokenByRefreshToken(resource, organizationId);
|
|
383
|
+
}
|
|
384
|
+
async #getOrganizationToken(organizationId) {
|
|
385
|
+
if (!this.logtoConfig.scopes?.includes(js.UserScope.Organizations)) {
|
|
386
|
+
throw new errors.LogtoClientError('missing_scope_organizations');
|
|
387
|
+
}
|
|
388
|
+
return this.#getAccessToken(undefined, organizationId);
|
|
389
|
+
}
|
|
361
390
|
}
|
|
391
|
+
/* eslint-enable max-lines */
|
|
362
392
|
|
|
363
393
|
Object.defineProperty(exports, 'LogtoError', {
|
|
364
394
|
enumerable: true,
|
|
@@ -376,6 +406,10 @@ Object.defineProperty(exports, 'Prompt', {
|
|
|
376
406
|
enumerable: true,
|
|
377
407
|
get: function () { return js.Prompt; }
|
|
378
408
|
});
|
|
409
|
+
Object.defineProperty(exports, 'ReservedResource', {
|
|
410
|
+
enumerable: true,
|
|
411
|
+
get: function () { return js.ReservedResource; }
|
|
412
|
+
});
|
|
379
413
|
Object.defineProperty(exports, 'ReservedScope', {
|
|
380
414
|
enumerable: true,
|
|
381
415
|
get: function () { return js.ReservedScope; }
|
|
@@ -384,9 +418,22 @@ Object.defineProperty(exports, 'UserScope', {
|
|
|
384
418
|
enumerable: true,
|
|
385
419
|
get: function () { return js.UserScope; }
|
|
386
420
|
});
|
|
421
|
+
Object.defineProperty(exports, 'buildOrganizationUrn', {
|
|
422
|
+
enumerable: true,
|
|
423
|
+
get: function () { return js.buildOrganizationUrn; }
|
|
424
|
+
});
|
|
425
|
+
Object.defineProperty(exports, 'getOrganizationIdFromUrn', {
|
|
426
|
+
enumerable: true,
|
|
427
|
+
get: function () { return js.getOrganizationIdFromUrn; }
|
|
428
|
+
});
|
|
429
|
+
Object.defineProperty(exports, 'organizationUrnPrefix', {
|
|
430
|
+
enumerable: true,
|
|
431
|
+
get: function () { return js.organizationUrnPrefix; }
|
|
432
|
+
});
|
|
387
433
|
exports.LogtoClientError = errors.LogtoClientError;
|
|
388
434
|
exports.isLogtoAccessTokenMap = index.isLogtoAccessTokenMap;
|
|
389
435
|
exports.isLogtoSignInSessionItem = index.isLogtoSignInSessionItem;
|
|
436
|
+
exports.normalizeLogtoConfig = index.normalizeLogtoConfig;
|
|
390
437
|
Object.defineProperty(exports, 'CacheKey', {
|
|
391
438
|
enumerable: true,
|
|
392
439
|
get: function () { return types.CacheKey; }
|
package/lib/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { type JWTVerifyGetKey } from 'jose';
|
|
|
4
4
|
import { ClientAdapterInstance, type ClientAdapter } from './adapter/index.js';
|
|
5
5
|
import type { AccessToken, LogtoConfig, LogtoSignInSessionItem } from './types/index.js';
|
|
6
6
|
export type { IdTokenClaims, LogtoErrorCode, UserInfoResponse, InteractionMode } from '@logto/js';
|
|
7
|
-
export { LogtoError, LogtoRequestError, OidcError, Prompt, ReservedScope, UserScope, } from '@logto/js';
|
|
7
|
+
export { LogtoError, LogtoRequestError, OidcError, Prompt, ReservedScope, ReservedResource, UserScope, organizationUrnPrefix, buildOrganizationUrn, getOrganizationIdFromUrn, } from '@logto/js';
|
|
8
8
|
export * from './errors.js';
|
|
9
9
|
export type { Storage, StorageKey, ClientAdapter } from './adapter/index.js';
|
|
10
10
|
export { PersistKey, CacheKey } from './adapter/index.js';
|
|
@@ -19,8 +19,8 @@ export * from './types/index.js';
|
|
|
19
19
|
*/
|
|
20
20
|
export default class LogtoClient {
|
|
21
21
|
#private;
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
readonly logtoConfig: LogtoConfig;
|
|
23
|
+
readonly getOidcConfig: (this: unknown) => Promise<import("@silverhand/essentials").KeysToCamelCase<{
|
|
24
24
|
authorization_endpoint: string;
|
|
25
25
|
token_endpoint: string;
|
|
26
26
|
userinfo_endpoint: string;
|
|
@@ -29,6 +29,34 @@ export default class LogtoClient {
|
|
|
29
29
|
jwks_uri: string;
|
|
30
30
|
issuer: string;
|
|
31
31
|
}>>;
|
|
32
|
+
/**
|
|
33
|
+
* Get the access token from the storage with refresh strategy.
|
|
34
|
+
*
|
|
35
|
+
* - If the access token has expired, it will try to fetch a new one using the Refresh Token.
|
|
36
|
+
* - If there's an ongoing Promise to fetch the access token, it will return the Promise.
|
|
37
|
+
*
|
|
38
|
+
* If you want to get the access token claims, use {@link getAccessTokenClaims} instead.
|
|
39
|
+
*
|
|
40
|
+
* @param resource The resource that the access token is granted for. If not
|
|
41
|
+
* specified, the access token will be used for OpenID Connect or the default
|
|
42
|
+
* resource, as specified in the Logto Console.
|
|
43
|
+
* @returns The access token string.
|
|
44
|
+
* @throws LogtoClientError if the user is not authenticated.
|
|
45
|
+
*/
|
|
46
|
+
readonly getAccessToken: (this: unknown, resource?: string | undefined, organizationId?: string | undefined) => Promise<string>;
|
|
47
|
+
/**
|
|
48
|
+
* Get the access token for the specified organization from the storage with refresh strategy.
|
|
49
|
+
*
|
|
50
|
+
* Scope {@link UserScope.Organizations} is required in the config to use organization-related
|
|
51
|
+
* methods.
|
|
52
|
+
*
|
|
53
|
+
* @param organizationId The ID of the organization that the access token is granted for.
|
|
54
|
+
* @returns The access token string.
|
|
55
|
+
* @throws LogtoClientError if the user is not authenticated.
|
|
56
|
+
* @remarks
|
|
57
|
+
* It uses the same refresh strategy as {@link getAccessToken}.
|
|
58
|
+
*/
|
|
59
|
+
readonly getOrganizationToken: (this: unknown, organizationId: string) => Promise<string>;
|
|
32
60
|
protected readonly getJwtVerifyGetKey: (...args: unknown[]) => Promise<JWTVerifyGetKey>;
|
|
33
61
|
protected readonly adapter: ClientAdapterInstance;
|
|
34
62
|
protected readonly accessTokenMap: Map<string, AccessToken>;
|
|
@@ -46,31 +74,24 @@ export default class LogtoClient {
|
|
|
46
74
|
* use {@link getIdTokenClaims} instead.
|
|
47
75
|
*/
|
|
48
76
|
getIdToken(): Promise<Nullable<string>>;
|
|
49
|
-
/**
|
|
50
|
-
* Get the Access Token from the storage. If the Access Token has expired, it
|
|
51
|
-
* will try to fetch a new one using the Refresh Token.
|
|
52
|
-
*
|
|
53
|
-
* If you want to get the Access Token claims, use {@link getAccessTokenClaims} instead.
|
|
54
|
-
*
|
|
55
|
-
* @param resource The resource that the Access Token is granted for. If not
|
|
56
|
-
* specified, the Access Token will be used for OpenID Connect or the default
|
|
57
|
-
* resource, as specified in the Logto Console.
|
|
58
|
-
* @returns The Access Token string.
|
|
59
|
-
* @throws LogtoClientError if the user is not authenticated.
|
|
60
|
-
*/
|
|
61
|
-
getAccessToken(resource?: string): Promise<string>;
|
|
62
77
|
/**
|
|
63
78
|
* Get the ID Token claims.
|
|
64
79
|
*/
|
|
65
80
|
getIdTokenClaims(): Promise<IdTokenClaims>;
|
|
66
81
|
/**
|
|
67
|
-
* Get the
|
|
82
|
+
* Get the access token claims for the specified resource.
|
|
68
83
|
*
|
|
69
|
-
* @param resource The resource that the
|
|
70
|
-
* specified, the
|
|
84
|
+
* @param resource The resource that the access token is granted for. If not
|
|
85
|
+
* specified, the access token will be used for OpenID Connect or the default
|
|
71
86
|
* resource, as specified in the Logto Console.
|
|
72
87
|
*/
|
|
73
88
|
getAccessTokenClaims(resource?: string): Promise<AccessTokenClaims>;
|
|
89
|
+
/**
|
|
90
|
+
* Get the organization token claims for the specified organization.
|
|
91
|
+
*
|
|
92
|
+
* @param organizationId The ID of the organization that the access token is granted for.
|
|
93
|
+
*/
|
|
94
|
+
getOrganizationTokenClaims(organizationId: string): Promise<AccessTokenClaims>;
|
|
74
95
|
/**
|
|
75
96
|
* Get the user information from the Userinfo Endpoint.
|
|
76
97
|
*
|
package/lib/index.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { LogtoError, LogtoRequestError, OidcError, Prompt, ReservedScope, UserScope } from '@logto/js';
|
|
1
|
+
import { decodeIdToken, decodeAccessToken, fetchUserInfo, generateSignInUri, verifyAndParseCodeFromCallbackUri, fetchTokenByAuthorizationCode, revoke, generateSignOutUri, fetchTokenByRefreshToken, verifyIdToken, fetchOidcConfig, UserScope } from '@logto/js';
|
|
2
|
+
export { LogtoError, LogtoRequestError, OidcError, Prompt, ReservedResource, ReservedScope, UserScope, buildOrganizationUrn, getOrganizationIdFromUrn, organizationUrnPrefix } from '@logto/js';
|
|
3
3
|
import { createRemoteJWKSet } from 'jose';
|
|
4
4
|
import { ClientAdapterInstance } from './adapter/index.js';
|
|
5
5
|
import { LogtoClientError } from './errors.js';
|
|
6
6
|
import { CachedRemoteJwkSet } from './remote-jwk-set.js';
|
|
7
|
-
import { isLogtoSignInSessionItem, isLogtoAccessTokenMap } from './types/index.js';
|
|
7
|
+
import { normalizeLogtoConfig, isLogtoSignInSessionItem, isLogtoAccessTokenMap } from './types/index.js';
|
|
8
8
|
import { buildAccessTokenKey, getDiscoveryEndpoint } from './utils/index.js';
|
|
9
9
|
import { memoize } from './utils/memoize.js';
|
|
10
10
|
import { once } from './utils/once.js';
|
|
11
11
|
import { PersistKey, CacheKey } from './adapter/types.js';
|
|
12
12
|
export { createRequester } from './utils/requester.js';
|
|
13
13
|
|
|
14
|
+
/* eslint-disable max-lines */
|
|
14
15
|
/**
|
|
15
16
|
* The Logto base client class that provides the essential methods for
|
|
16
17
|
* interacting with the Logto server.
|
|
@@ -21,13 +22,37 @@ export { createRequester } from './utils/requester.js';
|
|
|
21
22
|
class LogtoClient {
|
|
22
23
|
constructor(logtoConfig, adapter) {
|
|
23
24
|
this.getOidcConfig = memoize(this.#getOidcConfig);
|
|
25
|
+
/**
|
|
26
|
+
* Get the access token from the storage with refresh strategy.
|
|
27
|
+
*
|
|
28
|
+
* - If the access token has expired, it will try to fetch a new one using the Refresh Token.
|
|
29
|
+
* - If there's an ongoing Promise to fetch the access token, it will return the Promise.
|
|
30
|
+
*
|
|
31
|
+
* If you want to get the access token claims, use {@link getAccessTokenClaims} instead.
|
|
32
|
+
*
|
|
33
|
+
* @param resource The resource that the access token is granted for. If not
|
|
34
|
+
* specified, the access token will be used for OpenID Connect or the default
|
|
35
|
+
* resource, as specified in the Logto Console.
|
|
36
|
+
* @returns The access token string.
|
|
37
|
+
* @throws LogtoClientError if the user is not authenticated.
|
|
38
|
+
*/
|
|
39
|
+
this.getAccessToken = memoize(this.#getAccessToken);
|
|
40
|
+
/**
|
|
41
|
+
* Get the access token for the specified organization from the storage with refresh strategy.
|
|
42
|
+
*
|
|
43
|
+
* Scope {@link UserScope.Organizations} is required in the config to use organization-related
|
|
44
|
+
* methods.
|
|
45
|
+
*
|
|
46
|
+
* @param organizationId The ID of the organization that the access token is granted for.
|
|
47
|
+
* @returns The access token string.
|
|
48
|
+
* @throws LogtoClientError if the user is not authenticated.
|
|
49
|
+
* @remarks
|
|
50
|
+
* It uses the same refresh strategy as {@link getAccessToken}.
|
|
51
|
+
*/
|
|
52
|
+
this.getOrganizationToken = memoize(this.#getOrganizationToken);
|
|
24
53
|
this.getJwtVerifyGetKey = once(this.#getJwtVerifyGetKey);
|
|
25
54
|
this.accessTokenMap = new Map();
|
|
26
|
-
this.logtoConfig =
|
|
27
|
-
...logtoConfig,
|
|
28
|
-
prompt: logtoConfig.prompt ?? Prompt.Consent,
|
|
29
|
-
scopes: withDefaultScopes(logtoConfig.scopes).split(' '),
|
|
30
|
-
};
|
|
55
|
+
this.logtoConfig = normalizeLogtoConfig(logtoConfig);
|
|
31
56
|
this.adapter = new ClientAdapterInstance(adapter);
|
|
32
57
|
void this.loadAccessTokenMap();
|
|
33
58
|
}
|
|
@@ -50,36 +75,6 @@ class LogtoClient {
|
|
|
50
75
|
async getIdToken() {
|
|
51
76
|
return this.adapter.storage.getItem('idToken');
|
|
52
77
|
}
|
|
53
|
-
/**
|
|
54
|
-
* Get the Access Token from the storage. If the Access Token has expired, it
|
|
55
|
-
* will try to fetch a new one using the Refresh Token.
|
|
56
|
-
*
|
|
57
|
-
* If you want to get the Access Token claims, use {@link getAccessTokenClaims} instead.
|
|
58
|
-
*
|
|
59
|
-
* @param resource The resource that the Access Token is granted for. If not
|
|
60
|
-
* specified, the Access Token will be used for OpenID Connect or the default
|
|
61
|
-
* resource, as specified in the Logto Console.
|
|
62
|
-
* @returns The Access Token string.
|
|
63
|
-
* @throws LogtoClientError if the user is not authenticated.
|
|
64
|
-
*/
|
|
65
|
-
async getAccessToken(resource) {
|
|
66
|
-
if (!(await this.getIdToken())) {
|
|
67
|
-
throw new LogtoClientError('not_authenticated');
|
|
68
|
-
}
|
|
69
|
-
const accessTokenKey = buildAccessTokenKey(resource);
|
|
70
|
-
const accessToken = this.accessTokenMap.get(accessTokenKey);
|
|
71
|
-
if (accessToken && accessToken.expiresAt > Date.now() / 1000) {
|
|
72
|
-
return accessToken.token;
|
|
73
|
-
}
|
|
74
|
-
// Since the access token has expired, delete it from the map.
|
|
75
|
-
if (accessToken) {
|
|
76
|
-
this.accessTokenMap.delete(accessTokenKey);
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Need to fetch a new access token using refresh token.
|
|
80
|
-
*/
|
|
81
|
-
return this.getAccessTokenByRefreshToken(resource);
|
|
82
|
-
}
|
|
83
78
|
/**
|
|
84
79
|
* Get the ID Token claims.
|
|
85
80
|
*/
|
|
@@ -91,16 +86,25 @@ class LogtoClient {
|
|
|
91
86
|
return decodeIdToken(idToken);
|
|
92
87
|
}
|
|
93
88
|
/**
|
|
94
|
-
* Get the
|
|
89
|
+
* Get the access token claims for the specified resource.
|
|
95
90
|
*
|
|
96
|
-
* @param resource The resource that the
|
|
97
|
-
* specified, the
|
|
91
|
+
* @param resource The resource that the access token is granted for. If not
|
|
92
|
+
* specified, the access token will be used for OpenID Connect or the default
|
|
98
93
|
* resource, as specified in the Logto Console.
|
|
99
94
|
*/
|
|
100
95
|
async getAccessTokenClaims(resource) {
|
|
101
96
|
const accessToken = await this.getAccessToken(resource);
|
|
102
97
|
return decodeAccessToken(accessToken);
|
|
103
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Get the organization token claims for the specified organization.
|
|
101
|
+
*
|
|
102
|
+
* @param organizationId The ID of the organization that the access token is granted for.
|
|
103
|
+
*/
|
|
104
|
+
async getOrganizationTokenClaims(organizationId) {
|
|
105
|
+
const accessToken = await this.getOrganizationToken(organizationId);
|
|
106
|
+
return decodeAccessToken(accessToken);
|
|
107
|
+
}
|
|
104
108
|
/**
|
|
105
109
|
* Get the user information from the Userinfo Endpoint.
|
|
106
110
|
*
|
|
@@ -275,12 +279,12 @@ class LogtoClient {
|
|
|
275
279
|
async setRefreshToken(value) {
|
|
276
280
|
return this.adapter.setStorageItem(PersistKey.RefreshToken, value);
|
|
277
281
|
}
|
|
278
|
-
async getAccessTokenByRefreshToken(resource) {
|
|
282
|
+
async getAccessTokenByRefreshToken(resource, organizationId) {
|
|
279
283
|
const currentRefreshToken = await this.getRefreshToken();
|
|
280
284
|
if (!currentRefreshToken) {
|
|
281
285
|
throw new LogtoClientError('not_authenticated');
|
|
282
286
|
}
|
|
283
|
-
const accessTokenKey = buildAccessTokenKey(resource);
|
|
287
|
+
const accessTokenKey = buildAccessTokenKey(resource, organizationId);
|
|
284
288
|
const { appId: clientId } = this.logtoConfig;
|
|
285
289
|
const { tokenEndpoint } = await this.getOidcConfig();
|
|
286
290
|
const requestedAt = Math.round(Date.now() / 1000);
|
|
@@ -289,6 +293,7 @@ class LogtoClient {
|
|
|
289
293
|
tokenEndpoint,
|
|
290
294
|
refreshToken: currentRefreshToken,
|
|
291
295
|
resource,
|
|
296
|
+
organizationId,
|
|
292
297
|
}, this.adapter.requester);
|
|
293
298
|
this.accessTokenMap.set(accessTokenKey, {
|
|
294
299
|
token: accessToken,
|
|
@@ -355,6 +360,31 @@ class LogtoClient {
|
|
|
355
360
|
const cachedJwkSet = new CachedRemoteJwkSet(new URL(jwksUri), this.adapter);
|
|
356
361
|
return async (...args) => cachedJwkSet.getKey(...args);
|
|
357
362
|
}
|
|
363
|
+
async #getAccessToken(resource, organizationId) {
|
|
364
|
+
if (!(await this.isAuthenticated())) {
|
|
365
|
+
throw new LogtoClientError('not_authenticated');
|
|
366
|
+
}
|
|
367
|
+
const accessTokenKey = buildAccessTokenKey(resource, organizationId);
|
|
368
|
+
const accessToken = this.accessTokenMap.get(accessTokenKey);
|
|
369
|
+
if (accessToken && accessToken.expiresAt > Date.now() / 1000) {
|
|
370
|
+
return accessToken.token;
|
|
371
|
+
}
|
|
372
|
+
// Since the access token has expired, delete it from the map.
|
|
373
|
+
if (accessToken) {
|
|
374
|
+
this.accessTokenMap.delete(accessTokenKey);
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Need to fetch a new access token using refresh token.
|
|
378
|
+
*/
|
|
379
|
+
return this.getAccessTokenByRefreshToken(resource, organizationId);
|
|
380
|
+
}
|
|
381
|
+
async #getOrganizationToken(organizationId) {
|
|
382
|
+
if (!this.logtoConfig.scopes?.includes(UserScope.Organizations)) {
|
|
383
|
+
throw new LogtoClientError('missing_scope_organizations');
|
|
384
|
+
}
|
|
385
|
+
return this.#getAccessToken(undefined, organizationId);
|
|
386
|
+
}
|
|
358
387
|
}
|
|
388
|
+
/* eslint-enable max-lines */
|
|
359
389
|
|
|
360
|
-
export { CacheKey, LogtoClientError, PersistKey, LogtoClient as default, isLogtoAccessTokenMap, isLogtoSignInSessionItem };
|
|
390
|
+
export { CacheKey, LogtoClientError, PersistKey, LogtoClient as default, isLogtoAccessTokenMap, isLogtoSignInSessionItem, normalizeLogtoConfig };
|
package/lib/mock.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ export declare const createAdapters: (withCache?: boolean) => {
|
|
|
66
66
|
generateCodeVerifier: jest.Mock<string, [], any>;
|
|
67
67
|
generateState: jest.Mock<string, [], any>;
|
|
68
68
|
};
|
|
69
|
-
export declare const createClient: (prompt?: Prompt, storage?: MockedStorage, withCache?: boolean) => LogtoClientWithAccessors;
|
|
69
|
+
export declare const createClient: (prompt?: Prompt, storage?: MockedStorage, withCache?: boolean, scopes?: string[]) => LogtoClientWithAccessors;
|
|
70
70
|
/**
|
|
71
71
|
* Make protected fields accessible for test
|
|
72
72
|
*/
|
package/lib/types/index.cjs
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var js = require('@logto/js');
|
|
4
|
+
var essentials = require('@silverhand/essentials');
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Normalize the Logto client configuration per the following rules:
|
|
8
|
+
*
|
|
9
|
+
* - Add default scopes (`openid`, `offline_access` and `profile`) if not provided.
|
|
10
|
+
* - Add {@link ReservedResource.Organization} to resources if {@link UserScope.Organizations} is included in scopes.
|
|
11
|
+
*
|
|
12
|
+
* @param config The Logto client configuration to be normalized.
|
|
13
|
+
* @returns The normalized Logto client configuration.
|
|
14
|
+
*/
|
|
15
|
+
const normalizeLogtoConfig = (config) => {
|
|
16
|
+
const { prompt = js.Prompt.Consent, scopes = [], resources, ...rest } = config;
|
|
17
|
+
return {
|
|
18
|
+
...rest,
|
|
19
|
+
prompt,
|
|
20
|
+
scopes: js.withDefaultScopes(scopes).split(' '),
|
|
21
|
+
resources: scopes.includes(js.UserScope.Organizations)
|
|
22
|
+
? essentials.deduplicate([...(resources ?? []), js.ReservedResource.Organization])
|
|
23
|
+
: resources,
|
|
24
|
+
};
|
|
25
|
+
};
|
|
5
26
|
const isLogtoSignInSessionItem = (data) => {
|
|
6
27
|
if (!js.isArbitraryObject(data)) {
|
|
7
28
|
return false;
|
|
@@ -24,3 +45,4 @@ const isLogtoAccessTokenMap = (data) => {
|
|
|
24
45
|
|
|
25
46
|
exports.isLogtoAccessTokenMap = isLogtoAccessTokenMap;
|
|
26
47
|
exports.isLogtoSignInSessionItem = isLogtoSignInSessionItem;
|
|
48
|
+
exports.normalizeLogtoConfig = normalizeLogtoConfig;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Prompt } from '@logto/js';
|
|
2
2
|
/** The configuration object for the Logto client. */
|
|
3
3
|
export type LogtoConfig = {
|
|
4
4
|
/**
|
|
@@ -40,6 +40,16 @@ export type LogtoConfig = {
|
|
|
40
40
|
*/
|
|
41
41
|
prompt?: Prompt;
|
|
42
42
|
};
|
|
43
|
+
/**
|
|
44
|
+
* Normalize the Logto client configuration per the following rules:
|
|
45
|
+
*
|
|
46
|
+
* - Add default scopes (`openid`, `offline_access` and `profile`) if not provided.
|
|
47
|
+
* - Add {@link ReservedResource.Organization} to resources if {@link UserScope.Organizations} is included in scopes.
|
|
48
|
+
*
|
|
49
|
+
* @param config The Logto client configuration to be normalized.
|
|
50
|
+
* @returns The normalized Logto client configuration.
|
|
51
|
+
*/
|
|
52
|
+
export declare const normalizeLogtoConfig: (config: LogtoConfig) => LogtoConfig;
|
|
43
53
|
export type AccessToken = {
|
|
44
54
|
/** The access token string. */
|
|
45
55
|
token: string;
|
package/lib/types/index.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
|
-
import { isArbitraryObject } from '@logto/js';
|
|
1
|
+
import { Prompt, withDefaultScopes, UserScope, ReservedResource, isArbitraryObject } from '@logto/js';
|
|
2
|
+
import { deduplicate } from '@silverhand/essentials';
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Normalize the Logto client configuration per the following rules:
|
|
6
|
+
*
|
|
7
|
+
* - Add default scopes (`openid`, `offline_access` and `profile`) if not provided.
|
|
8
|
+
* - Add {@link ReservedResource.Organization} to resources if {@link UserScope.Organizations} is included in scopes.
|
|
9
|
+
*
|
|
10
|
+
* @param config The Logto client configuration to be normalized.
|
|
11
|
+
* @returns The normalized Logto client configuration.
|
|
12
|
+
*/
|
|
13
|
+
const normalizeLogtoConfig = (config) => {
|
|
14
|
+
const { prompt = Prompt.Consent, scopes = [], resources, ...rest } = config;
|
|
15
|
+
return {
|
|
16
|
+
...rest,
|
|
17
|
+
prompt,
|
|
18
|
+
scopes: withDefaultScopes(scopes).split(' '),
|
|
19
|
+
resources: scopes.includes(UserScope.Organizations)
|
|
20
|
+
? deduplicate([...(resources ?? []), ReservedResource.Organization])
|
|
21
|
+
: resources,
|
|
22
|
+
};
|
|
23
|
+
};
|
|
3
24
|
const isLogtoSignInSessionItem = (data) => {
|
|
4
25
|
if (!isArbitraryObject(data)) {
|
|
5
26
|
return false;
|
|
@@ -20,4 +41,4 @@ const isLogtoAccessTokenMap = (data) => {
|
|
|
20
41
|
});
|
|
21
42
|
};
|
|
22
43
|
|
|
23
|
-
export { isLogtoAccessTokenMap, isLogtoSignInSessionItem };
|
|
44
|
+
export { isLogtoAccessTokenMap, isLogtoSignInSessionItem, normalizeLogtoConfig };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/utils/index.cjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var js = require('@logto/js');
|
|
4
|
+
var essentials = require('@silverhand/essentials');
|
|
4
5
|
|
|
5
|
-
const buildAccessTokenKey = (resource = '', scopes = []) => `${scopes.slice().sort().join(' ')}@${resource}`;
|
|
6
|
+
const buildAccessTokenKey = (resource = '', organizationId, scopes = []) => `${scopes.slice().sort().join(' ')}@${resource}${essentials.conditionalString(organizationId && `#${organizationId}`)}`;
|
|
6
7
|
const getDiscoveryEndpoint = (endpoint) => new URL(js.discoveryPath, endpoint).toString();
|
|
7
8
|
|
|
8
9
|
exports.buildAccessTokenKey = buildAccessTokenKey;
|
package/lib/utils/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export * from './requester.js';
|
|
2
|
-
export declare const buildAccessTokenKey: (resource?: string, scopes?: string[]) => string;
|
|
2
|
+
export declare const buildAccessTokenKey: (resource?: string, organizationId?: string, scopes?: string[]) => string;
|
|
3
3
|
export declare const getDiscoveryEndpoint: (endpoint: string) => string;
|
package/lib/utils/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { discoveryPath } from '@logto/js';
|
|
2
|
+
import { conditionalString } from '@silverhand/essentials';
|
|
2
3
|
|
|
3
|
-
const buildAccessTokenKey = (resource = '', scopes = []) => `${scopes.slice().sort().join(' ')}@${resource}`;
|
|
4
|
+
const buildAccessTokenKey = (resource = '', organizationId, scopes = []) => `${scopes.slice().sort().join(' ')}@${resource}${conditionalString(organizationId && `#${organizationId}`)}`;
|
|
4
5
|
const getDiscoveryEndpoint = (endpoint) => new URL(discoveryPath, endpoint).toString();
|
|
5
6
|
|
|
6
7
|
export { buildAccessTokenKey, getDiscoveryEndpoint };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logto/client",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./lib/index.cjs",
|
|
6
6
|
"module": "./lib/index.js",
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"directory": "packages/client"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@logto/js": "^
|
|
24
|
+
"@logto/js": "^3.0.0",
|
|
25
25
|
"@silverhand/essentials": "^2.6.2",
|
|
26
26
|
"camelcase-keys": "^7.0.1",
|
|
27
|
-
"jose": "^
|
|
27
|
+
"jose": "^5.0.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@silverhand/eslint-config": "^4.0.1",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"eslint": "^8.44.0",
|
|
37
37
|
"jest": "^29.5.0",
|
|
38
38
|
"jest-matcher-specific-error": "^1.0.0",
|
|
39
|
-
"lint-staged": "^
|
|
39
|
+
"lint-staged": "^15.0.0",
|
|
40
40
|
"nock": "^13.3.0",
|
|
41
41
|
"prettier": "^3.0.0",
|
|
42
42
|
"text-encoder": "^0.0.4",
|