@logto/browser 0.1.2-rc.1 → 0.1.4

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 (3) hide show
  1. package/lib/index.d.ts +22 -10
  2. package/lib/index.js +60 -31
  3. package/package.json +10 -3
package/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { IdTokenClaims, OidcConfigResponse, Requester, UserInfoResponse } from '@logto/js';
1
+ import { IdTokenClaims, Requester, UserInfoResponse } from '@logto/js';
2
2
  import { Nullable } from '@silverhand/essentials';
3
3
  import { Infer } from 'superstruct';
4
4
  export type { IdTokenClaims, UserInfoResponse } from '@logto/js';
@@ -26,29 +26,41 @@ export declare const LogtoSignInSessionItemSchema: import("superstruct").Struct<
26
26
  }>;
27
27
  export declare type LogtoSignInSessionItem = Infer<typeof LogtoSignInSessionItemSchema>;
28
28
  export default class LogtoClient {
29
- protected logtoConfig: LogtoConfig;
30
- protected oidcConfig?: OidcConfigResponse;
31
- protected logtoStorageKey: string;
32
- protected requester: Requester;
33
- protected accessTokenMap: Map<string, AccessToken>;
29
+ protected readonly logtoConfig: LogtoConfig;
30
+ protected readonly getOidcConfig: () => Promise<import("@silverhand/essentials").KeysToCamelCase<{
31
+ authorization_endpoint: string;
32
+ token_endpoint: string;
33
+ userinfo_endpoint: string;
34
+ end_session_endpoint: string;
35
+ revocation_endpoint: string;
36
+ jwks_uri: string;
37
+ issuer: string;
38
+ }>>;
39
+ protected readonly getJwtVerifyGetKey: () => Promise<import("jose/dist/types/types").GetKeyFunction<import("jose").JWSHeaderParameters, import("jose").FlattenedJWSInput>>;
40
+ protected readonly logtoStorageKey: string;
41
+ protected readonly requester: Requester;
42
+ protected readonly accessTokenMap: Map<string, AccessToken>;
43
+ private readonly getAccessTokenPromiseMap;
34
44
  private _refreshToken;
35
45
  private _idToken;
36
46
  constructor(logtoConfig: LogtoConfig, requester?: <T>(input: RequestInfo, init?: RequestInit | undefined) => Promise<T>);
37
47
  get isAuthenticated(): boolean;
38
48
  protected get signInSession(): Nullable<LogtoSignInSessionItem>;
39
49
  protected set signInSession(logtoSignInSessionItem: Nullable<LogtoSignInSessionItem>);
40
- private get refreshToken();
50
+ get refreshToken(): Nullable<string>;
41
51
  private set refreshToken(value);
42
- private get idToken();
52
+ get idToken(): Nullable<string>;
43
53
  private set idToken(value);
44
- getAccessToken(resource?: string): Promise<Nullable<string>>;
54
+ getAccessToken(resource?: string): Promise<string>;
45
55
  getIdTokenClaims(): IdTokenClaims;
46
56
  fetchUserInfo(): Promise<UserInfoResponse>;
47
57
  signIn(redirectUri: string): Promise<void>;
48
58
  isSignInRedirected(url: string): boolean;
49
59
  handleSignInCallback(callbackUri: string): Promise<void>;
50
60
  signOut(postLogoutRedirectUri?: string): Promise<void>;
51
- private getOidcConfig;
61
+ private getAccessTokenByRefreshToken;
62
+ private _getOidcConfig;
63
+ private _getJwtVerifyGetKey;
52
64
  private verifyIdToken;
53
65
  private saveCodeToken;
54
66
  }
