@logto/browser 0.1.2 → 0.1.5

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 +23 -12
  2. package/lib/index.js +67 -39
  3. package/package.json +13 -6
package/lib/index.d.ts CHANGED
@@ -1,11 +1,11 @@
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';
5
5
  export * from './errors';
6
6
  export declare type LogtoConfig = {
7
7
  endpoint: string;
8
- clientId: string;
8
+ appId: string;
9
9
  scopes?: string[];
10
10
  resources?: string[];
11
11
  usingPersistStorage?: boolean;
@@ -26,29 +26,40 @@ 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>;
34
- private _refreshToken;
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;
35
44
  private _idToken;
36
45
  constructor(logtoConfig: LogtoConfig, requester?: <T>(input: RequestInfo, init?: RequestInit | undefined) => Promise<T>);
37
46
  get isAuthenticated(): boolean;
38
47
  protected get signInSession(): Nullable<LogtoSignInSessionItem>;
39
48
  protected set signInSession(logtoSignInSessionItem: Nullable<LogtoSignInSessionItem>);
40
- private get refreshToken();
49
+ get refreshToken(): Nullable<string>;
41
50
  private set refreshToken(value);
42
- private get idToken();
51
+ get idToken(): Nullable<string>;
43
52
  private set idToken(value);
44
- getAccessToken(resource?: string): Promise<Nullable<string>>;
53
+ getAccessToken(resource?: string): Promise<string>;
45
54
  getIdTokenClaims(): IdTokenClaims;
46
55
  fetchUserInfo(): Promise<UserInfoResponse>;
47
56
  signIn(redirectUri: string): Promise<void>;
48
57
  isSignInRedirected(url: string): boolean;
49
58
  handleSignInCallback(callbackUri: string): Promise<void>;
50
59
  signOut(postLogoutRedirectUri?: string): Promise<void>;
51
- private getOidcConfig;
60
+ private getAccessTokenByRefreshToken;
61
+ private _getOidcConfig;
62
+ private _getJwtVerifyGetKey;
52
63
  private verifyIdToken;
53
64
  private saveCodeToken;
54
65
  }
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,11 +28,13 @@ 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
- this.logtoStorageKey = (0, utils_1.buildLogtoKey)(logtoConfig.clientId);
36
+ this.logtoStorageKey = (0, utils_1.buildLogtoKey)(logtoConfig.appId);
30
37
  this.requester = requester;
31
- this._refreshToken = localStorage.getItem((0, utils_1.buildRefreshTokenKey)(this.logtoStorageKey));
32
38
  this._idToken = localStorage.getItem((0, utils_1.buildIdTokenKey)(this.logtoStorageKey));
33
39
  }
