@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.js
CHANGED
|
@@ -1,116 +1,3 @@
|
|
|
1
|
-
// src/server/services/OAuthClientsService.ts
|
|
2
|
-
var OAuthClientsService = class {
|
|
3
|
-
constructor(clientsRepo) {
|
|
4
|
-
this.clientsRepo = clientsRepo;
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* List all OAuth clients owned by a user
|
|
8
|
-
* @override
|
|
9
|
-
*/
|
|
10
|
-
async listForOwner(ownerUserId) {
|
|
11
|
-
return this.clientsRepo.listClientByOwner(ownerUserId);
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Get detailed information about a specific OAuth client
|
|
15
|
-
* @override
|
|
16
|
-
*/
|
|
17
|
-
async getByClientId(ownerUserId, clientId) {
|
|
18
|
-
const client = await this.clientsRepo.findClientById(clientId);
|
|
19
|
-
if (!client) {
|
|
20
|
-
throw new Error("Client not found");
|
|
21
|
-
}
|
|
22
|
-
if (client.owner_user_id !== ownerUserId) {
|
|
23
|
-
throw new Error("Access denied");
|
|
24
|
-
}
|
|
25
|
-
return this.mapToDetail(client);
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Create a new OAuth client
|
|
29
|
-
* @override
|
|
30
|
-
*/
|
|
31
|
-
async create(ownerUserId, input) {
|
|
32
|
-
const result = await this.clientsRepo.createClient(ownerUserId, input);
|
|
33
|
-
return {
|
|
34
|
-
client_id: result.client.client_id,
|
|
35
|
-
client_secret: result.clientSecret,
|
|
36
|
-
confidential: result.client.confidential,
|
|
37
|
-
client_name: result.client.client_name,
|
|
38
|
-
client_uri: result.client.client_uri,
|
|
39
|
-
redirect_uris: result.client.redirect_uris,
|
|
40
|
-
created_at: result.client.created_at
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Update an existing OAuth client
|
|
45
|
-
* @override
|
|
46
|
-
*/
|
|
47
|
-
async update(ownerUserId, clientId, input) {
|
|
48
|
-
const existing = await this.clientsRepo.findClientById(clientId);
|
|
49
|
-
if (!existing) {
|
|
50
|
-
throw new Error("Client not found");
|
|
51
|
-
}
|
|
52
|
-
if (existing.owner_user_id !== ownerUserId) {
|
|
53
|
-
throw new Error("Access denied");
|
|
54
|
-
}
|
|
55
|
-
return this.clientsRepo.updateClient(ownerUserId, clientId, input);
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Rotate the client secret
|
|
59
|
-
* @override
|
|
60
|
-
*/
|
|
61
|
-
async rotateSecret(ownerUserId, clientId) {
|
|
62
|
-
const existing = await this.clientsRepo.findClientById(clientId);
|
|
63
|
-
if (!existing) {
|
|
64
|
-
throw new Error("Client not found");
|
|
65
|
-
}
|
|
66
|
-
if (existing.owner_user_id !== ownerUserId) {
|
|
67
|
-
throw new Error("Access denied");
|
|
68
|
-
}
|
|
69
|
-
if (!existing.confidential) {
|
|
70
|
-
throw new Error("Public clients do not have a client_secret");
|
|
71
|
-
}
|
|
72
|
-
const result = await this.clientsRepo.rotateClientSecret(
|
|
73
|
-
ownerUserId,
|
|
74
|
-
clientId
|
|
75
|
-
);
|
|
76
|
-
return {
|
|
77
|
-
client_id: clientId,
|
|
78
|
-
client_secret: result.clientSecret
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Delete an OAuth client
|
|
83
|
-
* @override
|
|
84
|
-
*/
|
|
85
|
-
async delete(ownerUserId, clientId) {
|
|
86
|
-
const existing = await this.clientsRepo.findClientById(clientId);
|
|
87
|
-
if (!existing) {
|
|
88
|
-
throw new Error("Client not found");
|
|
89
|
-
}
|
|
90
|
-
if (existing.owner_user_id !== ownerUserId) {
|
|
91
|
-
throw new Error("Access denied");
|
|
92
|
-
}
|
|
93
|
-
await this.clientsRepo.deleteClient(ownerUserId, clientId);
|
|
94
|
-
}
|
|
95
|
-
mapToDetail(row) {
|
|
96
|
-
return {
|
|
97
|
-
client_id: row.client_id,
|
|
98
|
-
client_name: row.client_name,
|
|
99
|
-
client_uri: row.client_uri,
|
|
100
|
-
logo_uri: row.logo_uri,
|
|
101
|
-
redirect_uris: row.redirect_uris,
|
|
102
|
-
grant_types: row.grant_types,
|
|
103
|
-
scopes: row.scopes,
|
|
104
|
-
confidential: row.confidential,
|
|
105
|
-
created_at: row.created_at,
|
|
106
|
-
updated_at: row.updated_at
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
// src/server/services/OAuthTokenService.ts
|
|
112
|
-
import { createHash as createHash2, randomBytes } from "crypto";
|
|
113
|
-
|
|
114
1
|
// src/core/schema/OAuthAuthorizeSchema.ts
|
|
115
2
|
import { z } from "zod";
|
|
116
3
|
function isOAuthRedirectUri(value) {
|
|
@@ -127,7 +14,7 @@ function isOAuthRedirectUri(value) {
|
|
|
127
14
|
}
|
|
128
15
|
var oauthRedirectUriSchema = z.string().min(1).refine(isOAuthRedirectUri, { message: "Invalid redirect URI" });
|
|
129
16
|
var OAuthClientRowSchema = z.object({
|
|
130
|
-
id: z.
|
|
17
|
+
id: z.string(),
|
|
131
18
|
client_id: z.string(),
|
|
132
19
|
client_secret_hash: z.string().nullable().optional(),
|
|
133
20
|
client_name: z.string(),
|
|
@@ -137,7 +24,7 @@ var OAuthClientRowSchema = z.object({
|
|
|
137
24
|
grant_types: z.array(z.string()),
|
|
138
25
|
scopes: z.array(z.string()),
|
|
139
26
|
confidential: z.boolean(),
|
|
140
|
-
owner_user_id: z.
|
|
27
|
+
owner_user_id: z.string(),
|
|
141
28
|
created_at: z.string(),
|
|
142
29
|
updated_at: z.string()
|
|
143
30
|
});
|
|
@@ -210,7 +97,7 @@ var OAuthConsentBodySchema = z.object({
|
|
|
210
97
|
var OAuthAuthorizationCodeRowSchema = z.object({
|
|
211
98
|
code: z.string(),
|
|
212
99
|
client_id: z.string(),
|
|
213
|
-
user_id: z.
|
|
100
|
+
user_id: z.string(),
|
|
214
101
|
redirect_uri: z.string(),
|
|
215
102
|
scope: z.string().nullable().optional(),
|
|
216
103
|
code_challenge: z.string().nullable().optional(),
|
|
@@ -223,16 +110,16 @@ var OAuthAuthorizationCodeRowSchema = z.object({
|
|
|
223
110
|
// src/core/schema/OAuthClientSchema.ts
|
|
224
111
|
import { z as z2 } from "zod";
|
|
225
112
|
var OAuthUserCredentialsSchema = z2.object({
|
|
226
|
-
user_id: z2.
|
|
113
|
+
user_id: z2.string(),
|
|
227
114
|
provider_refresh_token: z2.string().nullable().optional(),
|
|
228
115
|
provider_session_token: z2.string().nullable().optional(),
|
|
229
116
|
updated_at: z2.string()
|
|
230
117
|
});
|
|
231
118
|
var OAuthRefreshTokenSchema = z2.object({
|
|
232
|
-
id: z2.
|
|
119
|
+
id: z2.string(),
|
|
233
120
|
refresh_token: z2.string(),
|
|
234
121
|
client_id: z2.string(),
|
|
235
|
-
user_id: z2.
|
|
122
|
+
user_id: z2.string(),
|
|
236
123
|
expires_at: z2.string(),
|
|
237
124
|
revoked: z2.boolean(),
|
|
238
125
|
created_at: z2.string()
|
|
@@ -249,12 +136,12 @@ var OAuthTokenResponseSchema = z2.object({
|
|
|
249
136
|
import { z as z3 } from "zod";
|
|
250
137
|
var OAuthUserInfoResponseSchema = z3.object({
|
|
251
138
|
sub: z3.string(),
|
|
252
|
-
email: z3.
|
|
139
|
+
email: z3.email(),
|
|
253
140
|
name: z3.string(),
|
|
254
141
|
roles: z3.array(z3.string()).optional()
|
|
255
142
|
});
|
|
256
143
|
var OAuthUserInfoErrorResponseSchema = z3.object({
|
|
257
|
-
error: z3.
|
|
144
|
+
error: z3.string(),
|
|
258
145
|
error_id: z3.string().optional()
|
|
259
146
|
});
|
|
260
147
|
|
|
@@ -335,16 +222,6 @@ function randomOAuthState() {
|
|
|
335
222
|
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
336
223
|
}
|
|
337
224
|
|
|
338
|
-
// src/server/utils/OAuthWrapperError.ts
|
|
339
|
-
import { ExecutorError } from "@qlover/fe-corekit";
|
|
340
|
-
var OAuthWrapperError = class extends ExecutorError {
|
|
341
|
-
constructor(id, status, cause) {
|
|
342
|
-
super(id, cause);
|
|
343
|
-
this.status = status;
|
|
344
|
-
}
|
|
345
|
-
name = "OAuthWrapperError";
|
|
346
|
-
};
|
|
347
|
-
|
|
348
225
|
// src/server/utils/pkce.ts
|
|
349
226
|
import { createHash, timingSafeEqual } from "crypto";
|
|
350
227
|
var PKCE_VERIFIER_MIN = 43;
|
|
@@ -378,267 +255,43 @@ function verifyPkceS256(verifier, challenge) {
|
|
|
378
255
|
}
|
|
379
256
|
}
|
|
380
257
|
|
|
381
|
-
// src/server/
|
|
382
|
-
function
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
static REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
392
|
-
/**
|
|
393
|
-
* @override
|
|
394
|
-
*/
|
|
395
|
-
async exchangeToken(rawFields) {
|
|
396
|
-
let request;
|
|
397
|
-
try {
|
|
398
|
-
request = OAuthTokenRequestSchema.parse(rawFields);
|
|
399
|
-
} catch {
|
|
400
|
-
throw new OAuthWrapperError(
|
|
401
|
-
OAuthRfcCodes.INVALID_REQUEST,
|
|
402
|
-
400,
|
|
403
|
-
"Malformed token request"
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
let client;
|
|
407
|
-
try {
|
|
408
|
-
client = await this.oauthRepo.verifyClientCredentials(
|
|
409
|
-
request.client_id,
|
|
410
|
-
request.client_secret
|
|
411
|
-
);
|
|
412
|
-
} catch {
|
|
413
|
-
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
|
|
414
|
-
}
|
|
415
|
-
if (!client.grant_types.includes(request.grant_type)) {
|
|
416
|
-
throw new OAuthWrapperError(OAuthRfcCodes.UNSUPPORTED_GRANT_TYPE, 400);
|
|
258
|
+
// src/server/utils/authorizeUtil.ts
|
|
259
|
+
function validatePkceParams(parsed, confidential) {
|
|
260
|
+
const hasChallenge = Boolean(parsed.code_challenge?.trim());
|
|
261
|
+
const hasMethod = Boolean(parsed.code_challenge_method);
|
|
262
|
+
if (!confidential) {
|
|
263
|
+
if (!hasChallenge || parsed.code_challenge_method !== "S256") {
|
|
264
|
+
return {
|
|
265
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
266
|
+
message: "Public clients must send code_challenge and code_challenge_method=S256."
|
|
267
|
+
};
|
|
417
268
|
}
|
|
418
|
-
if (
|
|
419
|
-
return
|
|
269
|
+
if (!isValidCodeChallenge(parsed.code_challenge)) {
|
|
270
|
+
return {
|
|
271
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
272
|
+
message: "Invalid code_challenge."
|
|
273
|
+
};
|
|
420
274
|
}
|
|
421
|
-
return
|
|
275
|
+
return null;
|
|
422
276
|
}
|
|
423
|
-
|
|
424
|
-
const authCode = await this.oauthRepo.consumeCode(request.code);
|
|
425
|
-
if (!authCode) {
|
|
426
|
-
throw new OAuthWrapperError(
|
|
427
|
-
OAuthRfcCodes.INVALID_GRANT,
|
|
428
|
-
400,
|
|
429
|
-
"Authorization code is invalid, expired, or already used"
|
|
430
|
-
);
|
|
431
|
-
}
|
|
432
|
-
if (authCode.client_id !== verifiedClientId) {
|
|
433
|
-
throw new OAuthWrapperError(
|
|
434
|
-
OAuthRfcCodes.INVALID_GRANT,
|
|
435
|
-
400,
|
|
436
|
-
"client_id mismatch"
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
|
-
if (authCode.redirect_uri !== request.redirect_uri) {
|
|
440
|
-
throw new OAuthWrapperError(
|
|
441
|
-
OAuthRfcCodes.INVALID_GRANT,
|
|
442
|
-
400,
|
|
443
|
-
"redirect_uri mismatch"
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
this.assertPkceForAuthorizationCode(request, authCode);
|
|
447
|
-
const providerTokens = await this.fetchProviderAccessToken(
|
|
448
|
-
authCode.user_id
|
|
449
|
-
);
|
|
450
|
-
const middlewareRefresh = await this.issueMiddlewareRefreshToken(
|
|
451
|
-
authCode.client_id,
|
|
452
|
-
authCode.user_id
|
|
453
|
-
);
|
|
277
|
+
if (hasChallenge !== hasMethod) {
|
|
454
278
|
return {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
expires_in: providerTokens.expires_in,
|
|
458
|
-
refresh_token: middlewareRefresh,
|
|
459
|
-
scope: authCode.scope ?? void 0
|
|
279
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
280
|
+
message: "code_challenge and code_challenge_method must be sent together."
|
|
460
281
|
};
|
|
461
282
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
OAuthRfcCodes.INVALID_GRANT,
|
|
469
|
-
400,
|
|
470
|
-
"code_verifier is required"
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
|
-
if (authCode.code_challenge_method !== "S256") {
|
|
474
|
-
throw new OAuthWrapperError(
|
|
475
|
-
OAuthRfcCodes.INVALID_GRANT,
|
|
476
|
-
400,
|
|
477
|
-
"Unsupported PKCE method"
|
|
478
|
-
);
|
|
479
|
-
}
|
|
480
|
-
if (!verifyPkceS256(verifier, storedChallenge)) {
|
|
481
|
-
throw new OAuthWrapperError(
|
|
482
|
-
OAuthRfcCodes.INVALID_GRANT,
|
|
483
|
-
400,
|
|
484
|
-
"code_verifier mismatch"
|
|
485
|
-
);
|
|
486
|
-
}
|
|
487
|
-
return;
|
|
283
|
+
if (hasChallenge) {
|
|
284
|
+
if (parsed.code_challenge_method !== "S256") {
|
|
285
|
+
return {
|
|
286
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
287
|
+
message: "Only code_challenge_method=S256 is supported."
|
|
288
|
+
};
|
|
488
289
|
}
|
|
489
|
-
if (!
|
|
490
|
-
|
|
491
|
-
OAuthRfcCodes.
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
async exchangeRefreshToken(request, verifiedClientId) {
|
|
498
|
-
const tokenHash = hashOpaqueToken(request.refresh_token);
|
|
499
|
-
const stored = await this.oauthRepo.findByTokenHash(tokenHash);
|
|
500
|
-
if (!stored || stored.revoked || stored.client_id !== verifiedClientId || new Date(stored.expires_at) <= /* @__PURE__ */ new Date()) {
|
|
501
|
-
throw new OAuthWrapperError(
|
|
502
|
-
OAuthRfcCodes.INVALID_GRANT,
|
|
503
|
-
400,
|
|
504
|
-
"Refresh token is invalid"
|
|
505
|
-
);
|
|
506
|
-
}
|
|
507
|
-
const providerTokens = await this.fetchProviderAccessToken(stored.user_id);
|
|
508
|
-
await this.oauthRepo.revokeByTokenHash(tokenHash);
|
|
509
|
-
const middlewareRefresh = await this.issueMiddlewareRefreshToken(
|
|
510
|
-
stored.client_id,
|
|
511
|
-
stored.user_id
|
|
512
|
-
);
|
|
513
|
-
return {
|
|
514
|
-
access_token: providerTokens.access_token,
|
|
515
|
-
token_type: "Bearer",
|
|
516
|
-
expires_in: providerTokens.expires_in,
|
|
517
|
-
refresh_token: middlewareRefresh
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
async fetchProviderAccessToken(userId) {
|
|
521
|
-
const credentials = await this.oauthRepo.getUserCredentials(userId);
|
|
522
|
-
const sessionToken = credentials?.provider_session_token?.trim();
|
|
523
|
-
if (!sessionToken) {
|
|
524
|
-
throw new OAuthWrapperError(
|
|
525
|
-
OAuthRfcCodes.INVALID_GRANT,
|
|
526
|
-
400,
|
|
527
|
-
"User credentials expired. Re-authorization required."
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
try {
|
|
531
|
-
const access = await this.userAdapter.exchangeAccessToken({
|
|
532
|
-
token: sessionToken
|
|
533
|
-
});
|
|
534
|
-
if (access.refresh_token) {
|
|
535
|
-
await this.oauthRepo.upsertUserCredentials(userId, {
|
|
536
|
-
provider_refresh_token: this.tokenEncryption.encrypt(
|
|
537
|
-
access.refresh_token
|
|
538
|
-
)
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
return {
|
|
542
|
-
access_token: access.access_token,
|
|
543
|
-
expires_in: access.expires_in
|
|
544
|
-
};
|
|
545
|
-
} catch {
|
|
546
|
-
throw new OAuthWrapperError(
|
|
547
|
-
OAuthRfcCodes.INVALID_GRANT,
|
|
548
|
-
400,
|
|
549
|
-
"Failed to obtain access token from user provider"
|
|
550
|
-
);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
async issueMiddlewareRefreshToken(clientId, userId) {
|
|
554
|
-
const plain = randomBytes(32).toString("base64url");
|
|
555
|
-
const expiresAt = new Date(
|
|
556
|
-
Date.now() + _OAuthTokenService.REFRESH_TOKEN_TTL_MS
|
|
557
|
-
).toISOString();
|
|
558
|
-
await this.oauthRepo.createRefreshToken({
|
|
559
|
-
refresh_token: hashOpaqueToken(plain),
|
|
560
|
-
client_id: clientId,
|
|
561
|
-
user_id: userId,
|
|
562
|
-
expires_at: expiresAt
|
|
563
|
-
});
|
|
564
|
-
return plain;
|
|
565
|
-
}
|
|
566
|
-
/**
|
|
567
|
-
* @override
|
|
568
|
-
* RFC 7009 — revoke middleware refresh tokens. Idempotent: unknown tokens are ignored.
|
|
569
|
-
*/
|
|
570
|
-
async revokeToken(rawFields) {
|
|
571
|
-
let request;
|
|
572
|
-
try {
|
|
573
|
-
request = OAuthTokenRevokeSchema.parse(rawFields);
|
|
574
|
-
} catch {
|
|
575
|
-
throw new OAuthWrapperError(
|
|
576
|
-
OAuthRfcCodes.INVALID_REQUEST,
|
|
577
|
-
400,
|
|
578
|
-
"Malformed revocation request"
|
|
579
|
-
);
|
|
580
|
-
}
|
|
581
|
-
try {
|
|
582
|
-
await this.oauthRepo.verifyClientCredentials(
|
|
583
|
-
request.client_id,
|
|
584
|
-
request.client_secret
|
|
585
|
-
);
|
|
586
|
-
} catch {
|
|
587
|
-
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
|
|
588
|
-
}
|
|
589
|
-
if (request.token_type_hint && request.token_type_hint !== "refresh_token") {
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
const tokenHash = hashOpaqueToken(request.token);
|
|
593
|
-
const stored = await this.oauthRepo.findByTokenHash(tokenHash);
|
|
594
|
-
if (!stored || stored.client_id !== request.client_id) {
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
await this.oauthRepo.revokeByTokenHash(tokenHash);
|
|
598
|
-
}
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
// src/server/services/OAuthWrapperService.ts
|
|
602
|
-
import { randomBytes as randomBytes2 } from "crypto";
|
|
603
|
-
import { ExecutorError as ExecutorError2 } from "@qlover/fe-corekit";
|
|
604
|
-
|
|
605
|
-
// src/server/utils/authorizeUtil.ts
|
|
606
|
-
function validatePkceParams(parsed, confidential) {
|
|
607
|
-
const hasChallenge = Boolean(parsed.code_challenge?.trim());
|
|
608
|
-
const hasMethod = Boolean(parsed.code_challenge_method);
|
|
609
|
-
if (!confidential) {
|
|
610
|
-
if (!hasChallenge || parsed.code_challenge_method !== "S256") {
|
|
611
|
-
return {
|
|
612
|
-
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
613
|
-
message: "Public clients must send code_challenge and code_challenge_method=S256."
|
|
614
|
-
};
|
|
615
|
-
}
|
|
616
|
-
if (!isValidCodeChallenge(parsed.code_challenge)) {
|
|
617
|
-
return {
|
|
618
|
-
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
619
|
-
message: "Invalid code_challenge."
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
-
return null;
|
|
623
|
-
}
|
|
624
|
-
if (hasChallenge !== hasMethod) {
|
|
625
|
-
return {
|
|
626
|
-
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
627
|
-
message: "code_challenge and code_challenge_method must be sent together."
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
if (hasChallenge) {
|
|
631
|
-
if (parsed.code_challenge_method !== "S256") {
|
|
632
|
-
return {
|
|
633
|
-
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
634
|
-
message: "Only code_challenge_method=S256 is supported."
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
if (!isValidCodeChallenge(parsed.code_challenge)) {
|
|
638
|
-
return {
|
|
639
|
-
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
640
|
-
message: "Invalid code_challenge."
|
|
641
|
-
};
|
|
290
|
+
if (!isValidCodeChallenge(parsed.code_challenge)) {
|
|
291
|
+
return {
|
|
292
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
293
|
+
message: "Invalid code_challenge."
|
|
294
|
+
};
|
|
642
295
|
}
|
|
643
296
|
}
|
|
644
297
|
return null;
|
|
@@ -678,44 +331,36 @@ function parseScopeList(scope) {
|
|
|
678
331
|
return scope.trim().split(/\s+/).filter(Boolean);
|
|
679
332
|
}
|
|
680
333
|
|
|
681
|
-
// src/server/services/
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
this.oauthRepo = oauthRepo;
|
|
689
|
-
}
|
|
690
|
-
/**
|
|
691
|
-
* @override
|
|
692
|
-
*/
|
|
693
|
-
getOAuthSession() {
|
|
694
|
-
return this.oauthSession;
|
|
334
|
+
// src/server/services/OAuthAbstractProvider.ts
|
|
335
|
+
import { ExecutorError } from "@qlover/fe-corekit";
|
|
336
|
+
import { randomBytes } from "crypto";
|
|
337
|
+
var OAuthAbstractProvider = class {
|
|
338
|
+
authCodeTTLMs = 5 * 60 * 1e3;
|
|
339
|
+
isQuery(query) {
|
|
340
|
+
return OAuthAuthorizeQuerySchema.safeParse(query).success;
|
|
695
341
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
*/
|
|
699
|
-
getOAuthAdapter() {
|
|
700
|
-
return this.userAdapter;
|
|
342
|
+
isValidateConsent(value) {
|
|
343
|
+
return OAuthConsentBodySchema.safeParse(value).success;
|
|
701
344
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
345
|
+
generageSessionPayload(userInfo) {
|
|
346
|
+
return {
|
|
347
|
+
userId: String(userInfo.id),
|
|
348
|
+
email: userInfo.email,
|
|
349
|
+
name: userInfo.name ?? userInfo.email,
|
|
350
|
+
providerSessionToken: userInfo.sessionToken
|
|
351
|
+
};
|
|
707
352
|
}
|
|
708
353
|
/**
|
|
709
354
|
* @override
|
|
710
355
|
*/
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
356
|
+
async logout(userId) {
|
|
357
|
+
const oauthRepo = this.getOAuthRepo();
|
|
358
|
+
await oauthRepo.revokeRefreshTokensByUserId(userId);
|
|
359
|
+
await oauthRepo.upsertUserCredentials(userId, {
|
|
360
|
+
provider_refresh_token: null,
|
|
361
|
+
provider_session_token: null
|
|
362
|
+
});
|
|
363
|
+
await this.clearSession();
|
|
719
364
|
}
|
|
720
365
|
/**
|
|
721
366
|
* @override
|
|
@@ -740,7 +385,8 @@ var OAuthWrapperService = class {
|
|
|
740
385
|
}
|
|
741
386
|
};
|
|
742
387
|
}
|
|
743
|
-
const
|
|
388
|
+
const oauthRepo = this.getOAuthRepo();
|
|
389
|
+
const client = await oauthRepo.findClientById(query.client_id);
|
|
744
390
|
if (!client) {
|
|
745
391
|
return {
|
|
746
392
|
ok: false,
|
|
@@ -798,14 +444,14 @@ var OAuthWrapperService = class {
|
|
|
798
444
|
*/
|
|
799
445
|
async processConsent(requestBody) {
|
|
800
446
|
if (!this.isValidateConsent(requestBody)) {
|
|
801
|
-
throw new
|
|
447
|
+
throw new ExecutorError(
|
|
802
448
|
OAuthRfcCodes.INVALID_REQUEST,
|
|
803
449
|
"Invalid consent request body"
|
|
804
450
|
);
|
|
805
451
|
}
|
|
806
|
-
const session = await this.
|
|
452
|
+
const session = await this.getSession();
|
|
807
453
|
if (!session) {
|
|
808
|
-
throw new
|
|
454
|
+
throw new ExecutorError(
|
|
809
455
|
OAuthRfcCodes.ACCESS_DENIED,
|
|
810
456
|
"User session expired. Please sign in again."
|
|
811
457
|
);
|
|
@@ -820,7 +466,7 @@ var OAuthWrapperService = class {
|
|
|
820
466
|
code_challenge_method: requestBody.code_challenge_method
|
|
821
467
|
});
|
|
822
468
|
if (!pageResult.ok) {
|
|
823
|
-
throw new
|
|
469
|
+
throw new ExecutorError(
|
|
824
470
|
pageResult.error.errorKey,
|
|
825
471
|
pageResult.error.message
|
|
826
472
|
);
|
|
@@ -835,9 +481,10 @@ var OAuthWrapperService = class {
|
|
|
835
481
|
})
|
|
836
482
|
};
|
|
837
483
|
}
|
|
838
|
-
const code =
|
|
839
|
-
const expiresAt = new Date(Date.now() +
|
|
840
|
-
|
|
484
|
+
const code = randomBytes(32).toString("base64url");
|
|
485
|
+
const expiresAt = new Date(Date.now() + this.authCodeTTLMs).toISOString();
|
|
486
|
+
const oauthRepo = this.getOAuthRepo();
|
|
487
|
+
await oauthRepo.create({
|
|
841
488
|
code,
|
|
842
489
|
client_id: data.clientId,
|
|
843
490
|
user_id: session.userId,
|
|
@@ -855,21 +502,423 @@ var OAuthWrapperService = class {
|
|
|
855
502
|
})
|
|
856
503
|
};
|
|
857
504
|
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// src/server/services/OAuthClientsService.ts
|
|
508
|
+
var OAuthClientsService = class {
|
|
509
|
+
constructor(clientsRepo) {
|
|
510
|
+
this.clientsRepo = clientsRepo;
|
|
511
|
+
}
|
|
512
|
+
clientsRepo;
|
|
858
513
|
/**
|
|
514
|
+
* List all OAuth clients owned by a user
|
|
859
515
|
* @override
|
|
860
516
|
*/
|
|
861
|
-
async
|
|
862
|
-
return
|
|
517
|
+
async listForOwner(ownerUserId) {
|
|
518
|
+
return this.clientsRepo.listClientByOwner(ownerUserId);
|
|
863
519
|
}
|
|
864
520
|
/**
|
|
521
|
+
* Get detailed information about a specific OAuth client
|
|
865
522
|
* @override
|
|
866
523
|
*/
|
|
867
|
-
async
|
|
868
|
-
|
|
524
|
+
async getByClientId(ownerUserId, clientId) {
|
|
525
|
+
const client = await this.clientsRepo.findClientById(clientId);
|
|
526
|
+
if (!client) {
|
|
527
|
+
throw new Error("Client not found");
|
|
528
|
+
}
|
|
529
|
+
if (client.owner_user_id !== ownerUserId) {
|
|
530
|
+
throw new Error("Access denied");
|
|
531
|
+
}
|
|
532
|
+
return this.mapToDetail(client);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Create a new OAuth client
|
|
536
|
+
* @override
|
|
537
|
+
*/
|
|
538
|
+
async create(ownerUserId, input) {
|
|
539
|
+
const result = await this.clientsRepo.createClient(ownerUserId, input);
|
|
540
|
+
return {
|
|
541
|
+
client_id: result.client.client_id,
|
|
542
|
+
client_secret: result.clientSecret,
|
|
543
|
+
confidential: result.client.confidential,
|
|
544
|
+
client_name: result.client.client_name,
|
|
545
|
+
client_uri: result.client.client_uri,
|
|
546
|
+
redirect_uris: result.client.redirect_uris,
|
|
547
|
+
created_at: result.client.created_at
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Update an existing OAuth client
|
|
552
|
+
* @override
|
|
553
|
+
*/
|
|
554
|
+
async update(ownerUserId, clientId, input) {
|
|
555
|
+
const existing = await this.clientsRepo.findClientById(clientId);
|
|
556
|
+
if (!existing) {
|
|
557
|
+
throw new Error("Client not found");
|
|
558
|
+
}
|
|
559
|
+
if (existing.owner_user_id !== ownerUserId) {
|
|
560
|
+
throw new Error("Access denied");
|
|
561
|
+
}
|
|
562
|
+
return this.clientsRepo.updateClient(ownerUserId, clientId, input);
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Rotate the client secret
|
|
566
|
+
* @override
|
|
567
|
+
*/
|
|
568
|
+
async rotateSecret(ownerUserId, clientId) {
|
|
569
|
+
const existing = await this.clientsRepo.findClientById(clientId);
|
|
570
|
+
if (!existing) {
|
|
571
|
+
throw new Error("Client not found");
|
|
572
|
+
}
|
|
573
|
+
if (existing.owner_user_id !== ownerUserId) {
|
|
574
|
+
throw new Error("Access denied");
|
|
575
|
+
}
|
|
576
|
+
if (!existing.confidential) {
|
|
577
|
+
throw new Error("Public clients do not have a client_secret");
|
|
578
|
+
}
|
|
579
|
+
const result = await this.clientsRepo.rotateClientSecret(
|
|
580
|
+
ownerUserId,
|
|
581
|
+
clientId
|
|
582
|
+
);
|
|
583
|
+
return {
|
|
584
|
+
client_id: clientId,
|
|
585
|
+
client_secret: result.clientSecret
|
|
586
|
+
};
|
|
869
587
|
}
|
|
870
588
|
/**
|
|
589
|
+
* Delete an OAuth client
|
|
871
590
|
* @override
|
|
872
591
|
*/
|
|
592
|
+
async delete(ownerUserId, clientId) {
|
|
593
|
+
const existing = await this.clientsRepo.findClientById(clientId);
|
|
594
|
+
if (!existing) {
|
|
595
|
+
throw new Error("Client not found");
|
|
596
|
+
}
|
|
597
|
+
if (existing.owner_user_id !== ownerUserId) {
|
|
598
|
+
throw new Error("Access denied");
|
|
599
|
+
}
|
|
600
|
+
await this.clientsRepo.deleteClient(ownerUserId, clientId);
|
|
601
|
+
}
|
|
602
|
+
mapToDetail(row) {
|
|
603
|
+
return {
|
|
604
|
+
client_id: row.client_id,
|
|
605
|
+
client_name: row.client_name,
|
|
606
|
+
client_uri: row.client_uri,
|
|
607
|
+
logo_uri: row.logo_uri,
|
|
608
|
+
redirect_uris: row.redirect_uris,
|
|
609
|
+
grant_types: row.grant_types,
|
|
610
|
+
scopes: row.scopes,
|
|
611
|
+
confidential: row.confidential,
|
|
612
|
+
created_at: row.created_at,
|
|
613
|
+
updated_at: row.updated_at
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
// src/server/services/OAuthTokenService.ts
|
|
619
|
+
import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
|
|
620
|
+
|
|
621
|
+
// src/server/utils/OAuthWrapperError.ts
|
|
622
|
+
import { ExecutorError as ExecutorError2 } from "@qlover/fe-corekit";
|
|
623
|
+
var OAuthWrapperError = class extends ExecutorError2 {
|
|
624
|
+
constructor(id, status, cause) {
|
|
625
|
+
super(id, cause);
|
|
626
|
+
this.status = status;
|
|
627
|
+
}
|
|
628
|
+
status;
|
|
629
|
+
name = "OAuthWrapperError";
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
// src/server/services/OAuthTokenService.ts
|
|
633
|
+
function hashOpaqueToken(token) {
|
|
634
|
+
return createHash2("sha256").update(token).digest("hex");
|
|
635
|
+
}
|
|
636
|
+
var OAuthTokenService = class _OAuthTokenService {
|
|
637
|
+
constructor(tokenEncryption, exchangeProviderAccessToken, oauthRepo) {
|
|
638
|
+
this.tokenEncryption = tokenEncryption;
|
|
639
|
+
this.exchangeProviderAccessToken = exchangeProviderAccessToken;
|
|
640
|
+
this.oauthRepo = oauthRepo;
|
|
641
|
+
}
|
|
642
|
+
tokenEncryption;
|
|
643
|
+
exchangeProviderAccessToken;
|
|
644
|
+
oauthRepo;
|
|
645
|
+
static REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
646
|
+
/**
|
|
647
|
+
* @override
|
|
648
|
+
*/
|
|
649
|
+
async exchangeToken(rawFields) {
|
|
650
|
+
let request;
|
|
651
|
+
try {
|
|
652
|
+
request = OAuthTokenRequestSchema.parse(rawFields);
|
|
653
|
+
} catch {
|
|
654
|
+
throw new OAuthWrapperError(
|
|
655
|
+
OAuthRfcCodes.INVALID_REQUEST,
|
|
656
|
+
400,
|
|
657
|
+
"Malformed token request"
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
let client;
|
|
661
|
+
try {
|
|
662
|
+
client = await this.oauthRepo.verifyClientCredentials(
|
|
663
|
+
request.client_id,
|
|
664
|
+
request.client_secret
|
|
665
|
+
);
|
|
666
|
+
} catch {
|
|
667
|
+
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
|
|
668
|
+
}
|
|
669
|
+
if (!client.grant_types.includes(request.grant_type)) {
|
|
670
|
+
throw new OAuthWrapperError(OAuthRfcCodes.UNSUPPORTED_GRANT_TYPE, 400);
|
|
671
|
+
}
|
|
672
|
+
if (request.grant_type === "authorization_code") {
|
|
673
|
+
return await this.exchangeAuthorizationCode(request, client.client_id);
|
|
674
|
+
}
|
|
675
|
+
return await this.exchangeRefreshToken(request, client.client_id);
|
|
676
|
+
}
|
|
677
|
+
async exchangeAuthorizationCode(request, verifiedClientId) {
|
|
678
|
+
const authCode = await this.oauthRepo.consumeCode(request.code);
|
|
679
|
+
if (!authCode) {
|
|
680
|
+
throw new OAuthWrapperError(
|
|
681
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
682
|
+
400,
|
|
683
|
+
"Authorization code is invalid, expired, or already used"
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
if (authCode.client_id !== verifiedClientId) {
|
|
687
|
+
throw new OAuthWrapperError(
|
|
688
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
689
|
+
400,
|
|
690
|
+
"client_id mismatch"
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
if (authCode.redirect_uri !== request.redirect_uri) {
|
|
694
|
+
throw new OAuthWrapperError(
|
|
695
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
696
|
+
400,
|
|
697
|
+
"redirect_uri mismatch"
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
this.assertPkceForAuthorizationCode(request, authCode);
|
|
701
|
+
const providerTokens = await this.fetchProviderAccessToken(
|
|
702
|
+
authCode.user_id
|
|
703
|
+
);
|
|
704
|
+
const middlewareRefresh = await this.issueMiddlewareRefreshToken(
|
|
705
|
+
authCode.client_id,
|
|
706
|
+
authCode.user_id
|
|
707
|
+
);
|
|
708
|
+
return {
|
|
709
|
+
access_token: providerTokens.access_token,
|
|
710
|
+
token_type: "Bearer",
|
|
711
|
+
expires_in: providerTokens.expires_in,
|
|
712
|
+
refresh_token: middlewareRefresh,
|
|
713
|
+
scope: authCode.scope ?? void 0
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
assertPkceForAuthorizationCode(request, authCode) {
|
|
717
|
+
const storedChallenge = authCode.code_challenge?.trim();
|
|
718
|
+
if (storedChallenge) {
|
|
719
|
+
const verifier = request.code_verifier?.trim();
|
|
720
|
+
if (!verifier) {
|
|
721
|
+
throw new OAuthWrapperError(
|
|
722
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
723
|
+
400,
|
|
724
|
+
"code_verifier is required"
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
if (authCode.code_challenge_method !== "S256") {
|
|
728
|
+
throw new OAuthWrapperError(
|
|
729
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
730
|
+
400,
|
|
731
|
+
"Unsupported PKCE method"
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
if (!verifyPkceS256(verifier, storedChallenge)) {
|
|
735
|
+
throw new OAuthWrapperError(
|
|
736
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
737
|
+
400,
|
|
738
|
+
"code_verifier mismatch"
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (!request.client_secret?.trim()) {
|
|
744
|
+
throw new OAuthWrapperError(
|
|
745
|
+
OAuthRfcCodes.INVALID_CLIENT,
|
|
746
|
+
401,
|
|
747
|
+
"client_secret is required when PKCE is not used"
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
async exchangeRefreshToken(request, verifiedClientId) {
|
|
752
|
+
const tokenHash = hashOpaqueToken(request.refresh_token);
|
|
753
|
+
const stored = await this.oauthRepo.findByTokenHash(tokenHash);
|
|
754
|
+
if (!stored || stored.revoked || stored.client_id !== verifiedClientId || new Date(stored.expires_at) <= /* @__PURE__ */ new Date()) {
|
|
755
|
+
throw new OAuthWrapperError(
|
|
756
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
757
|
+
400,
|
|
758
|
+
"Refresh token is invalid"
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
const providerTokens = await this.fetchProviderAccessToken(stored.user_id);
|
|
762
|
+
await this.oauthRepo.revokeByTokenHash(tokenHash);
|
|
763
|
+
const middlewareRefresh = await this.issueMiddlewareRefreshToken(
|
|
764
|
+
stored.client_id,
|
|
765
|
+
stored.user_id
|
|
766
|
+
);
|
|
767
|
+
return {
|
|
768
|
+
access_token: providerTokens.access_token,
|
|
769
|
+
token_type: "Bearer",
|
|
770
|
+
expires_in: providerTokens.expires_in,
|
|
771
|
+
refresh_token: middlewareRefresh
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
async fetchProviderAccessToken(userId) {
|
|
775
|
+
const credentials = await this.oauthRepo.getUserCredentials(userId);
|
|
776
|
+
const sessionToken = credentials?.provider_session_token?.trim();
|
|
777
|
+
if (!sessionToken) {
|
|
778
|
+
throw new OAuthWrapperError(
|
|
779
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
780
|
+
400,
|
|
781
|
+
"User credentials expired. Re-authorization required."
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
try {
|
|
785
|
+
const access = await this.exchangeProviderAccessToken({
|
|
786
|
+
token: sessionToken
|
|
787
|
+
});
|
|
788
|
+
if (access.refresh_token) {
|
|
789
|
+
await this.oauthRepo.upsertUserCredentials(userId, {
|
|
790
|
+
provider_refresh_token: this.tokenEncryption.encrypt(
|
|
791
|
+
access.refresh_token
|
|
792
|
+
)
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
access_token: access.access_token,
|
|
797
|
+
expires_in: access.expires_in
|
|
798
|
+
};
|
|
799
|
+
} catch {
|
|
800
|
+
throw new OAuthWrapperError(
|
|
801
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
802
|
+
400,
|
|
803
|
+
"Failed to obtain access token from user provider"
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
async issueMiddlewareRefreshToken(clientId, userId) {
|
|
808
|
+
const plain = randomBytes2(32).toString("base64url");
|
|
809
|
+
const expiresAt = new Date(
|
|
810
|
+
Date.now() + _OAuthTokenService.REFRESH_TOKEN_TTL_MS
|
|
811
|
+
).toISOString();
|
|
812
|
+
await this.oauthRepo.createRefreshToken({
|
|
813
|
+
refresh_token: hashOpaqueToken(plain),
|
|
814
|
+
client_id: clientId,
|
|
815
|
+
user_id: userId,
|
|
816
|
+
expires_at: expiresAt
|
|
817
|
+
});
|
|
818
|
+
return plain;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* @override
|
|
822
|
+
* RFC 7009 — revoke middleware refresh tokens. Idempotent: unknown tokens are ignored.
|
|
823
|
+
*/
|
|
824
|
+
async revokeToken(rawFields) {
|
|
825
|
+
let request;
|
|
826
|
+
try {
|
|
827
|
+
request = OAuthTokenRevokeSchema.parse(rawFields);
|
|
828
|
+
} catch {
|
|
829
|
+
throw new OAuthWrapperError(
|
|
830
|
+
OAuthRfcCodes.INVALID_REQUEST,
|
|
831
|
+
400,
|
|
832
|
+
"Malformed revocation request"
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
try {
|
|
836
|
+
await this.oauthRepo.verifyClientCredentials(
|
|
837
|
+
request.client_id,
|
|
838
|
+
request.client_secret
|
|
839
|
+
);
|
|
840
|
+
} catch {
|
|
841
|
+
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
|
|
842
|
+
}
|
|
843
|
+
if (request.token_type_hint && request.token_type_hint !== "refresh_token") {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
const tokenHash = hashOpaqueToken(request.token);
|
|
847
|
+
const stored = await this.oauthRepo.findByTokenHash(tokenHash);
|
|
848
|
+
if (!stored || stored.client_id !== request.client_id) {
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
await this.oauthRepo.revokeByTokenHash(tokenHash);
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
// src/server/services/OAuthWrapperService.ts
|
|
856
|
+
var OAuthWrapperService = class extends OAuthAbstractProvider {
|
|
857
|
+
constructor(oauthSession, tokenEncryption, oauthRepo) {
|
|
858
|
+
super();
|
|
859
|
+
this.oauthSession = oauthSession;
|
|
860
|
+
this.oauthRepo = oauthRepo;
|
|
861
|
+
this.tokenService = new OAuthTokenService(
|
|
862
|
+
tokenEncryption,
|
|
863
|
+
(params) => this.providerExchangeAccessToken(params),
|
|
864
|
+
oauthRepo
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
oauthSession;
|
|
868
|
+
oauthRepo;
|
|
869
|
+
tokenService;
|
|
870
|
+
/**
|
|
871
|
+
* @override
|
|
872
|
+
*/
|
|
873
|
+
async login(params) {
|
|
874
|
+
const credentials = await this.providerLogin(params);
|
|
875
|
+
const sessionToken = credentials.token;
|
|
876
|
+
if (!sessionToken) {
|
|
877
|
+
throw new Error("User provider login did not return a session token");
|
|
878
|
+
}
|
|
879
|
+
const userInfo = await this.providerGetUserInfo(sessionToken);
|
|
880
|
+
const sessionPayload = this.generageSessionPayload({
|
|
881
|
+
email: params.email,
|
|
882
|
+
...userInfo,
|
|
883
|
+
sessionToken
|
|
884
|
+
});
|
|
885
|
+
this.oauthSession.setSession(sessionPayload);
|
|
886
|
+
const oauthrepo = this.getOAuthRepo();
|
|
887
|
+
await oauthrepo.upsertUserCredentials(sessionPayload.userId, {
|
|
888
|
+
provider_session_token: sessionPayload.providerSessionToken
|
|
889
|
+
});
|
|
890
|
+
return sessionPayload;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* @override
|
|
894
|
+
*/
|
|
895
|
+
getOAuthRepo() {
|
|
896
|
+
return this.oauthRepo;
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* @override
|
|
900
|
+
*/
|
|
901
|
+
async exchangeToken(rawFields) {
|
|
902
|
+
return await this.tokenService.exchangeToken(rawFields);
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* @override
|
|
906
|
+
*/
|
|
907
|
+
getSession() {
|
|
908
|
+
return this.oauthSession.getSession();
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* @override
|
|
912
|
+
*/
|
|
913
|
+
clearSession() {
|
|
914
|
+
return this.oauthSession.clearSession();
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* @override
|
|
918
|
+
*/
|
|
919
|
+
async revokeToken(rawFields) {
|
|
920
|
+
return await this.tokenService.revokeToken(rawFields);
|
|
921
|
+
}
|
|
873
922
|
async logoutUser(userId) {
|
|
874
923
|
await this.oauthRepo.revokeRefreshTokensByUserId(userId);
|
|
875
924
|
await this.oauthRepo.upsertUserCredentials(userId, {
|
|
@@ -881,9 +930,9 @@ var OAuthWrapperService = class {
|
|
|
881
930
|
/**
|
|
882
931
|
* @override
|
|
883
932
|
*/
|
|
884
|
-
async
|
|
933
|
+
async getUserInfoWithAccessToken(accessToken) {
|
|
885
934
|
try {
|
|
886
|
-
const profile = await this.
|
|
935
|
+
const profile = await this.providerGetUserInfoByAccessToken(accessToken);
|
|
887
936
|
return this.toUserInfoResponse(profile);
|
|
888
937
|
} catch {
|
|
889
938
|
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
|
|
@@ -898,8 +947,7 @@ var OAuthWrapperService = class {
|
|
|
898
947
|
if (!email) {
|
|
899
948
|
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
|
|
900
949
|
}
|
|
901
|
-
const
|
|
902
|
-
const name = profile.name?.trim() || nameFromParts || email;
|
|
950
|
+
const name = profile.name?.trim() || email;
|
|
903
951
|
return {
|
|
904
952
|
sub,
|
|
905
953
|
email,
|
|
@@ -934,6 +982,7 @@ async function verifyClientSecret(secret, storedHash) {
|
|
|
934
982
|
return timingSafeEqual2(derived, expected);
|
|
935
983
|
}
|
|
936
984
|
export {
|
|
985
|
+
OAuthAbstractProvider,
|
|
937
986
|
OAuthAuthorizationCodeRowSchema,
|
|
938
987
|
OAuthAuthorizeQuerySchema,
|
|
939
988
|
OAuthClientCreateResponseSchema,
|