package/lib/index.js CHANGED
@@ -9,10 +9,14 @@ var __createBinding = (this && this.__createBinding) || (Object.create ? (functi
9
9
  var __exportStar = (this && this.__exportStar) || function(m, exports) {
10
10
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
11
11
  };
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
12
15
  Object.defineProperty(exports, "__esModule", { value: true });
13
16
  exports.LogtoSignInSessionItemSchema = void 0;
14
17
  const js_1 = require("@logto/js");
15
18
  const jose_1 = require("jose");
19
+ const lodash_once_1 = __importDefault(require("lodash.once"));
16
20
  const superstruct_1 = require("superstruct");
17
21
  const errors_1 = require("./errors");
18
22
  const utils_1 = require("./utils");
@@ -24,7 +28,10 @@ exports.LogtoSignInSessionItemSchema = (0, superstruct_1.type)({
24
28
  });
25
29
  class LogtoClient {
26
30
  constructor(logtoConfig, requester = (0, js_1.createRequester)()) {
31
+ this.getOidcConfig = (0, lodash_once_1.default)(this._getOidcConfig);
32
+ this.getJwtVerifyGetKey = (0, lodash_once_1.default)(this._getJwtVerifyGetKey);
27
33
  this.accessTokenMap = new Map();
34
+ this.getAccessTokenPromiseMap = new Map();
28
35
  this.logtoConfig = logtoConfig;
29
36
  this.logtoStorageKey = (0, utils_1.buildLogtoKey)(logtoConfig.clientId);
30
37
  this.requester = requester;
@@ -89,33 +96,28 @@ class LogtoClient {
89
96
  if (accessToken && accessToken.expiresAt > Date.now() / 1000) {
90
97
  return accessToken.token;
91
98
  }
92
- // Token expired, remove it from the map
99
+ // Since the access token has expired, delete it from the map.
93
100
  if (accessToken) {
94
101
  this.accessTokenMap.delete(accessTokenKey);
95
102
  }
96
- // Fetch new access token by refresh token
97
- const { clientId } = this.logtoConfig;
98
- if (!this.refreshToken) {
99
- throw new errors_1.LogtoClientError('not_authenticated');
100
- }
101
- try {
102
- const { tokenEndpoint } = await this.getOidcConfig();
103
- const { accessToken, refreshToken, idToken, scope, expiresIn } = await (0, js_1.fetchTokenByRefreshToken)({ clientId, tokenEndpoint, refreshToken: this.refreshToken, resource }, this.requester);
104
- this.accessTokenMap.set(accessTokenKey, {
105
- token: accessToken,
106
- scope,
107
- expiresAt: Math.round(Date.now() / 1000) + expiresIn,
108
- });
109
- this.refreshToken = refreshToken;
110
- if (idToken) {
111
- await this.verifyIdToken(idToken);
112
- this.idToken = idToken;
113
- }
114
- return accessToken;
115
- }
116
- catch (error) {
117
- throw new errors_1.LogtoClientError('get_access_token_by_refresh_token_failed', error);
103
+ /**
104
+ * Need to fetch a new access token using refresh token.
105
+ * Reuse the cached promise if exists.
106
+ */
107
+ const cachedPromise = this.getAccessTokenPromiseMap.get(accessTokenKey);
108
+ if (cachedPromise) {
109
+ return cachedPromise;
118
110
  }
111
+ /**
112
+ * Create a new promise and cache in map to avoid race condition.
113
+ * Since we enable "refresh token rotation" by default,
114
+ * it will be problematic when calling multiple `getAccessToken()` closely.
115
+ */
116
+ const promise = this.getAccessTokenByRefreshToken(resource);
117
+ this.getAccessTokenPromiseMap.set(accessTokenKey, promise);
118
+ const token = await promise;
119
+ this.getAccessTokenPromiseMap.delete(accessTokenKey);
120
+ return token;
119
121
  }
120
122
  getIdTokenClaims() {
121
123
  if (!this.idToken) {
@@ -202,19 +204,46 @@ class LogtoClient {
202
204
  this.idToken = null;
203
205
  window.location.assign(url);
204
206
  }
205
- async getOidcConfig() {
206
- if (!this.oidcConfig) {
207
- const { endpoint } = this.logtoConfig;
208
- const discoveryEndpoint = (0, utils_1.getDiscoveryEndpoint)(endpoint);
209
- this.oidcConfig = await (0, js_1.fetchOidcConfig)(discoveryEndpoint, this.requester);
207
+ async getAccessTokenByRefreshToken(resource) {
208
+ if (!this.refreshToken) {
209
+ throw new errors_1.LogtoClientError('not_authenticated');
210
+ }
211
+ try {
212
+ const accessTokenKey = (0, utils_1.buildAccessTokenKey)(resource);
213
+ const { clientId } = this.logtoConfig;
214
+ const { tokenEndpoint } = await this.getOidcConfig();
215
+ const { accessToken, refreshToken, idToken, scope, expiresIn } = await (0, js_1.fetchTokenByRefreshToken)({ clientId, tokenEndpoint, refreshToken: this.refreshToken, resource }, this.requester);
216
+ this.accessTokenMap.set(accessTokenKey, {
217
+ token: accessToken,
218
+ scope,
219
+ expiresAt: Math.round(Date.now() / 1000) + expiresIn,
220
+ });
221
+ this.refreshToken = refreshToken;
222
+ if (idToken) {
223
+ await this.verifyIdToken(idToken);
224
+ this.idToken = idToken;
225
+ }
226
+ return accessToken;
210
227
  }
211
- return this.oidcConfig;
228
+ catch (error) {
229
+ throw new errors_1.LogtoClientError('get_access_token_by_refresh_token_failed', error);
230
+ }
231
+ }
232
+ async _getOidcConfig() {
233
+ const { endpoint } = this.logtoConfig;
234
+ const discoveryEndpoint = (0, utils_1.getDiscoveryEndpoint)(endpoint);
235
+ return (0, js_1.fetchOidcConfig)(discoveryEndpoint, this.requester);
236
+ }
237
+ async _getJwtVerifyGetKey() {
238
+ const { jwksUri } = await this.getOidcConfig();
239
+ return (0, jose_1.createRemoteJWKSet)(new URL(jwksUri));
212
240
  }
213
241
  async verifyIdToken(idToken) {
214
242
  const { clientId } = this.logtoConfig;
215
- const { issuer, jwksUri } = await this.getOidcConfig();
243
+ const { issuer } = await this.getOidcConfig();
244
+ const jwtVerifyGetKey = await this.getJwtVerifyGetKey();
216
245
  try {
217
- await (0, js_1.verifyIdToken)(idToken, clientId, issuer, (0, jose_1.createRemoteJWKSet)(new URL(jwksUri)));
246
+ await (0, js_1.verifyIdToken)(idToken, clientId, issuer, jwtVerifyGetKey);
218
247
  }
219
248
  catch (error) {
220
249
  throw new errors_1.LogtoClientError('invalid_id_token', error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logto/browser",
3
- "version": "0.1.2-rc.1",
3
+ "version": "0.1.4",
4
4
  "main": "./lib/index.js",
5
5
  "exports": "./lib/index.js",
6
6
  "typings": "./lib/index.d.ts",
@@ -8,6 +8,11 @@
8
8
  "lib"
9
9
  ],
10
10
  "license": "MIT",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/logto-io/js.git",
14
+ "directory": "packages/browser"
15
+ },
11
16
  "scripts": {
12
17
  "dev:tsc": "tsc -p tsconfig.build.json -w --preserveWatchOutput",
13
18
  "preinstall": "npx only-allow pnpm",
@@ -19,10 +24,11 @@
19
24
  "prepack": "pnpm test && pnpm build"
20
25
  },
21
26
  "dependencies": {
22
- "@logto/js": "^0.1.2-rc.1",
27
+ "@logto/js": "^0.1.3",
23
28
  "@silverhand/essentials": "^1.1.6",
24
29
  "jose": "^4.5.0",
25
30
  "lodash.get": "^4.4.2",
31
+ "lodash.once": "^4.1.1",
26
32
  "superstruct": "^0.15.3"
27
33
  },
28
34
  "devDependencies": {
@@ -31,6 +37,7 @@
31
37
  "@silverhand/ts-config": "^0.9.1",
32
38
  "@types/jest": "^27.4.0",
33
39
  "@types/lodash.get": "^4.4.6",
40
+ "@types/lodash.once": "^4.1.6",
34
41
  "eslint": "^8.9.0",
35
42
  "jest": "^27.5.1",
36
43
  "jest-location-mock": "^1.0.9",
@@ -48,5 +55,5 @@
48
55
  "publishConfig": {
49
56
  "access": "public"
50
57
  },
51
- "gitHead": "8d5713ffeff62c3d12954e7ce194406f260a9546"
58
+ "gitHead": "a5eed81a3e3db3184a53f04f843db8ac707dd3ad"
52
59
  }