34
40
  get isAuthenticated() {
@@ -57,10 +63,9 @@ class LogtoClient {
57
63
  sessionStorage.setItem(this.logtoStorageKey, jsonItem);
58
64
  }
59
65
  get refreshToken() {
60
- return this._refreshToken;
66
+ return localStorage.getItem((0, utils_1.buildRefreshTokenKey)(this.logtoStorageKey));
61
67
  }
62
68
  set refreshToken(refreshToken) {
63
- this._refreshToken = refreshToken;
64
69
  const refreshTokenKey = (0, utils_1.buildRefreshTokenKey)(this.logtoStorageKey);
65
70
  if (!refreshToken) {
66
71
  localStorage.removeItem(refreshTokenKey);
@@ -89,33 +94,28 @@ class LogtoClient {
89
94
  if (accessToken && accessToken.expiresAt > Date.now() / 1000) {
90
95
  return accessToken.token;
91
96
  }
92
- // Token expired, remove it from the map
97
+ // Since the access token has expired, delete it from the map.
93
98
  if (accessToken) {
94
99
  this.accessTokenMap.delete(accessTokenKey);
95
100
  }
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);
101
+ /**
102
+ * Need to fetch a new access token using refresh token.
103
+ * Reuse the cached promise if exists.
104
+ */
105
+ const cachedPromise = this.getAccessTokenPromiseMap.get(accessTokenKey);
106
+ if (cachedPromise) {
107
+ return cachedPromise;
118
108
  }
109
+ /**
110
+ * Create a new promise and cache in map to avoid race condition.
111
+ * Since we enable "refresh token rotation" by default,
112
+ * it will be problematic when calling multiple `getAccessToken()` closely.
113
+ */
114
+ const promise = this.getAccessTokenByRefreshToken(resource);
115
+ this.getAccessTokenPromiseMap.set(accessTokenKey, promise);
116
+ const token = await promise;
117
+ this.getAccessTokenPromiseMap.delete(accessTokenKey);
118
+ return token;
119
119
  }
120
120
  getIdTokenClaims() {
121
121
  if (!this.idToken) {
@@ -132,7 +132,7 @@ class LogtoClient {
132
132
  return (0, js_1.fetchUserInfo)(userinfoEndpoint, accessToken, this.requester);
133
133
  }
134
134
  async signIn(redirectUri) {
135
- const { clientId, resources, scopes: customScopes } = this.logtoConfig;
135
+ const { appId: clientId, resources, scopes: customScopes } = this.logtoConfig;
136
136
  const { authorizationEndpoint } = await this.getOidcConfig();
137
137
  const codeVerifier = (0, js_1.generateCodeVerifier)();
138
138
  const codeChallenge = await (0, js_1.generateCodeChallenge)(codeVerifier);
@@ -166,7 +166,7 @@ class LogtoClient {
166
166
  }
167
167
  const { redirectUri, state, codeVerifier } = signInSession;
168
168
  const code = (0, js_1.verifyAndParseCodeFromCallbackUri)(callbackUri, redirectUri, state);
169
- const { clientId } = logtoConfig;
169
+ const { appId: clientId } = logtoConfig;
170
170
  const { tokenEndpoint } = await this.getOidcConfig();
171
171
  const codeTokenResponse = await (0, js_1.fetchTokenByAuthorizationCode)({
172
172
  clientId,
@@ -177,12 +177,13 @@ class LogtoClient {
177
177
  }, requester);
178
178
  await this.verifyIdToken(codeTokenResponse.idToken);
179
179
  this.saveCodeToken(codeTokenResponse);
180
+ this.signInSession = null;
180
181
  }
181
182
  async signOut(postLogoutRedirectUri) {
182
183
  if (!this.idToken) {
183
184
  throw new errors_1.LogtoClientError('not_authenticated');
184
185
  }
185
- const { clientId } = this.logtoConfig;
186
+ const { appId: clientId } = this.logtoConfig;
186
187
  const { endSessionEndpoint, revocationEndpoint } = await this.getOidcConfig();
187
188
  if (this.refreshToken) {
188
189
  try {
@@ -202,19 +203,46 @@ class LogtoClient {
202
203
  this.idToken = null;
203
204
  window.location.assign(url);
204
205
  }
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);
206
+ async getAccessTokenByRefreshToken(resource) {
207
+ if (!this.refreshToken) {
208
+ throw new errors_1.LogtoClientError('not_authenticated');
210
209
  }
211
- return this.oidcConfig;
210
+ try {
211
+ const accessTokenKey = (0, utils_1.buildAccessTokenKey)(resource);
212
+ const { appId: clientId } = this.logtoConfig;
213
+ const { tokenEndpoint } = await this.getOidcConfig();
214
+ const { accessToken, refreshToken, idToken, scope, expiresIn } = await (0, js_1.fetchTokenByRefreshToken)({ clientId, tokenEndpoint, refreshToken: this.refreshToken, resource }, this.requester);
215
+ this.accessTokenMap.set(accessTokenKey, {
216
+ token: accessToken,
217
+ scope,
218
+ expiresAt: Math.round(Date.now() / 1000) + expiresIn,
219
+ });
220
+ this.refreshToken = refreshToken;
221
+ if (idToken) {
222
+ await this.verifyIdToken(idToken);
223
+ this.idToken = idToken;
224
+ }
225
+ return accessToken;
226
+ }
227
+ catch (error) {
228
+ throw new errors_1.LogtoClientError('get_access_token_by_refresh_token_failed', error);
229
+ }
230
+ }
231
+ async _getOidcConfig() {
232
+ const { endpoint } = this.logtoConfig;
233
+ const discoveryEndpoint = (0, utils_1.getDiscoveryEndpoint)(endpoint);
234
+ return (0, js_1.fetchOidcConfig)(discoveryEndpoint, this.requester);
235
+ }
236
+ async _getJwtVerifyGetKey() {
237
+ const { jwksUri } = await this.getOidcConfig();
238
+ return (0, jose_1.createRemoteJWKSet)(new URL(jwksUri));
212
239
  }
213
240
  async verifyIdToken(idToken) {
214
- const { clientId } = this.logtoConfig;
215
- const { issuer, jwksUri } = await this.getOidcConfig();
241
+ const { appId } = this.logtoConfig;
242
+ const { issuer } = await this.getOidcConfig();
243
+ const jwtVerifyGetKey = await this.getJwtVerifyGetKey();
216
244
  try {
217
- await (0, js_1.verifyIdToken)(idToken, clientId, issuer, (0, jose_1.createRemoteJWKSet)(new URL(jwksUri)));
245
+ await (0, js_1.verifyIdToken)(idToken, appId, issuer, jwtVerifyGetKey);
218
246
  }
219
247
  catch (error) {
220
248
  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",
3
+ "version": "0.1.5",
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",
@@ -16,21 +21,23 @@
16
21
  "lint": "eslint --ext .ts src",
17
22
  "test": "jest",
18
23
  "test:coverage": "jest --silent --coverage",
19
- "prepack": "pnpm test && pnpm build"
24
+ "prepack": "pnpm test"
20
25
  },
21
26
  "dependencies": {
22
- "@logto/js": "^0.1.2",
27
+ "@logto/js": "^0.1.5",
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": {
29
35
  "@jest/types": "^27.5.1",
30
- "@silverhand/eslint-config": "^0.9.1",
31
- "@silverhand/ts-config": "^0.9.1",
36
+ "@silverhand/eslint-config": "^0.10.0",
37
+ "@silverhand/ts-config": "^0.10.0",
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": "74bd01ec73a5215b26a96f426b874de9ad474dae"
58
+ "gitHead": "ed98d55270ae923f95a57fe4f3bc5a5959518c06"
52
59
  }