@qlover/oauth-wrapper 0.2.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/CHANGELOG.md +27 -0
- package/README.md +313 -0
- package/README_EN.md +313 -0
- package/dist/client.cjs +835 -0
- package/dist/client.d.ts +341 -0
- package/dist/client.js +791 -0
- package/dist/core.cjs +301 -0
- package/dist/core.d.ts +460 -0
- package/dist/core.js +250 -0
- package/dist/index.cjs +1043 -0
- package/dist/index.d.ts +194 -0
- package/dist/index.js +977 -0
- package/package.json +72 -0
package/dist/client.cjs
ADDED
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/client/index.ts
|
|
21
|
+
var client_exports = {};
|
|
22
|
+
__export(client_exports, {
|
|
23
|
+
DEFAULT_OAUTH_AUTHORIZE_PATH: () => DEFAULT_OAUTH_AUTHORIZE_PATH,
|
|
24
|
+
DEFAULT_PKCE_STORAGE_KEY: () => DEFAULT_PKCE_STORAGE_KEY,
|
|
25
|
+
OAuthClient: () => OAuthClient,
|
|
26
|
+
OAuthGateway: () => OAuthGateway,
|
|
27
|
+
OAuthTokenErrorSchema: () => OAuthTokenErrorSchema,
|
|
28
|
+
OAuthTokenResponseSchema: () => OAuthTokenResponseSchema,
|
|
29
|
+
OAuthUserInfoSchema: () => OAuthUserInfoSchema,
|
|
30
|
+
OAuthWrapperEndpoints: () => OAuthWrapperEndpoints,
|
|
31
|
+
PKCESessionStore: () => PKCESessionStore,
|
|
32
|
+
buildAuthorizeSearchParams: () => buildAuthorizeSearchParams,
|
|
33
|
+
buildOAuthAuthorizeUrl: () => buildOAuthAuthorizeUrl,
|
|
34
|
+
buildOAuthLocaleHeaders: () => buildOAuthLocaleHeaders,
|
|
35
|
+
buildOAuthRedirectUri: () => buildOAuthRedirectUri,
|
|
36
|
+
defaultOAuthClientConfig: () => defaultOAuthClientConfig,
|
|
37
|
+
formatOAuthAuthorizeUrl: () => formatOAuthAuthorizeUrl,
|
|
38
|
+
mergeOAuthAuthorizationConfig: () => mergeOAuthAuthorizationConfig,
|
|
39
|
+
parseOAuthTokenError: () => parseOAuthTokenError,
|
|
40
|
+
parseOAuthTokenResponse: () => parseOAuthTokenResponse,
|
|
41
|
+
parseOAuthUserInfoResponse: () => parseOAuthUserInfoResponse,
|
|
42
|
+
resolveOAuthClientConfig: () => resolveOAuthClientConfig
|
|
43
|
+
});
|
|
44
|
+
module.exports = __toCommonJS(client_exports);
|
|
45
|
+
|
|
46
|
+
// src/client/interface/OAuthConfig.ts
|
|
47
|
+
var OAuthWrapperEndpoints = {
|
|
48
|
+
authorize: "/oauth/authorize",
|
|
49
|
+
token: "/oauth/token",
|
|
50
|
+
revoke: "/oauth/revoke",
|
|
51
|
+
userinfo: "/userinfo"
|
|
52
|
+
};
|
|
53
|
+
var DEFAULT_OAUTH_AUTHORIZE_PATH = OAuthWrapperEndpoints.authorize;
|
|
54
|
+
var DEFAULT_PKCE_STORAGE_KEY = "oauth-wrapper-pkcesession";
|
|
55
|
+
var defaultOAuthClientConfig = {
|
|
56
|
+
locale: "en",
|
|
57
|
+
localeIn: "path",
|
|
58
|
+
localeQueryParam: "local",
|
|
59
|
+
localeHeader: "Accept-Language",
|
|
60
|
+
serverUrl: "",
|
|
61
|
+
clientId: "",
|
|
62
|
+
scope: "openid profile email",
|
|
63
|
+
redirectPath: "oauth/callback"
|
|
64
|
+
};
|
|
65
|
+
function resolveOAuthClientConfig(partial) {
|
|
66
|
+
return {
|
|
67
|
+
...defaultOAuthClientConfig,
|
|
68
|
+
...partial
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/client/OAuthGateway.ts
|
|
73
|
+
var import_fe_corekit = require("@qlover/fe-corekit");
|
|
74
|
+
|
|
75
|
+
// src/client/oauthUrlUtils.ts
|
|
76
|
+
function resolveLocaleIn(options) {
|
|
77
|
+
if (options.localeIn) {
|
|
78
|
+
return options.localeIn;
|
|
79
|
+
}
|
|
80
|
+
return options.locale ? "path" : "none";
|
|
81
|
+
}
|
|
82
|
+
function buildAuthorizeSearchParams(input) {
|
|
83
|
+
const params = new URLSearchParams({
|
|
84
|
+
response_type: "code",
|
|
85
|
+
client_id: input.clientId,
|
|
86
|
+
redirect_uri: input.redirectUri,
|
|
87
|
+
scope: input.scope,
|
|
88
|
+
state: input.state,
|
|
89
|
+
code_challenge: input.codeChallenge,
|
|
90
|
+
code_challenge_method: "S256"
|
|
91
|
+
});
|
|
92
|
+
return params;
|
|
93
|
+
}
|
|
94
|
+
function mergeOAuthAuthorizationConfig(params, baseConfig) {
|
|
95
|
+
const serverUrl = (params?.serverUrl ?? baseConfig?.serverUrl)?.trim();
|
|
96
|
+
const clientId = (params?.clientId ?? baseConfig?.clientId)?.trim();
|
|
97
|
+
if (!serverUrl || !clientId) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
serverUrl: serverUrl.replace(/\/$/, ""),
|
|
102
|
+
clientId,
|
|
103
|
+
scope: params?.scope?.trim() || baseConfig?.scope || defaultOAuthClientConfig.scope,
|
|
104
|
+
redirectPath: params?.redirectPath?.trim() || baseConfig?.redirectPath || defaultOAuthClientConfig.redirectPath
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function buildOAuthRedirectUri(redirectPath, options = {}) {
|
|
108
|
+
const origin = (options.origin ?? (typeof window !== "undefined" ? window.location.origin : "")).replace(/\/$/, "");
|
|
109
|
+
const prefix = (options.routerPrefix ?? "").replace(/\/$/, "");
|
|
110
|
+
const path = redirectPath.replace(/^\//, "");
|
|
111
|
+
const localeIn = resolveLocaleIn(options);
|
|
112
|
+
const segments = [
|
|
113
|
+
prefix,
|
|
114
|
+
localeIn === "path" && options.locale ? options.locale : "",
|
|
115
|
+
path
|
|
116
|
+
].filter(Boolean);
|
|
117
|
+
let url = `${origin}/${segments.join("/")}`.replace(/([^:]\/)\/+/g, "$1");
|
|
118
|
+
if (options.locale && localeIn === "query") {
|
|
119
|
+
const param = options.localeQueryParam ?? "locale";
|
|
120
|
+
const joiner = url.includes("?") ? "&" : "?";
|
|
121
|
+
url = `${url}${joiner}${param}=${encodeURIComponent(options.locale)}`;
|
|
122
|
+
}
|
|
123
|
+
return url;
|
|
124
|
+
}
|
|
125
|
+
function formatOAuthAuthorizeUrl(serverUrl, authorizePath, searchParams, localeOptions = {}) {
|
|
126
|
+
const base = serverUrl.replace(/\/$/, "");
|
|
127
|
+
const path = authorizePath.startsWith("/") ? authorizePath : `/${authorizePath}`;
|
|
128
|
+
const localeIn = resolveLocaleIn(localeOptions);
|
|
129
|
+
const url = localeOptions.locale && localeIn === "path" ? `${base}/${localeOptions.locale}${path}` : `${base}${path}`;
|
|
130
|
+
const params = new URLSearchParams(searchParams);
|
|
131
|
+
if (localeOptions.locale && localeIn === "query") {
|
|
132
|
+
params.set(
|
|
133
|
+
localeOptions.localeQueryParam ?? "locale",
|
|
134
|
+
localeOptions.locale
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const qs = params.toString();
|
|
138
|
+
return qs ? `${url}?${qs}` : url;
|
|
139
|
+
}
|
|
140
|
+
function buildOAuthAuthorizeUrl(input) {
|
|
141
|
+
const { config, authorizePath, state, codeChallenge, redirectPath, scope } = input;
|
|
142
|
+
const redirectUri = buildOAuthRedirectUri(
|
|
143
|
+
redirectPath ?? config.redirectPath,
|
|
144
|
+
input
|
|
145
|
+
);
|
|
146
|
+
const searchParams = buildAuthorizeSearchParams({
|
|
147
|
+
clientId: config.clientId,
|
|
148
|
+
redirectUri,
|
|
149
|
+
scope: scope ?? config.scope,
|
|
150
|
+
state,
|
|
151
|
+
codeChallenge
|
|
152
|
+
});
|
|
153
|
+
return formatOAuthAuthorizeUrl(
|
|
154
|
+
config.serverUrl,
|
|
155
|
+
authorizePath,
|
|
156
|
+
searchParams,
|
|
157
|
+
input
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
function buildOAuthLocaleHeaders(options = {}) {
|
|
161
|
+
if (!options.locale || options.localeIn !== "header") {
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
const header = options.localeHeader ?? "Accept-Language";
|
|
165
|
+
return { [header]: options.locale };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/client/parseEnvelope.ts
|
|
169
|
+
var import_zod2 = require("zod");
|
|
170
|
+
|
|
171
|
+
// src/client/types.ts
|
|
172
|
+
var import_zod = require("zod");
|
|
173
|
+
var OAuthUserInfoSchema = import_zod.z.object({
|
|
174
|
+
sub: import_zod.z.string(),
|
|
175
|
+
email: import_zod.z.email(),
|
|
176
|
+
name: import_zod.z.string(),
|
|
177
|
+
roles: import_zod.z.array(import_zod.z.string()).optional()
|
|
178
|
+
});
|
|
179
|
+
var OAuthTokenResponseSchema = import_zod.z.object({
|
|
180
|
+
access_token: import_zod.z.string(),
|
|
181
|
+
token_type: import_zod.z.literal("Bearer").optional(),
|
|
182
|
+
expires_in: import_zod.z.number().optional(),
|
|
183
|
+
refresh_token: import_zod.z.string().optional(),
|
|
184
|
+
scope: import_zod.z.string().optional()
|
|
185
|
+
});
|
|
186
|
+
var OAuthTokenErrorSchema = import_zod.z.object({
|
|
187
|
+
error: import_zod.z.string(),
|
|
188
|
+
error_description: import_zod.z.string().optional(),
|
|
189
|
+
error_id: import_zod.z.string().optional()
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// src/client/parseEnvelope.ts
|
|
193
|
+
var OAuthAppApiSuccessSchema = (dataSchema) => import_zod2.z.object({
|
|
194
|
+
success: import_zod2.z.literal(true),
|
|
195
|
+
requestId: import_zod2.z.string().optional(),
|
|
196
|
+
data: dataSchema
|
|
197
|
+
});
|
|
198
|
+
var OAuthAppApiErrorSchema = import_zod2.z.object({
|
|
199
|
+
success: import_zod2.z.literal(false),
|
|
200
|
+
id: import_zod2.z.string().optional(),
|
|
201
|
+
message: import_zod2.z.string().optional(),
|
|
202
|
+
requestId: import_zod2.z.string().optional()
|
|
203
|
+
});
|
|
204
|
+
function formatAppApiError(json, fallback) {
|
|
205
|
+
const wrapped = OAuthAppApiErrorSchema.safeParse(json);
|
|
206
|
+
if (wrapped.success) {
|
|
207
|
+
return wrapped.data.message?.trim() || wrapped.data.id || fallback;
|
|
208
|
+
}
|
|
209
|
+
const oauth = OAuthTokenErrorSchema.safeParse(json);
|
|
210
|
+
if (oauth.success) {
|
|
211
|
+
return oauth.data.error_description?.trim() || oauth.data.error;
|
|
212
|
+
}
|
|
213
|
+
return fallback;
|
|
214
|
+
}
|
|
215
|
+
function parseOAuthTokenResponse(json) {
|
|
216
|
+
const direct = OAuthTokenResponseSchema.safeParse(json);
|
|
217
|
+
if (direct.success) {
|
|
218
|
+
return direct.data;
|
|
219
|
+
}
|
|
220
|
+
const wrapped = OAuthAppApiSuccessSchema(OAuthTokenResponseSchema).safeParse(
|
|
221
|
+
json
|
|
222
|
+
);
|
|
223
|
+
if (wrapped.success) {
|
|
224
|
+
return wrapped.data.data;
|
|
225
|
+
}
|
|
226
|
+
throw new Error(
|
|
227
|
+
formatAppApiError(json, "Invalid token response from authorization server")
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
function parseOAuthTokenError(json, fallback) {
|
|
231
|
+
return formatAppApiError(json, fallback);
|
|
232
|
+
}
|
|
233
|
+
function parseOAuthUserInfoResponse(json) {
|
|
234
|
+
const direct = OAuthUserInfoSchema.safeParse(json);
|
|
235
|
+
if (direct.success) {
|
|
236
|
+
return direct.data;
|
|
237
|
+
}
|
|
238
|
+
const wrapped = OAuthAppApiSuccessSchema(OAuthUserInfoSchema).safeParse(json);
|
|
239
|
+
if (wrapped.success) {
|
|
240
|
+
return wrapped.data.data;
|
|
241
|
+
}
|
|
242
|
+
throw new Error(
|
|
243
|
+
formatAppApiError(
|
|
244
|
+
json,
|
|
245
|
+
"Invalid userinfo response from authorization server"
|
|
246
|
+
)
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/core/utils/pkce.ts
|
|
251
|
+
var PKCE_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
252
|
+
function generatePkceVerifier(length = 64) {
|
|
253
|
+
const size = Math.min(128, Math.max(43, length));
|
|
254
|
+
const values = new Uint8Array(size);
|
|
255
|
+
crypto.getRandomValues(values);
|
|
256
|
+
let result = "";
|
|
257
|
+
for (let i = 0; i < size; i++) {
|
|
258
|
+
result += PKCE_CHARSET[values[i] % PKCE_CHARSET.length];
|
|
259
|
+
}
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
function base64UrlEncode(buffer) {
|
|
263
|
+
const bytes = new Uint8Array(buffer);
|
|
264
|
+
let binary = "";
|
|
265
|
+
for (const byte of bytes) {
|
|
266
|
+
binary += String.fromCharCode(byte);
|
|
267
|
+
}
|
|
268
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
269
|
+
}
|
|
270
|
+
async function computePkceS256Challenge(verifier) {
|
|
271
|
+
const data = new TextEncoder().encode(verifier);
|
|
272
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
273
|
+
return base64UrlEncode(digest);
|
|
274
|
+
}
|
|
275
|
+
function randomOAuthState() {
|
|
276
|
+
const bytes = new Uint8Array(16);
|
|
277
|
+
crypto.getRandomValues(bytes);
|
|
278
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/client/OAuthGateway.ts
|
|
282
|
+
function parseCallbackParams(params) {
|
|
283
|
+
if (params instanceof URLSearchParams) {
|
|
284
|
+
return {
|
|
285
|
+
code: params.get("code") ?? void 0,
|
|
286
|
+
state: params.get("state") ?? void 0,
|
|
287
|
+
error: params.get("error") ?? void 0,
|
|
288
|
+
error_description: params.get("error_description") ?? void 0
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return params ?? {};
|
|
292
|
+
}
|
|
293
|
+
function callbackDedupeKey(params) {
|
|
294
|
+
if (!params.code || !params.state) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
return `${params.code}:${params.state}`;
|
|
298
|
+
}
|
|
299
|
+
var OAuthGateway = class {
|
|
300
|
+
constructor(options) {
|
|
301
|
+
this.options = options;
|
|
302
|
+
}
|
|
303
|
+
callbackInflight = /* @__PURE__ */ new Map();
|
|
304
|
+
get localeOptions() {
|
|
305
|
+
return {
|
|
306
|
+
locale: this.options.locale,
|
|
307
|
+
localeIn: this.options.localeIn,
|
|
308
|
+
localeQueryParam: this.options.localeQueryParam,
|
|
309
|
+
localeHeader: this.options.localeHeader
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
get authorizePath() {
|
|
313
|
+
return this.options.authorizePath ?? DEFAULT_OAUTH_AUTHORIZE_PATH;
|
|
314
|
+
}
|
|
315
|
+
get urlOptions() {
|
|
316
|
+
return {
|
|
317
|
+
origin: this.options.origin,
|
|
318
|
+
routerPrefix: this.options.routerPrefix
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
getServerUrl() {
|
|
322
|
+
const serverUrl = this.options.config?.serverUrl;
|
|
323
|
+
if (!serverUrl) {
|
|
324
|
+
throw new Error("OAuth is not configured");
|
|
325
|
+
}
|
|
326
|
+
return serverUrl;
|
|
327
|
+
}
|
|
328
|
+
buildRequestHeaders(headers) {
|
|
329
|
+
return {
|
|
330
|
+
...buildOAuthLocaleHeaders(this.localeOptions),
|
|
331
|
+
...headers
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
async requestJson(path, init, errorFallback) {
|
|
335
|
+
const requester = this.options.requester;
|
|
336
|
+
if (requester) {
|
|
337
|
+
const response = await requester.request({
|
|
338
|
+
url: path,
|
|
339
|
+
method: init.method,
|
|
340
|
+
headers: init.headers,
|
|
341
|
+
data: init.body
|
|
342
|
+
});
|
|
343
|
+
if (response.status < 200 || response.status >= 300) {
|
|
344
|
+
throw new Error(parseOAuthTokenError(response.data, errorFallback));
|
|
345
|
+
}
|
|
346
|
+
return response.data;
|
|
347
|
+
}
|
|
348
|
+
const res = await fetch(`${this.getServerUrl()}${path}`, {
|
|
349
|
+
method: init.method,
|
|
350
|
+
headers: init.headers,
|
|
351
|
+
body: init.body
|
|
352
|
+
});
|
|
353
|
+
const json = await res.json();
|
|
354
|
+
if (!res.ok) {
|
|
355
|
+
throw new Error(parseOAuthTokenError(json, errorFallback));
|
|
356
|
+
}
|
|
357
|
+
return json;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* @override
|
|
361
|
+
*/
|
|
362
|
+
async authorize(paramsOrUrl) {
|
|
363
|
+
if (typeof paramsOrUrl === "string") {
|
|
364
|
+
window.location.assign(paramsOrUrl);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const config = mergeOAuthAuthorizationConfig(
|
|
368
|
+
paramsOrUrl,
|
|
369
|
+
this.options.config
|
|
370
|
+
);
|
|
371
|
+
if (!config) {
|
|
372
|
+
throw new Error("OAuth is not configured");
|
|
373
|
+
}
|
|
374
|
+
const session = this.options.store.loadPkceSession();
|
|
375
|
+
if (!session) {
|
|
376
|
+
throw new Error("PKCE session is required before authorize");
|
|
377
|
+
}
|
|
378
|
+
const url = buildOAuthAuthorizeUrl({
|
|
379
|
+
config,
|
|
380
|
+
authorizePath: this.authorizePath,
|
|
381
|
+
redirectPath: paramsOrUrl.redirectPath,
|
|
382
|
+
scope: paramsOrUrl.scope,
|
|
383
|
+
state: session.state,
|
|
384
|
+
codeChallenge: await computePkceS256Challenge(session.codeVerifier),
|
|
385
|
+
...this.urlOptions,
|
|
386
|
+
...this.localeOptions
|
|
387
|
+
});
|
|
388
|
+
if (paramsOrUrl.target === "_blank") {
|
|
389
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
window.location.assign(url);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* @override
|
|
396
|
+
*/
|
|
397
|
+
async getToken(request) {
|
|
398
|
+
const body = new URLSearchParams({
|
|
399
|
+
grant_type: request.grant_type,
|
|
400
|
+
code: request.code,
|
|
401
|
+
redirect_uri: request.redirect_uri,
|
|
402
|
+
client_id: request.client_id,
|
|
403
|
+
code_verifier: request.code_verifier
|
|
404
|
+
});
|
|
405
|
+
const json = await this.requestJson(
|
|
406
|
+
OAuthWrapperEndpoints.token,
|
|
407
|
+
{
|
|
408
|
+
method: import_fe_corekit.HttpMethods.POST,
|
|
409
|
+
headers: this.buildRequestHeaders({
|
|
410
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
411
|
+
}),
|
|
412
|
+
body: body.toString()
|
|
413
|
+
},
|
|
414
|
+
"Token exchange failed"
|
|
415
|
+
);
|
|
416
|
+
return parseOAuthTokenResponse(json);
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* @override
|
|
420
|
+
*/
|
|
421
|
+
async getUserInfo(accessToken) {
|
|
422
|
+
const json = await this.requestJson(
|
|
423
|
+
OAuthWrapperEndpoints.userinfo,
|
|
424
|
+
{
|
|
425
|
+
method: import_fe_corekit.HttpMethods.GET,
|
|
426
|
+
headers: this.buildRequestHeaders({
|
|
427
|
+
Authorization: `Bearer ${accessToken}`
|
|
428
|
+
})
|
|
429
|
+
},
|
|
430
|
+
"Failed to fetch userinfo"
|
|
431
|
+
);
|
|
432
|
+
return parseOAuthUserInfoResponse(json);
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* @override
|
|
436
|
+
*/
|
|
437
|
+
async oAuthWrapperCallback(params) {
|
|
438
|
+
const parsed = parseCallbackParams(params);
|
|
439
|
+
const dedupeKey = callbackDedupeKey(parsed);
|
|
440
|
+
if (dedupeKey) {
|
|
441
|
+
const inflight = this.callbackInflight.get(dedupeKey);
|
|
442
|
+
if (inflight) {
|
|
443
|
+
return inflight;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const promise = this.runOAuthWrapperCallback(parsed);
|
|
447
|
+
if (dedupeKey) {
|
|
448
|
+
this.callbackInflight.set(dedupeKey, promise);
|
|
449
|
+
promise.finally(() => {
|
|
450
|
+
this.callbackInflight.delete(dedupeKey);
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
return promise;
|
|
454
|
+
}
|
|
455
|
+
async runOAuthWrapperCallback(params) {
|
|
456
|
+
if (params.error) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
params.error_description?.trim() || params.error || "Authorization denied"
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
if (!params.code || !params.state) {
|
|
462
|
+
throw new Error("Missing authorization code or state");
|
|
463
|
+
}
|
|
464
|
+
const store = this.options.store;
|
|
465
|
+
const session = store.loadPkceSession();
|
|
466
|
+
if (!session) {
|
|
467
|
+
throw new Error("OAuth session expired; please sign in again");
|
|
468
|
+
}
|
|
469
|
+
if (session.state !== params.state) {
|
|
470
|
+
store.clearPkceSession();
|
|
471
|
+
throw new Error("Invalid state (possible CSRF)");
|
|
472
|
+
}
|
|
473
|
+
const currentLocale = this.options.locale;
|
|
474
|
+
if (session.locale != null && currentLocale != null && session.locale !== currentLocale) {
|
|
475
|
+
store.clearPkceSession();
|
|
476
|
+
throw new Error("Locale mismatch during OAuth callback");
|
|
477
|
+
}
|
|
478
|
+
const config = this.options.config;
|
|
479
|
+
if (!config) {
|
|
480
|
+
throw new Error("OAuth is not configured");
|
|
481
|
+
}
|
|
482
|
+
const redirectUri = buildOAuthRedirectUri(
|
|
483
|
+
config.redirectPath ?? defaultOAuthClientConfig.redirectPath,
|
|
484
|
+
{ ...this.urlOptions, ...this.localeOptions }
|
|
485
|
+
);
|
|
486
|
+
try {
|
|
487
|
+
const tokens = await this.getToken({
|
|
488
|
+
grant_type: "authorization_code",
|
|
489
|
+
code: params.code,
|
|
490
|
+
redirect_uri: redirectUri,
|
|
491
|
+
client_id: config.clientId,
|
|
492
|
+
code_verifier: session.codeVerifier
|
|
493
|
+
});
|
|
494
|
+
const userinfo = await this.getUserInfo(tokens.access_token);
|
|
495
|
+
return this.options.mapUser(
|
|
496
|
+
userinfo,
|
|
497
|
+
tokens.access_token,
|
|
498
|
+
tokens.refresh_token
|
|
499
|
+
);
|
|
500
|
+
} finally {
|
|
501
|
+
store.clearPkceSession();
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* @override
|
|
506
|
+
*/
|
|
507
|
+
async revokeToken(options) {
|
|
508
|
+
const config = this.options.config;
|
|
509
|
+
if (!config || !options?.refreshToken) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const body = new URLSearchParams({
|
|
513
|
+
token: options.refreshToken,
|
|
514
|
+
token_type_hint: "refresh_token",
|
|
515
|
+
client_id: config.clientId
|
|
516
|
+
});
|
|
517
|
+
await this.postRevokeToken(body);
|
|
518
|
+
}
|
|
519
|
+
async postRevokeToken(body) {
|
|
520
|
+
const path = OAuthWrapperEndpoints.revoke;
|
|
521
|
+
const headers = this.buildRequestHeaders({
|
|
522
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
523
|
+
});
|
|
524
|
+
const requester = this.options.requester;
|
|
525
|
+
if (requester) {
|
|
526
|
+
await requester.request({
|
|
527
|
+
url: path,
|
|
528
|
+
method: import_fe_corekit.HttpMethods.POST,
|
|
529
|
+
headers,
|
|
530
|
+
data: body.toString()
|
|
531
|
+
});
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
await fetch(`${this.getServerUrl()}${path}`, {
|
|
535
|
+
method: import_fe_corekit.HttpMethods.POST,
|
|
536
|
+
headers,
|
|
537
|
+
body: body.toString()
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// src/client/SessionStorage.ts
|
|
543
|
+
var sessionStorage = {
|
|
544
|
+
setItem(key, value) {
|
|
545
|
+
window.sessionStorage.setItem(key, value);
|
|
546
|
+
},
|
|
547
|
+
getItem(key, defaultValue) {
|
|
548
|
+
return window.sessionStorage.getItem(key) ?? defaultValue;
|
|
549
|
+
},
|
|
550
|
+
removeItem(key) {
|
|
551
|
+
window.sessionStorage.removeItem(key);
|
|
552
|
+
},
|
|
553
|
+
clear() {
|
|
554
|
+
window.sessionStorage.clear();
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
// src/client/PKCESessionStore.ts
|
|
559
|
+
function createDefaultPkceStorage() {
|
|
560
|
+
return {
|
|
561
|
+
setItem(key, value) {
|
|
562
|
+
sessionStorage.setItem(key, JSON.stringify(value));
|
|
563
|
+
},
|
|
564
|
+
getItem(key) {
|
|
565
|
+
const raw = sessionStorage.getItem(key);
|
|
566
|
+
if (!raw) {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
try {
|
|
570
|
+
const parsed = JSON.parse(raw);
|
|
571
|
+
if (typeof parsed.state === "string" && typeof parsed.codeVerifier === "string" && (parsed.locale === void 0 || typeof parsed.locale === "string")) {
|
|
572
|
+
return parsed;
|
|
573
|
+
}
|
|
574
|
+
} catch {
|
|
575
|
+
}
|
|
576
|
+
return null;
|
|
577
|
+
},
|
|
578
|
+
removeItem(key) {
|
|
579
|
+
sessionStorage.removeItem(key);
|
|
580
|
+
},
|
|
581
|
+
clear() {
|
|
582
|
+
sessionStorage.clear();
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
var PKCESessionStore = class {
|
|
587
|
+
pkceStorage;
|
|
588
|
+
pkceStorageKey;
|
|
589
|
+
constructor(options) {
|
|
590
|
+
this.pkceStorage = options?.pkceStorage ?? createDefaultPkceStorage();
|
|
591
|
+
this.pkceStorageKey = options?.pkceStorageKey ?? DEFAULT_PKCE_STORAGE_KEY;
|
|
592
|
+
}
|
|
593
|
+
savePkceSession(session) {
|
|
594
|
+
this.pkceStorage.setItem(this.pkceStorageKey, session);
|
|
595
|
+
}
|
|
596
|
+
loadPkceSession() {
|
|
597
|
+
return this.pkceStorage.getItem(this.pkceStorageKey, null);
|
|
598
|
+
}
|
|
599
|
+
clearPkceSession() {
|
|
600
|
+
this.pkceStorage.removeItem(this.pkceStorageKey);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
// src/client/OAuthClient.ts
|
|
605
|
+
var OAUTH_CLIENT_CONFIG_KEYS = [
|
|
606
|
+
"serverUrl",
|
|
607
|
+
"clientId",
|
|
608
|
+
"scope",
|
|
609
|
+
"redirectPath",
|
|
610
|
+
"origin",
|
|
611
|
+
"routerPrefix",
|
|
612
|
+
"locale",
|
|
613
|
+
"localeIn",
|
|
614
|
+
"localeQueryParam",
|
|
615
|
+
"localeHeader"
|
|
616
|
+
];
|
|
617
|
+
function pickClientConfig(options) {
|
|
618
|
+
const fromOptions = Object.fromEntries(
|
|
619
|
+
OAUTH_CLIENT_CONFIG_KEYS.filter((key) => options[key] !== void 0).map(
|
|
620
|
+
(key) => [key, options[key]]
|
|
621
|
+
)
|
|
622
|
+
);
|
|
623
|
+
return {
|
|
624
|
+
...fromOptions,
|
|
625
|
+
...options.config
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
var OAuthClient = class {
|
|
629
|
+
serviceName;
|
|
630
|
+
config;
|
|
631
|
+
authorizePath;
|
|
632
|
+
mapUser;
|
|
633
|
+
requester;
|
|
634
|
+
logger;
|
|
635
|
+
pkceStore;
|
|
636
|
+
gateway;
|
|
637
|
+
constructor(options) {
|
|
638
|
+
const {
|
|
639
|
+
serviceName = "OAuthClient",
|
|
640
|
+
gateway,
|
|
641
|
+
mapUser,
|
|
642
|
+
requester,
|
|
643
|
+
logger,
|
|
644
|
+
authorizePath = DEFAULT_OAUTH_AUTHORIZE_PATH,
|
|
645
|
+
pkceStorage,
|
|
646
|
+
pkceStorageKey
|
|
647
|
+
} = options;
|
|
648
|
+
this.serviceName = serviceName;
|
|
649
|
+
this.config = resolveOAuthClientConfig(pickClientConfig(options));
|
|
650
|
+
this.pkceStore = new PKCESessionStore({ pkceStorage, pkceStorageKey });
|
|
651
|
+
this.mapUser = mapUser;
|
|
652
|
+
this.requester = requester;
|
|
653
|
+
this.logger = logger;
|
|
654
|
+
this.authorizePath = authorizePath;
|
|
655
|
+
this.gateway = gateway ?? new OAuthGateway({
|
|
656
|
+
store: this.pkceStore,
|
|
657
|
+
config: this.authorizationConfig,
|
|
658
|
+
mapUser: mapUser ?? ((userinfo) => {
|
|
659
|
+
return userinfo;
|
|
660
|
+
}),
|
|
661
|
+
requester,
|
|
662
|
+
authorizePath,
|
|
663
|
+
origin: this.config.origin,
|
|
664
|
+
routerPrefix: this.config.routerPrefix,
|
|
665
|
+
locale: this.config.locale,
|
|
666
|
+
localeIn: this.config.localeIn,
|
|
667
|
+
localeQueryParam: this.config.localeQueryParam,
|
|
668
|
+
localeHeader: this.config.localeHeader
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
patchConfig(config) {
|
|
672
|
+
Object.assign(this.config, config);
|
|
673
|
+
}
|
|
674
|
+
get authorizationConfig() {
|
|
675
|
+
const serverUrl = this.config.serverUrl?.trim();
|
|
676
|
+
const clientId = this.config.clientId?.trim();
|
|
677
|
+
if (!serverUrl || !clientId) {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
serverUrl: serverUrl.replace(/\/$/, ""),
|
|
682
|
+
clientId,
|
|
683
|
+
scope: this.config.scope || defaultOAuthClientConfig.scope,
|
|
684
|
+
redirectPath: this.config.redirectPath || defaultOAuthClientConfig.redirectPath
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
get urlOptions() {
|
|
688
|
+
const { origin, routerPrefix } = this.config;
|
|
689
|
+
return { origin, routerPrefix };
|
|
690
|
+
}
|
|
691
|
+
get localeOptions() {
|
|
692
|
+
const { locale, localeIn, localeQueryParam, localeHeader } = this.config;
|
|
693
|
+
return { locale, localeIn, localeQueryParam, localeHeader };
|
|
694
|
+
}
|
|
695
|
+
getGateway() {
|
|
696
|
+
return this.gateway;
|
|
697
|
+
}
|
|
698
|
+
getLogger() {
|
|
699
|
+
return this.logger;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* @override
|
|
703
|
+
*/
|
|
704
|
+
isConfigured() {
|
|
705
|
+
return this.authorizationConfig != null;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* @override
|
|
709
|
+
*/
|
|
710
|
+
async startOAuthLogin(params) {
|
|
711
|
+
const config = mergeOAuthAuthorizationConfig(
|
|
712
|
+
params,
|
|
713
|
+
this.authorizationConfig
|
|
714
|
+
);
|
|
715
|
+
if (!config) {
|
|
716
|
+
throw new Error("OAuth is not configured");
|
|
717
|
+
}
|
|
718
|
+
const verifier = generatePkceVerifier();
|
|
719
|
+
const challenge = await computePkceS256Challenge(verifier);
|
|
720
|
+
const state = randomOAuthState();
|
|
721
|
+
const redirectPath = params?.redirectPath ?? config.redirectPath;
|
|
722
|
+
this.pkceStore.savePkceSession({
|
|
723
|
+
state,
|
|
724
|
+
codeVerifier: verifier,
|
|
725
|
+
locale: this.config.locale
|
|
726
|
+
});
|
|
727
|
+
const redirectUri = buildOAuthRedirectUri(redirectPath, {
|
|
728
|
+
...this.urlOptions,
|
|
729
|
+
...this.localeOptions
|
|
730
|
+
});
|
|
731
|
+
const url = buildOAuthAuthorizeUrl({
|
|
732
|
+
config,
|
|
733
|
+
authorizePath: this.authorizePath,
|
|
734
|
+
redirectPath: params?.redirectPath,
|
|
735
|
+
scope: params?.scope,
|
|
736
|
+
state,
|
|
737
|
+
codeChallenge: challenge,
|
|
738
|
+
...this.urlOptions,
|
|
739
|
+
...this.localeOptions
|
|
740
|
+
});
|
|
741
|
+
this.logger?.debug?.(`[${String(this.serviceName)}] startOAuthLogin`, {
|
|
742
|
+
locale: this.config.locale,
|
|
743
|
+
redirectUri,
|
|
744
|
+
statePreview: `${state.slice(0, 8)}\u2026`,
|
|
745
|
+
pkceSaved: this.pkceStore.loadPkceSession() != null
|
|
746
|
+
});
|
|
747
|
+
if (params?.target === "_blank") {
|
|
748
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
window.location.assign(url);
|
|
752
|
+
}
|
|
753
|
+
async completeOAuthCallback(params) {
|
|
754
|
+
const session = this.pkceStore.loadPkceSession();
|
|
755
|
+
const callbackState = params instanceof URLSearchParams ? params.get("state") : params?.state;
|
|
756
|
+
this.logger?.debug?.(
|
|
757
|
+
`[${String(this.serviceName)}] completeOAuthCallback`,
|
|
758
|
+
{
|
|
759
|
+
hasPkceSession: session != null,
|
|
760
|
+
callbackStatePreview: callbackState ? `${callbackState.slice(0, 8)}\u2026` : null,
|
|
761
|
+
sessionStatePreview: session?.state ? `${session.state.slice(0, 8)}\u2026` : null,
|
|
762
|
+
locale: this.config.locale
|
|
763
|
+
}
|
|
764
|
+
);
|
|
765
|
+
try {
|
|
766
|
+
const result = await this.gateway.oAuthWrapperCallback(params);
|
|
767
|
+
this.logger?.debug?.(
|
|
768
|
+
`[${String(this.serviceName)}] completeOAuthCallback succeeded`
|
|
769
|
+
);
|
|
770
|
+
return result;
|
|
771
|
+
} catch (error) {
|
|
772
|
+
this.logger?.warn?.(
|
|
773
|
+
`[${String(this.serviceName)}] completeOAuthCallback failed`,
|
|
774
|
+
{
|
|
775
|
+
message: error instanceof Error ? error.message : String(error),
|
|
776
|
+
hasPkceSessionAfterFailure: this.pkceStore.loadPkceSession() != null
|
|
777
|
+
}
|
|
778
|
+
);
|
|
779
|
+
throw error;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
parseOAuthCallbackSearchParams(searchParams) {
|
|
783
|
+
return {
|
|
784
|
+
code: searchParams.get("code") ?? void 0,
|
|
785
|
+
state: searchParams.get("state") ?? void 0,
|
|
786
|
+
error: searchParams.get("error") ?? void 0,
|
|
787
|
+
error_description: searchParams.get("error_description") ?? void 0
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* @override
|
|
792
|
+
*/
|
|
793
|
+
async revokeToken(refreshToken) {
|
|
794
|
+
if (!this.isConfigured()) {
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
await this.gateway.revokeToken({
|
|
798
|
+
refreshToken: refreshToken ?? void 0
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Fetch user profile from the OAuth userinfo endpoint and map it via {@link mapUser}.
|
|
803
|
+
*/
|
|
804
|
+
async fetchUserInfo(accessToken, refreshToken) {
|
|
805
|
+
if (!this.isConfigured()) {
|
|
806
|
+
throw new Error("OAuth is not configured");
|
|
807
|
+
}
|
|
808
|
+
const userinfo = await this.gateway.getUserInfo(accessToken);
|
|
809
|
+
const mapper = this.mapUser ?? ((info) => info);
|
|
810
|
+
return await mapper(userinfo, accessToken, refreshToken);
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
814
|
+
0 && (module.exports = {
|
|
815
|
+
DEFAULT_OAUTH_AUTHORIZE_PATH,
|
|
816
|
+
DEFAULT_PKCE_STORAGE_KEY,
|
|
817
|
+
OAuthClient,
|
|
818
|
+
OAuthGateway,
|
|
819
|
+
OAuthTokenErrorSchema,
|
|
820
|
+
OAuthTokenResponseSchema,
|
|
821
|
+
OAuthUserInfoSchema,
|
|
822
|
+
OAuthWrapperEndpoints,
|
|
823
|
+
PKCESessionStore,
|
|
824
|
+
buildAuthorizeSearchParams,
|
|
825
|
+
buildOAuthAuthorizeUrl,
|
|
826
|
+
buildOAuthLocaleHeaders,
|
|
827
|
+
buildOAuthRedirectUri,
|
|
828
|
+
defaultOAuthClientConfig,
|
|
829
|
+
formatOAuthAuthorizeUrl,
|
|
830
|
+
mergeOAuthAuthorizationConfig,
|
|
831
|
+
parseOAuthTokenError,
|
|
832
|
+
parseOAuthTokenResponse,
|
|
833
|
+
parseOAuthUserInfoResponse,
|
|
834
|
+
resolveOAuthClientConfig
|
|
835
|
+
});
|