@logto/client 2.6.7 → 2.7.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.
@@ -3,24 +3,26 @@
3
3
  var js = require('@logto/js');
4
4
  var jose = require('jose');
5
5
 
6
- const issuedAtTimeTolerance = 300; // 5 minutes
7
- const verifyIdToken = async (idToken, clientId, issuer, jwks) => {
8
- const result = await jose.jwtVerify(idToken, jwks, { audience: clientId, issuer });
9
- if (Math.abs((result.payload.iat ?? 0) - Date.now() / 1000) > issuedAtTimeTolerance) {
6
+ const defaultClockTolerance = 300; // 5 minutes
7
+ const verifyIdToken = async (idToken, clientId, issuer, jwks, clockTolerance = defaultClockTolerance) => {
8
+ const result = await jose.jwtVerify(idToken, jwks, { audience: clientId, issuer, clockTolerance });
9
+ if (Math.abs((result.payload.iat ?? 0) - Date.now() / 1000) > clockTolerance) {
10
10
  throw new js.LogtoError('id_token.invalid_iat');
11
11
  }
12
12
  };
13
13
  class DefaultJwtVerifier {
14
- constructor(client) {
14
+ constructor(client, clockTolerance = defaultClockTolerance) {
15
15
  this.client = client;
16
+ this.clockTolerance = clockTolerance;
16
17
  }
17
18
  async verifyIdToken(idToken) {
18
19
  const { appId } = this.client.logtoConfig;
19
20
  const { issuer, jwksUri } = await this.client.getOidcConfig();
20
21
  this.getJwtVerifyGetKey ||= jose.createRemoteJWKSet(new URL(jwksUri));
21
- await verifyIdToken(idToken, appId, issuer, this.getJwtVerifyGetKey);
22
+ await verifyIdToken(idToken, appId, issuer, this.getJwtVerifyGetKey, this.clockTolerance);
22
23
  }
23
24
  }
24
25
 
25
26
  exports.DefaultJwtVerifier = DefaultJwtVerifier;
27
+ exports.defaultClockTolerance = defaultClockTolerance;
26
28
  exports.verifyIdToken = verifyIdToken;
@@ -1,10 +1,12 @@
1
1
  import type { JWTVerifyGetKey } from 'jose';
2
2
  import { type StandardLogtoClient } from '../client.js';
3
3
  import { type JwtVerifier } from './types.js';
4
- export declare const verifyIdToken: (idToken: string, clientId: string, issuer: string, jwks: JWTVerifyGetKey) => Promise<void>;
4
+ export declare const defaultClockTolerance = 300;
5
+ export declare const verifyIdToken: (idToken: string, clientId: string, issuer: string, jwks: JWTVerifyGetKey, clockTolerance?: number) => Promise<void>;
5
6
  export declare class DefaultJwtVerifier implements JwtVerifier {
6
7
  protected client: StandardLogtoClient;
8
+ readonly clockTolerance: number;
7
9
  protected getJwtVerifyGetKey?: JWTVerifyGetKey;
8
- constructor(client: StandardLogtoClient);
10
+ constructor(client: StandardLogtoClient, clockTolerance?: number);
9
11
  verifyIdToken(idToken: string): Promise<void>;
10
12
  }
@@ -1,23 +1,24 @@
1
1
  import { LogtoError } from '@logto/js';
2
2
  import { createRemoteJWKSet, jwtVerify } from 'jose';
3
3
 
4
- const issuedAtTimeTolerance = 300; // 5 minutes
5
- const verifyIdToken = async (idToken, clientId, issuer, jwks) => {
6
- const result = await jwtVerify(idToken, jwks, { audience: clientId, issuer });
7
- if (Math.abs((result.payload.iat ?? 0) - Date.now() / 1000) > issuedAtTimeTolerance) {
4
+ const defaultClockTolerance = 300; // 5 minutes
5
+ const verifyIdToken = async (idToken, clientId, issuer, jwks, clockTolerance = defaultClockTolerance) => {
6
+ const result = await jwtVerify(idToken, jwks, { audience: clientId, issuer, clockTolerance });
7
+ if (Math.abs((result.payload.iat ?? 0) - Date.now() / 1000) > clockTolerance) {
8
8
  throw new LogtoError('id_token.invalid_iat');
9
9
  }
10
10
  };
11
11
  class DefaultJwtVerifier {
12
- constructor(client) {
12
+ constructor(client, clockTolerance = defaultClockTolerance) {
13
13
  this.client = client;
14
+ this.clockTolerance = clockTolerance;
14
15
  }
15
16
  async verifyIdToken(idToken) {
16
17
  const { appId } = this.client.logtoConfig;
17
18
  const { issuer, jwksUri } = await this.client.getOidcConfig();
18
19
  this.getJwtVerifyGetKey ||= createRemoteJWKSet(new URL(jwksUri));
19
- await verifyIdToken(idToken, appId, issuer, this.getJwtVerifyGetKey);
20
+ await verifyIdToken(idToken, appId, issuer, this.getJwtVerifyGetKey, this.clockTolerance);
20
21
  }
21
22
  }
22
23
 
23
- export { DefaultJwtVerifier, verifyIdToken };
24
+ export { DefaultJwtVerifier, defaultClockTolerance, verifyIdToken };
package/lib/client.cjs CHANGED
@@ -23,6 +23,9 @@ var types = require('./adapter/types.cjs');
23
23
  * can use `StandardLogtoClient` and provide your own JWT verifier.
24
24
  */
25
25
  class StandardLogtoClient {
26
+ get jwtVerifier() {
27
+ return this.jwtVerifierInstance;
28
+ }
26
29
  constructor(logtoConfig, adapter, buildJwtVerifier) {
27
30
  /**
28
31
  * Get the OIDC configuration from the discovery endpoint. This method will
@@ -78,9 +81,17 @@ class StandardLogtoClient {
78
81
  this.accessTokenMap = new Map();
79
82
  this.logtoConfig = index.normalizeLogtoConfig(logtoConfig);
80
83
  this.adapter = new index$1.ClientAdapterInstance(adapter);
81
- this.jwtVerifier = buildJwtVerifier(this);
84
+ this.jwtVerifierInstance = buildJwtVerifier(this);
82
85
  void this.loadAccessTokenMap();
83
86
  }
87
+ /**
88
+ * Set the JWT verifier for the client.
89
+ * @param buildJwtVerifier The JWT verifier instance or a function that returns the JWT verifier instance.
90
+ */
91
+ setJwtVerifier(buildJwtVerifier) {
92
+ this.jwtVerifierInstance =
93
+ typeof buildJwtVerifier === 'function' ? buildJwtVerifier(this) : buildJwtVerifier;
94
+ }
84
95
  /**
85
96
  * Check if the user is authenticated by checking if the ID token exists.
86
97
  */
package/lib/client.d.ts CHANGED
@@ -85,9 +85,15 @@ export declare class StandardLogtoClient {
85
85
  */
86
86
  readonly handleSignInCallback: (this: unknown, callbackUri: string) => Promise<void>;
87
87
  readonly adapter: ClientAdapterInstance;
88
- readonly jwtVerifier: JwtVerifier;
88
+ protected jwtVerifierInstance: JwtVerifier;
89
89
  protected readonly accessTokenMap: Map<string, AccessToken>;
90
+ get jwtVerifier(): JwtVerifier;
90
91
  constructor(logtoConfig: LogtoConfig, adapter: ClientAdapter, buildJwtVerifier: (client: StandardLogtoClient) => JwtVerifier);
92
+ /**
93
+ * Set the JWT verifier for the client.
94
+ * @param buildJwtVerifier The JWT verifier instance or a function that returns the JWT verifier instance.
95
+ */
96
+ setJwtVerifier(buildJwtVerifier: JwtVerifier | ((client: StandardLogtoClient) => JwtVerifier)): void;
91
97
  /**
92
98
  * Check if the user is authenticated by checking if the ID token exists.
93
99
  */
package/lib/client.js CHANGED
@@ -21,6 +21,9 @@ import { PersistKey, CacheKey } from './adapter/types.js';
21
21
  * can use `StandardLogtoClient` and provide your own JWT verifier.
22
22
  */
23
23
  class StandardLogtoClient {
24
+ get jwtVerifier() {
25
+ return this.jwtVerifierInstance;
26
+ }
24
27
  constructor(logtoConfig, adapter, buildJwtVerifier) {
25
28
  /**
26
29
  * Get the OIDC configuration from the discovery endpoint. This method will
@@ -76,9 +79,17 @@ class StandardLogtoClient {
76
79
  this.accessTokenMap = new Map();
77
80
  this.logtoConfig = normalizeLogtoConfig(logtoConfig);
78
81
  this.adapter = new ClientAdapterInstance(adapter);
79
- this.jwtVerifier = buildJwtVerifier(this);
82
+ this.jwtVerifierInstance = buildJwtVerifier(this);
80
83
  void this.loadAccessTokenMap();
81
84
  }
85
+ /**
86
+ * Set the JWT verifier for the client.
87
+ * @param buildJwtVerifier The JWT verifier instance or a function that returns the JWT verifier instance.
88
+ */
89
+ setJwtVerifier(buildJwtVerifier) {
90
+ this.jwtVerifierInstance =
91
+ typeof buildJwtVerifier === 'function' ? buildJwtVerifier(this) : buildJwtVerifier;
92
+ }
82
93
  /**
83
94
  * Check if the user is authenticated by checking if the ID token exists.
84
95
  */
@@ -6,18 +6,21 @@ var essentials = require('@silverhand/essentials');
6
6
  /**
7
7
  * Normalize the Logto client configuration per the following rules:
8
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.
9
+ * - Add default scopes (`openid`, `offline_access` and `profile`) if not provided and
10
+ * `includeReservedScopes` is `true`.
11
+ * - Add {@link ReservedResource.Organization} to resources if {@link UserScope.Organizations} is
12
+ * included in scopes.
11
13
  *
12
14
  * @param config The Logto client configuration to be normalized.
13
15
  * @returns The normalized Logto client configuration.
14
16
  */
15
17
  const normalizeLogtoConfig = (config) => {
16
18
  const { prompt = js.Prompt.Consent, scopes = [], resources, ...rest } = config;
19
+ const includeReservedScopes = config.includeReservedScopes ?? true;
17
20
  return {
18
21
  ...rest,
19
22
  prompt,
20
- scopes: js.withDefaultScopes(scopes).split(' '),
23
+ scopes: includeReservedScopes ? js.withReservedScopes(scopes).split(' ') : scopes,
21
24
  resources: scopes.includes(js.UserScope.Organizations)
22
25
  ? essentials.deduplicate([...(resources ?? []), js.ReservedResource.Organization])
23
26
  : resources,
@@ -39,12 +39,20 @@ export type LogtoConfig = {
39
39
  * The prompt parameter to be used for the authorization request.
40
40
  */
41
41
  prompt?: Prompt | Prompt[];
42
+ /**
43
+ * Whether to include reserved scopes (`openid`, `offline_access` and `profile`) in the scopes.
44
+ *
45
+ * @default true
46
+ */
47
+ includeReservedScopes?: boolean;
42
48
  };
43
49
  /**
44
50
  * Normalize the Logto client configuration per the following rules:
45
51
  *
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.
52
+ * - Add default scopes (`openid`, `offline_access` and `profile`) if not provided and
53
+ * `includeReservedScopes` is `true`.
54
+ * - Add {@link ReservedResource.Organization} to resources if {@link UserScope.Organizations} is
55
+ * included in scopes.
48
56
  *
49
57
  * @param config The Logto client configuration to be normalized.
50
58
  * @returns The normalized Logto client configuration.
@@ -1,21 +1,24 @@
1
- import { Prompt, withDefaultScopes, UserScope, ReservedResource, isArbitraryObject } from '@logto/js';
1
+ import { Prompt, withReservedScopes, UserScope, ReservedResource, isArbitraryObject } from '@logto/js';
2
2
  import { deduplicate } from '@silverhand/essentials';
3
3
 
4
4
  /**
5
5
  * Normalize the Logto client configuration per the following rules:
6
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.
7
+ * - Add default scopes (`openid`, `offline_access` and `profile`) if not provided and
8
+ * `includeReservedScopes` is `true`.
9
+ * - Add {@link ReservedResource.Organization} to resources if {@link UserScope.Organizations} is
10
+ * included in scopes.
9
11
  *
10
12
  * @param config The Logto client configuration to be normalized.
11
13
  * @returns The normalized Logto client configuration.
12
14
  */
13
15
  const normalizeLogtoConfig = (config) => {
14
16
  const { prompt = Prompt.Consent, scopes = [], resources, ...rest } = config;
17
+ const includeReservedScopes = config.includeReservedScopes ?? true;
15
18
  return {
16
19
  ...rest,
17
20
  prompt,
18
- scopes: withDefaultScopes(scopes).split(' '),
21
+ scopes: includeReservedScopes ? withReservedScopes(scopes).split(' ') : scopes,
19
22
  resources: scopes.includes(UserScope.Organizations)
20
23
  ? deduplicate([...(resources ?? []), ReservedResource.Organization])
21
24
  : resources,
@@ -15,12 +15,12 @@ const createRequester = (fetchFunction) => {
15
15
  if (!response.ok) {
16
16
  const responseJson = await response.json();
17
17
  console.error(`Logto requester error: [status=${response.status}]`, responseJson);
18
- if (!js.isLogtoRequestError(responseJson)) {
18
+ if (!js.isLogtoRequestErrorJson(responseJson)) {
19
19
  throw new js.LogtoError('unexpected_response_error', responseJson);
20
20
  }
21
21
  // Expected request error from server
22
22
  const { code, message } = responseJson;
23
- throw new js.LogtoRequestError(code, message);
23
+ throw new js.LogtoRequestError(code, message, response.clone());
24
24
  }
25
25
  return response.json();
26
26
  };
@@ -1,4 +1,4 @@
1
- import { isLogtoRequestError, LogtoError, LogtoRequestError } from '@logto/js';
1
+ import { isLogtoRequestErrorJson, LogtoError, LogtoRequestError } from '@logto/js';
2
2
 
3
3
  /**
4
4
  * A factory function that creates a requester by accepting a `fetch`-like function.
@@ -13,12 +13,12 @@ const createRequester = (fetchFunction) => {
13
13
  if (!response.ok) {
14
14
  const responseJson = await response.json();
15
15
  console.error(`Logto requester error: [status=${response.status}]`, responseJson);
16
- if (!isLogtoRequestError(responseJson)) {
16
+ if (!isLogtoRequestErrorJson(responseJson)) {
17
17
  throw new LogtoError('unexpected_response_error', responseJson);
18
18
  }
19
19
  // Expected request error from server
20
20
  const { code, message } = responseJson;
21
- throw new LogtoRequestError(code, message);
21
+ throw new LogtoRequestError(code, message, response.clone());
22
22
  }
23
23
  return response.json();
24
24
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logto/client",
3
- "version": "2.6.7",
3
+ "version": "2.7.0",
4
4
  "type": "module",
5
5
  "main": "./lib/index.cjs",
6
6
  "module": "./lib/index.js",
@@ -32,7 +32,7 @@
32
32
  "@silverhand/essentials": "^2.8.7",
33
33
  "camelcase-keys": "^7.0.1",
34
34
  "jose": "^5.2.2",
35
- "@logto/js": "^4.1.1"
35
+ "@logto/js": "^4.1.3"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@silverhand/eslint-config": "^6.0.1",
@@ -42,7 +42,7 @@
42
42
  "eslint": "^8.57.0",
43
43
  "happy-dom": "^14.0.0",
44
44
  "lint-staged": "^15.0.0",
45
- "nock": "14.0.0-beta.5",
45
+ "nock": "14.0.0-beta.7",
46
46
  "prettier": "^3.0.0",
47
47
  "typescript": "^5.3.3",
48
48
  "vitest": "^1.4.0"