@qlover/oauth-wrapper 0.2.0 → 0.2.3
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 +11 -0
- package/dist/client.cjs +3 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js +3 -1
- package/dist/core.cjs +8 -8
- package/dist/core.d.ts +59 -141
- package/dist/core.js +8 -8
- package/dist/index.cjs +483 -433
- package/dist/index.d.ts +106 -42
- package/dist/index.js +482 -433
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/server/index.ts
|
|
21
21
|
var server_exports = {};
|
|
22
22
|
__export(server_exports, {
|
|
23
|
+
OAuthAbstractProvider: () => OAuthAbstractProvider,
|
|
23
24
|
OAuthAuthorizationCodeRowSchema: () => OAuthAuthorizationCodeRowSchema,
|
|
24
25
|
OAuthAuthorizeQuerySchema: () => OAuthAuthorizeQuerySchema,
|
|
25
26
|
OAuthClientCreateResponseSchema: () => OAuthClientCreateResponseSchema,
|
|
@@ -63,119 +64,6 @@ __export(server_exports, {
|
|
|
63
64
|
});
|
|
64
65
|
module.exports = __toCommonJS(server_exports);
|
|
65
66
|
|
|
66
|
-
// src/server/services/OAuthClientsService.ts
|
|
67
|
-
var OAuthClientsService = class {
|
|
68
|
-
constructor(clientsRepo) {
|
|
69
|
-
this.clientsRepo = clientsRepo;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* List all OAuth clients owned by a user
|
|
73
|
-
* @override
|
|
74
|
-
*/
|
|
75
|
-
async listForOwner(ownerUserId) {
|
|
76
|
-
return this.clientsRepo.listClientByOwner(ownerUserId);
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Get detailed information about a specific OAuth client
|
|
80
|
-
* @override
|
|
81
|
-
*/
|
|
82
|
-
async getByClientId(ownerUserId, clientId) {
|
|
83
|
-
const client = await this.clientsRepo.findClientById(clientId);
|
|
84
|
-
if (!client) {
|
|
85
|
-
throw new Error("Client not found");
|
|
86
|
-
}
|
|
87
|
-
if (client.owner_user_id !== ownerUserId) {
|
|
88
|
-
throw new Error("Access denied");
|
|
89
|
-
}
|
|
90
|
-
return this.mapToDetail(client);
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Create a new OAuth client
|
|
94
|
-
* @override
|
|
95
|
-
*/
|
|
96
|
-
async create(ownerUserId, input) {
|
|
97
|
-
const result = await this.clientsRepo.createClient(ownerUserId, input);
|
|
98
|
-
return {
|
|
99
|
-
client_id: result.client.client_id,
|
|
100
|
-
client_secret: result.clientSecret,
|
|
101
|
-
confidential: result.client.confidential,
|
|
102
|
-
client_name: result.client.client_name,
|
|
103
|
-
client_uri: result.client.client_uri,
|
|
104
|
-
redirect_uris: result.client.redirect_uris,
|
|
105
|
-
created_at: result.client.created_at
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Update an existing OAuth client
|
|
110
|
-
* @override
|
|
111
|
-
*/
|
|
112
|
-
async update(ownerUserId, clientId, input) {
|
|
113
|
-
const existing = await this.clientsRepo.findClientById(clientId);
|
|
114
|
-
if (!existing) {
|
|
115
|
-
throw new Error("Client not found");
|
|
116
|
-
}
|
|
117
|
-
if (existing.owner_user_id !== ownerUserId) {
|
|
118
|
-
throw new Error("Access denied");
|
|
119
|
-
}
|
|
120
|
-
return this.clientsRepo.updateClient(ownerUserId, clientId, input);
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Rotate the client secret
|
|
124
|
-
* @override
|
|
125
|
-
*/
|
|
126
|
-
async rotateSecret(ownerUserId, clientId) {
|
|
127
|
-
const existing = await this.clientsRepo.findClientById(clientId);
|
|
128
|
-
if (!existing) {
|
|
129
|
-
throw new Error("Client not found");
|
|
130
|
-
}
|
|
131
|
-
if (existing.owner_user_id !== ownerUserId) {
|
|
132
|
-
throw new Error("Access denied");
|
|
133
|
-
}
|
|
134
|
-
if (!existing.confidential) {
|
|
135
|
-
throw new Error("Public clients do not have a client_secret");
|
|
136
|
-
}
|
|
137
|
-
const result = await this.clientsRepo.rotateClientSecret(
|
|
138
|
-
ownerUserId,
|
|
139
|
-
clientId
|
|
140
|
-
);
|
|
141
|
-
return {
|
|
142
|
-
client_id: clientId,
|
|
143
|
-
client_secret: result.clientSecret
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Delete an OAuth client
|
|
148
|
-
* @override
|
|
149
|
-
*/
|
|
150
|
-
async delete(ownerUserId, clientId) {
|
|
151
|
-
const existing = await this.clientsRepo.findClientById(clientId);
|
|
152
|
-
if (!existing) {
|
|
153
|
-
throw new Error("Client not found");
|
|
154
|
-
}
|
|
155
|
-
if (existing.owner_user_id !== ownerUserId) {
|
|
156
|
-
throw new Error("Access denied");
|
|
157
|
-
}
|
|
158
|
-
await this.clientsRepo.deleteClient(ownerUserId, clientId);
|
|
159
|
-
}
|
|
160
|
-
mapToDetail(row) {
|
|
161
|
-
return {
|
|
162
|
-
client_id: row.client_id,
|
|
163
|
-
client_name: row.client_name,
|
|
164
|
-
client_uri: row.client_uri,
|
|
165
|
-
logo_uri: row.logo_uri,
|
|
166
|
-
redirect_uris: row.redirect_uris,
|
|
167
|
-
grant_types: row.grant_types,
|
|
168
|
-
scopes: row.scopes,
|
|
169
|
-
confidential: row.confidential,
|
|
170
|
-
created_at: row.created_at,
|
|
171
|
-
updated_at: row.updated_at
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
// src/server/services/OAuthTokenService.ts
|
|
177
|
-
var import_crypto2 = require("crypto");
|
|
178
|
-
|
|
179
67
|
// src/core/schema/OAuthAuthorizeSchema.ts
|
|
180
68
|
var import_zod = require("zod");
|
|
181
69
|
function isOAuthRedirectUri(value) {
|
|
@@ -192,7 +80,7 @@ function isOAuthRedirectUri(value) {
|
|
|
192
80
|
}
|
|
193
81
|
var oauthRedirectUriSchema = import_zod.z.string().min(1).refine(isOAuthRedirectUri, { message: "Invalid redirect URI" });
|
|
194
82
|
var OAuthClientRowSchema = import_zod.z.object({
|
|
195
|
-
id: import_zod.z.
|
|
83
|
+
id: import_zod.z.string(),
|
|
196
84
|
client_id: import_zod.z.string(),
|
|
197
85
|
client_secret_hash: import_zod.z.string().nullable().optional(),
|
|
198
86
|
client_name: import_zod.z.string(),
|
|
@@ -202,7 +90,7 @@ var OAuthClientRowSchema = import_zod.z.object({
|
|
|
202
90
|
grant_types: import_zod.z.array(import_zod.z.string()),
|
|
203
91
|
scopes: import_zod.z.array(import_zod.z.string()),
|
|
204
92
|
confidential: import_zod.z.boolean(),
|
|
205
|
-
owner_user_id: import_zod.z.
|
|
93
|
+
owner_user_id: import_zod.z.string(),
|
|
206
94
|
created_at: import_zod.z.string(),
|
|
207
95
|
updated_at: import_zod.z.string()
|
|
208
96
|
});
|
|
@@ -275,7 +163,7 @@ var OAuthConsentBodySchema = import_zod.z.object({
|
|
|
275
163
|
var OAuthAuthorizationCodeRowSchema = import_zod.z.object({
|
|
276
164
|
code: import_zod.z.string(),
|
|
277
165
|
client_id: import_zod.z.string(),
|
|
278
|
-
user_id: import_zod.z.
|
|
166
|
+
user_id: import_zod.z.string(),
|
|
279
167
|
redirect_uri: import_zod.z.string(),
|
|
280
168
|
scope: import_zod.z.string().nullable().optional(),
|
|
281
169
|
code_challenge: import_zod.z.string().nullable().optional(),
|
|
@@ -288,16 +176,16 @@ var OAuthAuthorizationCodeRowSchema = import_zod.z.object({
|
|
|
288
176
|
// src/core/schema/OAuthClientSchema.ts
|
|
289
177
|
var import_zod2 = require("zod");
|
|
290
178
|
var OAuthUserCredentialsSchema = import_zod2.z.object({
|
|
291
|
-
user_id: import_zod2.z.
|
|
179
|
+
user_id: import_zod2.z.string(),
|
|
292
180
|
provider_refresh_token: import_zod2.z.string().nullable().optional(),
|
|
293
181
|
provider_session_token: import_zod2.z.string().nullable().optional(),
|
|
294
182
|
updated_at: import_zod2.z.string()
|
|
295
183
|
});
|
|
296
184
|
var OAuthRefreshTokenSchema = import_zod2.z.object({
|
|
297
|
-
id: import_zod2.z.
|
|
185
|
+
id: import_zod2.z.string(),
|
|
298
186
|
refresh_token: import_zod2.z.string(),
|
|
299
187
|
client_id: import_zod2.z.string(),
|
|
300
|
-
user_id: import_zod2.z.
|
|
188
|
+
user_id: import_zod2.z.string(),
|
|
301
189
|
expires_at: import_zod2.z.string(),
|
|
302
190
|
revoked: import_zod2.z.boolean(),
|
|
303
191
|
created_at: import_zod2.z.string()
|
|
@@ -314,12 +202,12 @@ var OAuthTokenResponseSchema = import_zod2.z.object({
|
|
|
314
202
|
var import_zod3 = require("zod");
|
|
315
203
|
var OAuthUserInfoResponseSchema = import_zod3.z.object({
|
|
316
204
|
sub: import_zod3.z.string(),
|
|
317
|
-
email: import_zod3.z.
|
|
205
|
+
email: import_zod3.z.email(),
|
|
318
206
|
name: import_zod3.z.string(),
|
|
319
207
|
roles: import_zod3.z.array(import_zod3.z.string()).optional()
|
|
320
208
|
});
|
|
321
209
|
var OAuthUserInfoErrorResponseSchema = import_zod3.z.object({
|
|
322
|
-
error: import_zod3.z.
|
|
210
|
+
error: import_zod3.z.string(),
|
|
323
211
|
error_id: import_zod3.z.string().optional()
|
|
324
212
|
});
|
|
325
213
|
|
|
@@ -400,16 +288,6 @@ function randomOAuthState() {
|
|
|
400
288
|
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
401
289
|
}
|
|
402
290
|
|
|
403
|
-
// src/server/utils/OAuthWrapperError.ts
|
|
404
|
-
var import_fe_corekit = require("@qlover/fe-corekit");
|
|
405
|
-
var OAuthWrapperError = class extends import_fe_corekit.ExecutorError {
|
|
406
|
-
constructor(id, status, cause) {
|
|
407
|
-
super(id, cause);
|
|
408
|
-
this.status = status;
|
|
409
|
-
}
|
|
410
|
-
name = "OAuthWrapperError";
|
|
411
|
-
};
|
|
412
|
-
|
|
413
291
|
// src/server/utils/pkce.ts
|
|
414
292
|
var import_crypto = require("crypto");
|
|
415
293
|
var PKCE_VERIFIER_MIN = 43;
|
|
@@ -443,267 +321,43 @@ function verifyPkceS256(verifier, challenge) {
|
|
|
443
321
|
}
|
|
444
322
|
}
|
|
445
323
|
|
|
446
|
-
// src/server/
|
|
447
|
-
function
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
static REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
457
|
-
/**
|
|
458
|
-
* @override
|
|
459
|
-
*/
|
|
460
|
-
async exchangeToken(rawFields) {
|
|
461
|
-
let request;
|
|
462
|
-
try {
|
|
463
|
-
request = OAuthTokenRequestSchema.parse(rawFields);
|
|
464
|
-
} catch {
|
|
465
|
-
throw new OAuthWrapperError(
|
|
466
|
-
OAuthRfcCodes.INVALID_REQUEST,
|
|
467
|
-
400,
|
|
468
|
-
"Malformed token request"
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
let client;
|
|
472
|
-
try {
|
|
473
|
-
client = await this.oauthRepo.verifyClientCredentials(
|
|
474
|
-
request.client_id,
|
|
475
|
-
request.client_secret
|
|
476
|
-
);
|
|
477
|
-
} catch {
|
|
478
|
-
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
|
|
479
|
-
}
|
|
480
|
-
if (!client.grant_types.includes(request.grant_type)) {
|
|
481
|
-
throw new OAuthWrapperError(OAuthRfcCodes.UNSUPPORTED_GRANT_TYPE, 400);
|
|
324
|
+
// src/server/utils/authorizeUtil.ts
|
|
325
|
+
function validatePkceParams(parsed, confidential) {
|
|
326
|
+
const hasChallenge = Boolean(parsed.code_challenge?.trim());
|
|
327
|
+
const hasMethod = Boolean(parsed.code_challenge_method);
|
|
328
|
+
if (!confidential) {
|
|
329
|
+
if (!hasChallenge || parsed.code_challenge_method !== "S256") {
|
|
330
|
+
return {
|
|
331
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
332
|
+
message: "Public clients must send code_challenge and code_challenge_method=S256."
|
|
333
|
+
};
|
|
482
334
|
}
|
|
483
|
-
if (
|
|
484
|
-
return
|
|
335
|
+
if (!isValidCodeChallenge(parsed.code_challenge)) {
|
|
336
|
+
return {
|
|
337
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
338
|
+
message: "Invalid code_challenge."
|
|
339
|
+
};
|
|
485
340
|
}
|
|
486
|
-
return
|
|
341
|
+
return null;
|
|
487
342
|
}
|
|
488
|
-
|
|
489
|
-
const authCode = await this.oauthRepo.consumeCode(request.code);
|
|
490
|
-
if (!authCode) {
|
|
491
|
-
throw new OAuthWrapperError(
|
|
492
|
-
OAuthRfcCodes.INVALID_GRANT,
|
|
493
|
-
400,
|
|
494
|
-
"Authorization code is invalid, expired, or already used"
|
|
495
|
-
);
|
|
496
|
-
}
|
|
497
|
-
if (authCode.client_id !== verifiedClientId) {
|
|
498
|
-
throw new OAuthWrapperError(
|
|
499
|
-
OAuthRfcCodes.INVALID_GRANT,
|
|
500
|
-
400,
|
|
501
|
-
"client_id mismatch"
|
|
502
|
-
);
|
|
503
|
-
}
|
|
504
|
-
if (authCode.redirect_uri !== request.redirect_uri) {
|
|
505
|
-
throw new OAuthWrapperError(
|
|
506
|
-
OAuthRfcCodes.INVALID_GRANT,
|
|
507
|
-
400,
|
|
508
|
-
"redirect_uri mismatch"
|
|
509
|
-
);
|
|
510
|
-
}
|
|
511
|
-
this.assertPkceForAuthorizationCode(request, authCode);
|
|
512
|
-
const providerTokens = await this.fetchProviderAccessToken(
|
|
513
|
-
authCode.user_id
|
|
514
|
-
);
|
|
515
|
-
const middlewareRefresh = await this.issueMiddlewareRefreshToken(
|
|
516
|
-
authCode.client_id,
|
|
517
|
-
authCode.user_id
|
|
518
|
-
);
|
|
343
|
+
if (hasChallenge !== hasMethod) {
|
|
519
344
|
return {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
expires_in: providerTokens.expires_in,
|
|
523
|
-
refresh_token: middlewareRefresh,
|
|
524
|
-
scope: authCode.scope ?? void 0
|
|
345
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
346
|
+
message: "code_challenge and code_challenge_method must be sent together."
|
|
525
347
|
};
|
|
526
348
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
OAuthRfcCodes.INVALID_GRANT,
|
|
534
|
-
400,
|
|
535
|
-
"code_verifier is required"
|
|
536
|
-
);
|
|
537
|
-
}
|
|
538
|
-
if (authCode.code_challenge_method !== "S256") {
|
|
539
|
-
throw new OAuthWrapperError(
|
|
540
|
-
OAuthRfcCodes.INVALID_GRANT,
|
|
541
|
-
400,
|
|
542
|
-
"Unsupported PKCE method"
|
|
543
|
-
);
|
|
544
|
-
}
|
|
545
|
-
if (!verifyPkceS256(verifier, storedChallenge)) {
|
|
546
|
-
throw new OAuthWrapperError(
|
|
547
|
-
OAuthRfcCodes.INVALID_GRANT,
|
|
548
|
-
400,
|
|
549
|
-
"code_verifier mismatch"
|
|
550
|
-
);
|
|
551
|
-
}
|
|
552
|
-
return;
|
|
349
|
+
if (hasChallenge) {
|
|
350
|
+
if (parsed.code_challenge_method !== "S256") {
|
|
351
|
+
return {
|
|
352
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
353
|
+
message: "Only code_challenge_method=S256 is supported."
|
|
354
|
+
};
|
|
553
355
|
}
|
|
554
|
-
if (!
|
|
555
|
-
|
|
556
|
-
OAuthRfcCodes.
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
async exchangeRefreshToken(request, verifiedClientId) {
|
|
563
|
-
const tokenHash = hashOpaqueToken(request.refresh_token);
|
|
564
|
-
const stored = await this.oauthRepo.findByTokenHash(tokenHash);
|
|
565
|
-
if (!stored || stored.revoked || stored.client_id !== verifiedClientId || new Date(stored.expires_at) <= /* @__PURE__ */ new Date()) {
|
|
566
|
-
throw new OAuthWrapperError(
|
|
567
|
-
OAuthRfcCodes.INVALID_GRANT,
|
|
568
|
-
400,
|
|
569
|
-
"Refresh token is invalid"
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
|
-
const providerTokens = await this.fetchProviderAccessToken(stored.user_id);
|
|
573
|
-
await this.oauthRepo.revokeByTokenHash(tokenHash);
|
|
574
|
-
const middlewareRefresh = await this.issueMiddlewareRefreshToken(
|
|
575
|
-
stored.client_id,
|
|
576
|
-
stored.user_id
|
|
577
|
-
);
|
|
578
|
-
return {
|
|
579
|
-
access_token: providerTokens.access_token,
|
|
580
|
-
token_type: "Bearer",
|
|
581
|
-
expires_in: providerTokens.expires_in,
|
|
582
|
-
refresh_token: middlewareRefresh
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
async fetchProviderAccessToken(userId) {
|
|
586
|
-
const credentials = await this.oauthRepo.getUserCredentials(userId);
|
|
587
|
-
const sessionToken = credentials?.provider_session_token?.trim();
|
|
588
|
-
if (!sessionToken) {
|
|
589
|
-
throw new OAuthWrapperError(
|
|
590
|
-
OAuthRfcCodes.INVALID_GRANT,
|
|
591
|
-
400,
|
|
592
|
-
"User credentials expired. Re-authorization required."
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
try {
|
|
596
|
-
const access = await this.userAdapter.exchangeAccessToken({
|
|
597
|
-
token: sessionToken
|
|
598
|
-
});
|
|
599
|
-
if (access.refresh_token) {
|
|
600
|
-
await this.oauthRepo.upsertUserCredentials(userId, {
|
|
601
|
-
provider_refresh_token: this.tokenEncryption.encrypt(
|
|
602
|
-
access.refresh_token
|
|
603
|
-
)
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
|
-
return {
|
|
607
|
-
access_token: access.access_token,
|
|
608
|
-
expires_in: access.expires_in
|
|
609
|
-
};
|
|
610
|
-
} catch {
|
|
611
|
-
throw new OAuthWrapperError(
|
|
612
|
-
OAuthRfcCodes.INVALID_GRANT,
|
|
613
|
-
400,
|
|
614
|
-
"Failed to obtain access token from user provider"
|
|
615
|
-
);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
async issueMiddlewareRefreshToken(clientId, userId) {
|
|
619
|
-
const plain = (0, import_crypto2.randomBytes)(32).toString("base64url");
|
|
620
|
-
const expiresAt = new Date(
|
|
621
|
-
Date.now() + _OAuthTokenService.REFRESH_TOKEN_TTL_MS
|
|
622
|
-
).toISOString();
|
|
623
|
-
await this.oauthRepo.createRefreshToken({
|
|
624
|
-
refresh_token: hashOpaqueToken(plain),
|
|
625
|
-
client_id: clientId,
|
|
626
|
-
user_id: userId,
|
|
627
|
-
expires_at: expiresAt
|
|
628
|
-
});
|
|
629
|
-
return plain;
|
|
630
|
-
}
|
|
631
|
-
/**
|
|
632
|
-
* @override
|
|
633
|
-
* RFC 7009 — revoke middleware refresh tokens. Idempotent: unknown tokens are ignored.
|
|
634
|
-
*/
|
|
635
|
-
async revokeToken(rawFields) {
|
|
636
|
-
let request;
|
|
637
|
-
try {
|
|
638
|
-
request = OAuthTokenRevokeSchema.parse(rawFields);
|
|
639
|
-
} catch {
|
|
640
|
-
throw new OAuthWrapperError(
|
|
641
|
-
OAuthRfcCodes.INVALID_REQUEST,
|
|
642
|
-
400,
|
|
643
|
-
"Malformed revocation request"
|
|
644
|
-
);
|
|
645
|
-
}
|
|
646
|
-
try {
|
|
647
|
-
await this.oauthRepo.verifyClientCredentials(
|
|
648
|
-
request.client_id,
|
|
649
|
-
request.client_secret
|
|
650
|
-
);
|
|
651
|
-
} catch {
|
|
652
|
-
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
|
|
653
|
-
}
|
|
654
|
-
if (request.token_type_hint && request.token_type_hint !== "refresh_token") {
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
const tokenHash = hashOpaqueToken(request.token);
|
|
658
|
-
const stored = await this.oauthRepo.findByTokenHash(tokenHash);
|
|
659
|
-
if (!stored || stored.client_id !== request.client_id) {
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
await this.oauthRepo.revokeByTokenHash(tokenHash);
|
|
663
|
-
}
|
|
664
|
-
};
|
|
665
|
-
|
|
666
|
-
// src/server/services/OAuthWrapperService.ts
|
|
667
|
-
var import_crypto3 = require("crypto");
|
|
668
|
-
var import_fe_corekit2 = require("@qlover/fe-corekit");
|
|
669
|
-
|
|
670
|
-
// src/server/utils/authorizeUtil.ts
|
|
671
|
-
function validatePkceParams(parsed, confidential) {
|
|
672
|
-
const hasChallenge = Boolean(parsed.code_challenge?.trim());
|
|
673
|
-
const hasMethod = Boolean(parsed.code_challenge_method);
|
|
674
|
-
if (!confidential) {
|
|
675
|
-
if (!hasChallenge || parsed.code_challenge_method !== "S256") {
|
|
676
|
-
return {
|
|
677
|
-
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
678
|
-
message: "Public clients must send code_challenge and code_challenge_method=S256."
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
if (!isValidCodeChallenge(parsed.code_challenge)) {
|
|
682
|
-
return {
|
|
683
|
-
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
684
|
-
message: "Invalid code_challenge."
|
|
685
|
-
};
|
|
686
|
-
}
|
|
687
|
-
return null;
|
|
688
|
-
}
|
|
689
|
-
if (hasChallenge !== hasMethod) {
|
|
690
|
-
return {
|
|
691
|
-
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
692
|
-
message: "code_challenge and code_challenge_method must be sent together."
|
|
693
|
-
};
|
|
694
|
-
}
|
|
695
|
-
if (hasChallenge) {
|
|
696
|
-
if (parsed.code_challenge_method !== "S256") {
|
|
697
|
-
return {
|
|
698
|
-
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
699
|
-
message: "Only code_challenge_method=S256 is supported."
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
|
-
if (!isValidCodeChallenge(parsed.code_challenge)) {
|
|
703
|
-
return {
|
|
704
|
-
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
705
|
-
message: "Invalid code_challenge."
|
|
706
|
-
};
|
|
356
|
+
if (!isValidCodeChallenge(parsed.code_challenge)) {
|
|
357
|
+
return {
|
|
358
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
359
|
+
message: "Invalid code_challenge."
|
|
360
|
+
};
|
|
707
361
|
}
|
|
708
362
|
}
|
|
709
363
|
return null;
|
|
@@ -743,44 +397,36 @@ function parseScopeList(scope) {
|
|
|
743
397
|
return scope.trim().split(/\s+/).filter(Boolean);
|
|
744
398
|
}
|
|
745
399
|
|
|
746
|
-
// src/server/services/
|
|
747
|
-
var
|
|
748
|
-
var
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
this.oauthRepo = oauthRepo;
|
|
754
|
-
}
|
|
755
|
-
/**
|
|
756
|
-
* @override
|
|
757
|
-
*/
|
|
758
|
-
getOAuthSession() {
|
|
759
|
-
return this.oauthSession;
|
|
400
|
+
// src/server/services/OAuthAbstractProvider.ts
|
|
401
|
+
var import_fe_corekit = require("@qlover/fe-corekit");
|
|
402
|
+
var import_crypto2 = require("crypto");
|
|
403
|
+
var OAuthAbstractProvider = class {
|
|
404
|
+
authCodeTTLMs = 5 * 60 * 1e3;
|
|
405
|
+
isQuery(query) {
|
|
406
|
+
return OAuthAuthorizeQuerySchema.safeParse(query).success;
|
|
760
407
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
*/
|
|
764
|
-
getOAuthAdapter() {
|
|
765
|
-
return this.userAdapter;
|
|
408
|
+
isValidateConsent(value) {
|
|
409
|
+
return OAuthConsentBodySchema.safeParse(value).success;
|
|
766
410
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
411
|
+
generageSessionPayload(userInfo) {
|
|
412
|
+
return {
|
|
413
|
+
userId: String(userInfo.id),
|
|
414
|
+
email: userInfo.email,
|
|
415
|
+
name: userInfo.name ?? userInfo.email,
|
|
416
|
+
providerSessionToken: userInfo.sessionToken
|
|
417
|
+
};
|
|
772
418
|
}
|
|
773
419
|
/**
|
|
774
420
|
* @override
|
|
775
421
|
*/
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
422
|
+
async logout(userId) {
|
|
423
|
+
const oauthRepo = this.getOAuthRepo();
|
|
424
|
+
await oauthRepo.revokeRefreshTokensByUserId(userId);
|
|
425
|
+
await oauthRepo.upsertUserCredentials(userId, {
|
|
426
|
+
provider_refresh_token: null,
|
|
427
|
+
provider_session_token: null
|
|
428
|
+
});
|
|
429
|
+
await this.clearSession();
|
|
784
430
|
}
|
|
785
431
|
/**
|
|
786
432
|
* @override
|
|
@@ -805,7 +451,8 @@ var OAuthWrapperService = class {
|
|
|
805
451
|
}
|
|
806
452
|
};
|
|
807
453
|
}
|
|
808
|
-
const
|
|
454
|
+
const oauthRepo = this.getOAuthRepo();
|
|
455
|
+
const client = await oauthRepo.findClientById(query.client_id);
|
|
809
456
|
if (!client) {
|
|
810
457
|
return {
|
|
811
458
|
ok: false,
|
|
@@ -863,14 +510,14 @@ var OAuthWrapperService = class {
|
|
|
863
510
|
*/
|
|
864
511
|
async processConsent(requestBody) {
|
|
865
512
|
if (!this.isValidateConsent(requestBody)) {
|
|
866
|
-
throw new
|
|
513
|
+
throw new import_fe_corekit.ExecutorError(
|
|
867
514
|
OAuthRfcCodes.INVALID_REQUEST,
|
|
868
515
|
"Invalid consent request body"
|
|
869
516
|
);
|
|
870
517
|
}
|
|
871
|
-
const session = await this.
|
|
518
|
+
const session = await this.getSession();
|
|
872
519
|
if (!session) {
|
|
873
|
-
throw new
|
|
520
|
+
throw new import_fe_corekit.ExecutorError(
|
|
874
521
|
OAuthRfcCodes.ACCESS_DENIED,
|
|
875
522
|
"User session expired. Please sign in again."
|
|
876
523
|
);
|
|
@@ -885,7 +532,7 @@ var OAuthWrapperService = class {
|
|
|
885
532
|
code_challenge_method: requestBody.code_challenge_method
|
|
886
533
|
});
|
|
887
534
|
if (!pageResult.ok) {
|
|
888
|
-
throw new
|
|
535
|
+
throw new import_fe_corekit.ExecutorError(
|
|
889
536
|
pageResult.error.errorKey,
|
|
890
537
|
pageResult.error.message
|
|
891
538
|
);
|
|
@@ -900,9 +547,10 @@ var OAuthWrapperService = class {
|
|
|
900
547
|
})
|
|
901
548
|
};
|
|
902
549
|
}
|
|
903
|
-
const code = (0,
|
|
904
|
-
const expiresAt = new Date(Date.now() +
|
|
905
|
-
|
|
550
|
+
const code = (0, import_crypto2.randomBytes)(32).toString("base64url");
|
|
551
|
+
const expiresAt = new Date(Date.now() + this.authCodeTTLMs).toISOString();
|
|
552
|
+
const oauthRepo = this.getOAuthRepo();
|
|
553
|
+
await oauthRepo.create({
|
|
906
554
|
code,
|
|
907
555
|
client_id: data.clientId,
|
|
908
556
|
user_id: session.userId,
|
|
@@ -920,21 +568,423 @@ var OAuthWrapperService = class {
|
|
|
920
568
|
})
|
|
921
569
|
};
|
|
922
570
|
}
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
// src/server/services/OAuthClientsService.ts
|
|
574
|
+
var OAuthClientsService = class {
|
|
575
|
+
constructor(clientsRepo) {
|
|
576
|
+
this.clientsRepo = clientsRepo;
|
|
577
|
+
}
|
|
578
|
+
clientsRepo;
|
|
923
579
|
/**
|
|
580
|
+
* List all OAuth clients owned by a user
|
|
924
581
|
* @override
|
|
925
582
|
*/
|
|
926
|
-
async
|
|
927
|
-
return
|
|
583
|
+
async listForOwner(ownerUserId) {
|
|
584
|
+
return this.clientsRepo.listClientByOwner(ownerUserId);
|
|
928
585
|
}
|
|
929
586
|
/**
|
|
587
|
+
* Get detailed information about a specific OAuth client
|
|
930
588
|
* @override
|
|
931
589
|
*/
|
|
932
|
-
async
|
|
933
|
-
|
|
590
|
+
async getByClientId(ownerUserId, clientId) {
|
|
591
|
+
const client = await this.clientsRepo.findClientById(clientId);
|
|
592
|
+
if (!client) {
|
|
593
|
+
throw new Error("Client not found");
|
|
594
|
+
}
|
|
595
|
+
if (client.owner_user_id !== ownerUserId) {
|
|
596
|
+
throw new Error("Access denied");
|
|
597
|
+
}
|
|
598
|
+
return this.mapToDetail(client);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Create a new OAuth client
|
|
602
|
+
* @override
|
|
603
|
+
*/
|
|
604
|
+
async create(ownerUserId, input) {
|
|
605
|
+
const result = await this.clientsRepo.createClient(ownerUserId, input);
|
|
606
|
+
return {
|
|
607
|
+
client_id: result.client.client_id,
|
|
608
|
+
client_secret: result.clientSecret,
|
|
609
|
+
confidential: result.client.confidential,
|
|
610
|
+
client_name: result.client.client_name,
|
|
611
|
+
client_uri: result.client.client_uri,
|
|
612
|
+
redirect_uris: result.client.redirect_uris,
|
|
613
|
+
created_at: result.client.created_at
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Update an existing OAuth client
|
|
618
|
+
* @override
|
|
619
|
+
*/
|
|
620
|
+
async update(ownerUserId, clientId, input) {
|
|
621
|
+
const existing = await this.clientsRepo.findClientById(clientId);
|
|
622
|
+
if (!existing) {
|
|
623
|
+
throw new Error("Client not found");
|
|
624
|
+
}
|
|
625
|
+
if (existing.owner_user_id !== ownerUserId) {
|
|
626
|
+
throw new Error("Access denied");
|
|
627
|
+
}
|
|
628
|
+
return this.clientsRepo.updateClient(ownerUserId, clientId, input);
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Rotate the client secret
|
|
632
|
+
* @override
|
|
633
|
+
*/
|
|
634
|
+
async rotateSecret(ownerUserId, clientId) {
|
|
635
|
+
const existing = await this.clientsRepo.findClientById(clientId);
|
|
636
|
+
if (!existing) {
|
|
637
|
+
throw new Error("Client not found");
|
|
638
|
+
}
|
|
639
|
+
if (existing.owner_user_id !== ownerUserId) {
|
|
640
|
+
throw new Error("Access denied");
|
|
641
|
+
}
|
|
642
|
+
if (!existing.confidential) {
|
|
643
|
+
throw new Error("Public clients do not have a client_secret");
|
|
644
|
+
}
|
|
645
|
+
const result = await this.clientsRepo.rotateClientSecret(
|
|
646
|
+
ownerUserId,
|
|
647
|
+
clientId
|
|
648
|
+
);
|
|
649
|
+
return {
|
|
650
|
+
client_id: clientId,
|
|
651
|
+
client_secret: result.clientSecret
|
|
652
|
+
};
|
|
934
653
|
}
|
|
935
654
|
/**
|
|
655
|
+
* Delete an OAuth client
|
|
936
656
|
* @override
|
|
937
657
|
*/
|
|
658
|
+
async delete(ownerUserId, clientId) {
|
|
659
|
+
const existing = await this.clientsRepo.findClientById(clientId);
|
|
660
|
+
if (!existing) {
|
|
661
|
+
throw new Error("Client not found");
|
|
662
|
+
}
|
|
663
|
+
if (existing.owner_user_id !== ownerUserId) {
|
|
664
|
+
throw new Error("Access denied");
|
|
665
|
+
}
|
|
666
|
+
await this.clientsRepo.deleteClient(ownerUserId, clientId);
|
|
667
|
+
}
|
|
668
|
+
mapToDetail(row) {
|
|
669
|
+
return {
|
|
670
|
+
client_id: row.client_id,
|
|
671
|
+
client_name: row.client_name,
|
|
672
|
+
client_uri: row.client_uri,
|
|
673
|
+
logo_uri: row.logo_uri,
|
|
674
|
+
redirect_uris: row.redirect_uris,
|
|
675
|
+
grant_types: row.grant_types,
|
|
676
|
+
scopes: row.scopes,
|
|
677
|
+
confidential: row.confidential,
|
|
678
|
+
created_at: row.created_at,
|
|
679
|
+
updated_at: row.updated_at
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
// src/server/services/OAuthTokenService.ts
|
|
685
|
+
var import_crypto3 = require("crypto");
|
|
686
|
+
|
|
687
|
+
// src/server/utils/OAuthWrapperError.ts
|
|
688
|
+
var import_fe_corekit2 = require("@qlover/fe-corekit");
|
|
689
|
+
var OAuthWrapperError = class extends import_fe_corekit2.ExecutorError {
|
|
690
|
+
constructor(id, status, cause) {
|
|
691
|
+
super(id, cause);
|
|
692
|
+
this.status = status;
|
|
693
|
+
}
|
|
694
|
+
status;
|
|
695
|
+
name = "OAuthWrapperError";
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
// src/server/services/OAuthTokenService.ts
|
|
699
|
+
function hashOpaqueToken(token) {
|
|
700
|
+
return (0, import_crypto3.createHash)("sha256").update(token).digest("hex");
|
|
701
|
+
}
|
|
702
|
+
var OAuthTokenService = class _OAuthTokenService {
|
|
703
|
+
constructor(tokenEncryption, exchangeProviderAccessToken, oauthRepo) {
|
|
704
|
+
this.tokenEncryption = tokenEncryption;
|
|
705
|
+
this.exchangeProviderAccessToken = exchangeProviderAccessToken;
|
|
706
|
+
this.oauthRepo = oauthRepo;
|
|
707
|
+
}
|
|
708
|
+
tokenEncryption;
|
|
709
|
+
exchangeProviderAccessToken;
|
|
710
|
+
oauthRepo;
|
|
711
|
+
static REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
712
|
+
/**
|
|
713
|
+
* @override
|
|
714
|
+
*/
|
|
715
|
+
async exchangeToken(rawFields) {
|
|
716
|
+
let request;
|
|
717
|
+
try {
|
|
718
|
+
request = OAuthTokenRequestSchema.parse(rawFields);
|
|
719
|
+
} catch {
|
|
720
|
+
throw new OAuthWrapperError(
|
|
721
|
+
OAuthRfcCodes.INVALID_REQUEST,
|
|
722
|
+
400,
|
|
723
|
+
"Malformed token request"
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
let client;
|
|
727
|
+
try {
|
|
728
|
+
client = await this.oauthRepo.verifyClientCredentials(
|
|
729
|
+
request.client_id,
|
|
730
|
+
request.client_secret
|
|
731
|
+
);
|
|
732
|
+
} catch {
|
|
733
|
+
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
|
|
734
|
+
}
|
|
735
|
+
if (!client.grant_types.includes(request.grant_type)) {
|
|
736
|
+
throw new OAuthWrapperError(OAuthRfcCodes.UNSUPPORTED_GRANT_TYPE, 400);
|
|
737
|
+
}
|
|
738
|
+
if (request.grant_type === "authorization_code") {
|
|
739
|
+
return await this.exchangeAuthorizationCode(request, client.client_id);
|
|
740
|
+
}
|
|
741
|
+
return await this.exchangeRefreshToken(request, client.client_id);
|
|
742
|
+
}
|
|
743
|
+
async exchangeAuthorizationCode(request, verifiedClientId) {
|
|
744
|
+
const authCode = await this.oauthRepo.consumeCode(request.code);
|
|
745
|
+
if (!authCode) {
|
|
746
|
+
throw new OAuthWrapperError(
|
|
747
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
748
|
+
400,
|
|
749
|
+
"Authorization code is invalid, expired, or already used"
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
if (authCode.client_id !== verifiedClientId) {
|
|
753
|
+
throw new OAuthWrapperError(
|
|
754
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
755
|
+
400,
|
|
756
|
+
"client_id mismatch"
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
if (authCode.redirect_uri !== request.redirect_uri) {
|
|
760
|
+
throw new OAuthWrapperError(
|
|
761
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
762
|
+
400,
|
|
763
|
+
"redirect_uri mismatch"
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
this.assertPkceForAuthorizationCode(request, authCode);
|
|
767
|
+
const providerTokens = await this.fetchProviderAccessToken(
|
|
768
|
+
authCode.user_id
|
|
769
|
+
);
|
|
770
|
+
const middlewareRefresh = await this.issueMiddlewareRefreshToken(
|
|
771
|
+
authCode.client_id,
|
|
772
|
+
authCode.user_id
|
|
773
|
+
);
|
|
774
|
+
return {
|
|
775
|
+
access_token: providerTokens.access_token,
|
|
776
|
+
token_type: "Bearer",
|
|
777
|
+
expires_in: providerTokens.expires_in,
|
|
778
|
+
refresh_token: middlewareRefresh,
|
|
779
|
+
scope: authCode.scope ?? void 0
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
assertPkceForAuthorizationCode(request, authCode) {
|
|
783
|
+
const storedChallenge = authCode.code_challenge?.trim();
|
|
784
|
+
if (storedChallenge) {
|
|
785
|
+
const verifier = request.code_verifier?.trim();
|
|
786
|
+
if (!verifier) {
|
|
787
|
+
throw new OAuthWrapperError(
|
|
788
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
789
|
+
400,
|
|
790
|
+
"code_verifier is required"
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
if (authCode.code_challenge_method !== "S256") {
|
|
794
|
+
throw new OAuthWrapperError(
|
|
795
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
796
|
+
400,
|
|
797
|
+
"Unsupported PKCE method"
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
if (!verifyPkceS256(verifier, storedChallenge)) {
|
|
801
|
+
throw new OAuthWrapperError(
|
|
802
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
803
|
+
400,
|
|
804
|
+
"code_verifier mismatch"
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
if (!request.client_secret?.trim()) {
|
|
810
|
+
throw new OAuthWrapperError(
|
|
811
|
+
OAuthRfcCodes.INVALID_CLIENT,
|
|
812
|
+
401,
|
|
813
|
+
"client_secret is required when PKCE is not used"
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
async exchangeRefreshToken(request, verifiedClientId) {
|
|
818
|
+
const tokenHash = hashOpaqueToken(request.refresh_token);
|
|
819
|
+
const stored = await this.oauthRepo.findByTokenHash(tokenHash);
|
|
820
|
+
if (!stored || stored.revoked || stored.client_id !== verifiedClientId || new Date(stored.expires_at) <= /* @__PURE__ */ new Date()) {
|
|
821
|
+
throw new OAuthWrapperError(
|
|
822
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
823
|
+
400,
|
|
824
|
+
"Refresh token is invalid"
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
const providerTokens = await this.fetchProviderAccessToken(stored.user_id);
|
|
828
|
+
await this.oauthRepo.revokeByTokenHash(tokenHash);
|
|
829
|
+
const middlewareRefresh = await this.issueMiddlewareRefreshToken(
|
|
830
|
+
stored.client_id,
|
|
831
|
+
stored.user_id
|
|
832
|
+
);
|
|
833
|
+
return {
|
|
834
|
+
access_token: providerTokens.access_token,
|
|
835
|
+
token_type: "Bearer",
|
|
836
|
+
expires_in: providerTokens.expires_in,
|
|
837
|
+
refresh_token: middlewareRefresh
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
async fetchProviderAccessToken(userId) {
|
|
841
|
+
const credentials = await this.oauthRepo.getUserCredentials(userId);
|
|
842
|
+
const sessionToken = credentials?.provider_session_token?.trim();
|
|
843
|
+
if (!sessionToken) {
|
|
844
|
+
throw new OAuthWrapperError(
|
|
845
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
846
|
+
400,
|
|
847
|
+
"User credentials expired. Re-authorization required."
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
try {
|
|
851
|
+
const access = await this.exchangeProviderAccessToken({
|
|
852
|
+
token: sessionToken
|
|
853
|
+
});
|
|
854
|
+
if (access.refresh_token) {
|
|
855
|
+
await this.oauthRepo.upsertUserCredentials(userId, {
|
|
856
|
+
provider_refresh_token: this.tokenEncryption.encrypt(
|
|
857
|
+
access.refresh_token
|
|
858
|
+
)
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
return {
|
|
862
|
+
access_token: access.access_token,
|
|
863
|
+
expires_in: access.expires_in
|
|
864
|
+
};
|
|
865
|
+
} catch {
|
|
866
|
+
throw new OAuthWrapperError(
|
|
867
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
868
|
+
400,
|
|
869
|
+
"Failed to obtain access token from user provider"
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
async issueMiddlewareRefreshToken(clientId, userId) {
|
|
874
|
+
const plain = (0, import_crypto3.randomBytes)(32).toString("base64url");
|
|
875
|
+
const expiresAt = new Date(
|
|
876
|
+
Date.now() + _OAuthTokenService.REFRESH_TOKEN_TTL_MS
|
|
877
|
+
).toISOString();
|
|
878
|
+
await this.oauthRepo.createRefreshToken({
|
|
879
|
+
refresh_token: hashOpaqueToken(plain),
|
|
880
|
+
client_id: clientId,
|
|
881
|
+
user_id: userId,
|
|
882
|
+
expires_at: expiresAt
|
|
883
|
+
});
|
|
884
|
+
return plain;
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* @override
|
|
888
|
+
* RFC 7009 — revoke middleware refresh tokens. Idempotent: unknown tokens are ignored.
|
|
889
|
+
*/
|
|
890
|
+
async revokeToken(rawFields) {
|
|
891
|
+
let request;
|
|
892
|
+
try {
|
|
893
|
+
request = OAuthTokenRevokeSchema.parse(rawFields);
|
|
894
|
+
} catch {
|
|
895
|
+
throw new OAuthWrapperError(
|
|
896
|
+
OAuthRfcCodes.INVALID_REQUEST,
|
|
897
|
+
400,
|
|
898
|
+
"Malformed revocation request"
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
try {
|
|
902
|
+
await this.oauthRepo.verifyClientCredentials(
|
|
903
|
+
request.client_id,
|
|
904
|
+
request.client_secret
|
|
905
|
+
);
|
|
906
|
+
} catch {
|
|
907
|
+
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
|
|
908
|
+
}
|
|
909
|
+
if (request.token_type_hint && request.token_type_hint !== "refresh_token") {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
const tokenHash = hashOpaqueToken(request.token);
|
|
913
|
+
const stored = await this.oauthRepo.findByTokenHash(tokenHash);
|
|
914
|
+
if (!stored || stored.client_id !== request.client_id) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
await this.oauthRepo.revokeByTokenHash(tokenHash);
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
// src/server/services/OAuthWrapperService.ts
|
|
922
|
+
var OAuthWrapperService = class extends OAuthAbstractProvider {
|
|
923
|
+
constructor(oauthSession, tokenEncryption, oauthRepo) {
|
|
924
|
+
super();
|
|
925
|
+
this.oauthSession = oauthSession;
|
|
926
|
+
this.oauthRepo = oauthRepo;
|
|
927
|
+
this.tokenService = new OAuthTokenService(
|
|
928
|
+
tokenEncryption,
|
|
929
|
+
(params) => this.providerExchangeAccessToken(params),
|
|
930
|
+
oauthRepo
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
oauthSession;
|
|
934
|
+
oauthRepo;
|
|
935
|
+
tokenService;
|
|
936
|
+
/**
|
|
937
|
+
* @override
|
|
938
|
+
*/
|
|
939
|
+
async login(params) {
|
|
940
|
+
const credentials = await this.providerLogin(params);
|
|
941
|
+
const sessionToken = credentials.token;
|
|
942
|
+
if (!sessionToken) {
|
|
943
|
+
throw new Error("User provider login did not return a session token");
|
|
944
|
+
}
|
|
945
|
+
const userInfo = await this.providerGetUserInfo(sessionToken);
|
|
946
|
+
const sessionPayload = this.generageSessionPayload({
|
|
947
|
+
email: params.email,
|
|
948
|
+
...userInfo,
|
|
949
|
+
sessionToken
|
|
950
|
+
});
|
|
951
|
+
this.oauthSession.setSession(sessionPayload);
|
|
952
|
+
const oauthrepo = this.getOAuthRepo();
|
|
953
|
+
await oauthrepo.upsertUserCredentials(sessionPayload.userId, {
|
|
954
|
+
provider_session_token: sessionPayload.providerSessionToken
|
|
955
|
+
});
|
|
956
|
+
return sessionPayload;
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* @override
|
|
960
|
+
*/
|
|
961
|
+
getOAuthRepo() {
|
|
962
|
+
return this.oauthRepo;
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* @override
|
|
966
|
+
*/
|
|
967
|
+
async exchangeToken(rawFields) {
|
|
968
|
+
return await this.tokenService.exchangeToken(rawFields);
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* @override
|
|
972
|
+
*/
|
|
973
|
+
getSession() {
|
|
974
|
+
return this.oauthSession.getSession();
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* @override
|
|
978
|
+
*/
|
|
979
|
+
clearSession() {
|
|
980
|
+
return this.oauthSession.clearSession();
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* @override
|
|
984
|
+
*/
|
|
985
|
+
async revokeToken(rawFields) {
|
|
986
|
+
return await this.tokenService.revokeToken(rawFields);
|
|
987
|
+
}
|
|
938
988
|
async logoutUser(userId) {
|
|
939
989
|
await this.oauthRepo.revokeRefreshTokensByUserId(userId);
|
|
940
990
|
await this.oauthRepo.upsertUserCredentials(userId, {
|
|
@@ -946,9 +996,9 @@ var OAuthWrapperService = class {
|
|
|
946
996
|
/**
|
|
947
997
|
* @override
|
|
948
998
|
*/
|
|
949
|
-
async
|
|
999
|
+
async getUserInfoWithAccessToken(accessToken) {
|
|
950
1000
|
try {
|
|
951
|
-
const profile = await this.
|
|
1001
|
+
const profile = await this.providerGetUserInfoByAccessToken(accessToken);
|
|
952
1002
|
return this.toUserInfoResponse(profile);
|
|
953
1003
|
} catch {
|
|
954
1004
|
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
|
|
@@ -963,8 +1013,7 @@ var OAuthWrapperService = class {
|
|
|
963
1013
|
if (!email) {
|
|
964
1014
|
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
|
|
965
1015
|
}
|
|
966
|
-
const
|
|
967
|
-
const name = profile.name?.trim() || nameFromParts || email;
|
|
1016
|
+
const name = profile.name?.trim() || email;
|
|
968
1017
|
return {
|
|
969
1018
|
sub,
|
|
970
1019
|
email,
|
|
@@ -1000,6 +1049,7 @@ async function verifyClientSecret(secret, storedHash) {
|
|
|
1000
1049
|
}
|
|
1001
1050
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1002
1051
|
0 && (module.exports = {
|
|
1052
|
+
OAuthAbstractProvider,
|
|
1003
1053
|
OAuthAuthorizationCodeRowSchema,
|
|
1004
1054
|
OAuthAuthorizeQuerySchema,
|
|
1005
1055
|
OAuthClientCreateResponseSchema,
|