@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.
- package/lib/index.d.ts +23 -12
- package/lib/index.js +67 -39
- package/package.json +13 -6
package/lib/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { IdTokenClaims,
|
|
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
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
49
|
+
get refreshToken(): Nullable<string>;
|
|
41
50
|
private set refreshToken(value);
|
|
42
|
-
|
|
51
|
+
get idToken(): Nullable<string>;
|
|
43
52
|
private set idToken(value);
|
|
44
|
-
getAccessToken(resource?: string): Promise<
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
206
|
-
if (!this.
|
|
207
|
-
|
|
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
|
-
|
|
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 {
|
|
215
|
-
const { issuer
|
|
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,
|
|
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.
|
|
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
|
|
24
|
+
"prepack": "pnpm test"
|
|
20
25
|
},
|
|
21
26
|
"dependencies": {
|
|
22
|
-
"@logto/js": "^0.1.
|
|
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.
|
|
31
|
-
"@silverhand/ts-config": "^0.
|
|
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": "
|
|
58
|
+
"gitHead": "ed98d55270ae923f95a57fe4f3bc5a5959518c06"
|
|
52
59
|
}
|