@spfn/auth 0.2.0-beta.62 → 0.2.0-beta.65
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/dist/{authenticate-DcOkuB7d.d.ts → authenticate-mfVRzeIK.d.ts} +121 -2
- package/dist/index.d.ts +33 -3
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +209 -283
- package/dist/server.js +218 -50
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -6492,6 +6492,50 @@ var init_invitations_repository = __esm({
|
|
|
6492
6492
|
}
|
|
6493
6493
|
});
|
|
6494
6494
|
|
|
6495
|
+
// src/server/lib/oauth/token-cipher.ts
|
|
6496
|
+
import crypto3 from "crypto";
|
|
6497
|
+
import { env as env3 } from "@spfn/auth/config";
|
|
6498
|
+
function getTokenKey() {
|
|
6499
|
+
return crypto3.createHash("sha256").update(`social-token:${env3.SPFN_AUTH_SESSION_SECRET}`).digest();
|
|
6500
|
+
}
|
|
6501
|
+
function isEncrypted(value) {
|
|
6502
|
+
return value.startsWith(ENC_PREFIX);
|
|
6503
|
+
}
|
|
6504
|
+
function encryptToken(plain) {
|
|
6505
|
+
if (isEncrypted(plain)) {
|
|
6506
|
+
return plain;
|
|
6507
|
+
}
|
|
6508
|
+
const iv = crypto3.randomBytes(IV_BYTES);
|
|
6509
|
+
const cipher = crypto3.createCipheriv("aes-256-gcm", getTokenKey(), iv);
|
|
6510
|
+
const ciphertext = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
|
|
6511
|
+
const tag = cipher.getAuthTag();
|
|
6512
|
+
return ENC_PREFIX + Buffer.concat([iv, tag, ciphertext]).toString("base64");
|
|
6513
|
+
}
|
|
6514
|
+
function decryptToken(stored) {
|
|
6515
|
+
if (!isEncrypted(stored)) {
|
|
6516
|
+
return stored;
|
|
6517
|
+
}
|
|
6518
|
+
const packed = Buffer.from(stored.slice(ENC_PREFIX.length), "base64");
|
|
6519
|
+
if (packed.length < IV_BYTES + TAG_BYTES) {
|
|
6520
|
+
throw new Error("Malformed encrypted token: payload too short");
|
|
6521
|
+
}
|
|
6522
|
+
const iv = packed.subarray(0, IV_BYTES);
|
|
6523
|
+
const tag = packed.subarray(IV_BYTES, IV_BYTES + TAG_BYTES);
|
|
6524
|
+
const ciphertext = packed.subarray(IV_BYTES + TAG_BYTES);
|
|
6525
|
+
const decipher = crypto3.createDecipheriv("aes-256-gcm", getTokenKey(), iv);
|
|
6526
|
+
decipher.setAuthTag(tag);
|
|
6527
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
6528
|
+
}
|
|
6529
|
+
var ENC_PREFIX, IV_BYTES, TAG_BYTES;
|
|
6530
|
+
var init_token_cipher = __esm({
|
|
6531
|
+
"src/server/lib/oauth/token-cipher.ts"() {
|
|
6532
|
+
"use strict";
|
|
6533
|
+
ENC_PREFIX = "enc:v1:";
|
|
6534
|
+
IV_BYTES = 12;
|
|
6535
|
+
TAG_BYTES = 16;
|
|
6536
|
+
}
|
|
6537
|
+
});
|
|
6538
|
+
|
|
6495
6539
|
// src/server/repositories/social-accounts.repository.ts
|
|
6496
6540
|
import { eq as eq10, and as and7 } from "drizzle-orm";
|
|
6497
6541
|
import { BaseRepository as BaseRepository10 } from "@spfn/core/db";
|
|
@@ -6500,7 +6544,38 @@ var init_social_accounts_repository = __esm({
|
|
|
6500
6544
|
"src/server/repositories/social-accounts.repository.ts"() {
|
|
6501
6545
|
"use strict";
|
|
6502
6546
|
init_entities();
|
|
6547
|
+
init_token_cipher();
|
|
6503
6548
|
SocialAccountsRepository = class extends BaseRepository10 {
|
|
6549
|
+
/**
|
|
6550
|
+
* 저장 row 의 토큰을 평문으로 복호화해 반환한다.
|
|
6551
|
+
*
|
|
6552
|
+
* 레거시 평문(마커 없음)이 감지되면 즉시 재암호화해 저장하는
|
|
6553
|
+
* self-healing 마이그레이션을 수행한다. 호출자에게는 항상 평문이 반환되어
|
|
6554
|
+
* 외부 API 계약(평문 토큰)이 유지된다.
|
|
6555
|
+
*/
|
|
6556
|
+
async decryptAccount(account) {
|
|
6557
|
+
if (!account) {
|
|
6558
|
+
return account;
|
|
6559
|
+
}
|
|
6560
|
+
const heal = {};
|
|
6561
|
+
if (account.accessToken && !isEncrypted(account.accessToken)) {
|
|
6562
|
+
heal.accessToken = encryptToken(account.accessToken);
|
|
6563
|
+
}
|
|
6564
|
+
if (account.refreshToken && !isEncrypted(account.refreshToken)) {
|
|
6565
|
+
heal.refreshToken = encryptToken(account.refreshToken);
|
|
6566
|
+
}
|
|
6567
|
+
if (heal.accessToken || heal.refreshToken) {
|
|
6568
|
+
try {
|
|
6569
|
+
await this.db.update(userSocialAccounts).set(heal).where(eq10(userSocialAccounts.id, account.id));
|
|
6570
|
+
} catch {
|
|
6571
|
+
}
|
|
6572
|
+
}
|
|
6573
|
+
return {
|
|
6574
|
+
...account,
|
|
6575
|
+
accessToken: account.accessToken ? decryptToken(account.accessToken) : account.accessToken,
|
|
6576
|
+
refreshToken: account.refreshToken ? decryptToken(account.refreshToken) : account.refreshToken
|
|
6577
|
+
};
|
|
6578
|
+
}
|
|
6504
6579
|
/**
|
|
6505
6580
|
* provider와 providerUserId로 소셜 계정 조회
|
|
6506
6581
|
* Read replica 사용
|
|
@@ -6512,14 +6587,15 @@ var init_social_accounts_repository = __esm({
|
|
|
6512
6587
|
eq10(userSocialAccounts.providerUserId, providerUserId)
|
|
6513
6588
|
)
|
|
6514
6589
|
).limit(1);
|
|
6515
|
-
return result[0] ?? null;
|
|
6590
|
+
return this.decryptAccount(result[0] ?? null);
|
|
6516
6591
|
}
|
|
6517
6592
|
/**
|
|
6518
6593
|
* userId로 모든 소셜 계정 조회
|
|
6519
6594
|
* Read replica 사용
|
|
6520
6595
|
*/
|
|
6521
6596
|
async findByUserId(userId) {
|
|
6522
|
-
|
|
6597
|
+
const result = await this.readDb.select().from(userSocialAccounts).where(eq10(userSocialAccounts.userId, userId));
|
|
6598
|
+
return Promise.all(result.map((account) => this.decryptAccount(account)));
|
|
6523
6599
|
}
|
|
6524
6600
|
/**
|
|
6525
6601
|
* userId와 provider로 소셜 계정 조회
|
|
@@ -6532,18 +6608,21 @@ var init_social_accounts_repository = __esm({
|
|
|
6532
6608
|
eq10(userSocialAccounts.provider, provider)
|
|
6533
6609
|
)
|
|
6534
6610
|
).limit(1);
|
|
6535
|
-
return result[0] ?? null;
|
|
6611
|
+
return this.decryptAccount(result[0] ?? null);
|
|
6536
6612
|
}
|
|
6537
6613
|
/**
|
|
6538
6614
|
* 소셜 계정 생성
|
|
6539
6615
|
* Write primary 사용
|
|
6540
6616
|
*/
|
|
6541
6617
|
async create(data) {
|
|
6542
|
-
|
|
6618
|
+
const created = await this._create(userSocialAccounts, {
|
|
6543
6619
|
...data,
|
|
6620
|
+
accessToken: data.accessToken ? encryptToken(data.accessToken) : data.accessToken,
|
|
6621
|
+
refreshToken: data.refreshToken ? encryptToken(data.refreshToken) : data.refreshToken,
|
|
6544
6622
|
createdAt: /* @__PURE__ */ new Date(),
|
|
6545
6623
|
updatedAt: /* @__PURE__ */ new Date()
|
|
6546
6624
|
});
|
|
6625
|
+
return this.decryptAccount(created);
|
|
6547
6626
|
}
|
|
6548
6627
|
/**
|
|
6549
6628
|
* 토큰 정보 업데이트
|
|
@@ -6552,9 +6631,11 @@ var init_social_accounts_repository = __esm({
|
|
|
6552
6631
|
async updateTokens(id11, data) {
|
|
6553
6632
|
const result = await this.db.update(userSocialAccounts).set({
|
|
6554
6633
|
...data,
|
|
6634
|
+
accessToken: data.accessToken ? encryptToken(data.accessToken) : data.accessToken,
|
|
6635
|
+
refreshToken: data.refreshToken ? encryptToken(data.refreshToken) : data.refreshToken,
|
|
6555
6636
|
updatedAt: /* @__PURE__ */ new Date()
|
|
6556
6637
|
}).where(eq10(userSocialAccounts.id, id11)).returning();
|
|
6557
|
-
return result[0] ?? null;
|
|
6638
|
+
return this.decryptAccount(result[0] ?? null);
|
|
6558
6639
|
}
|
|
6559
6640
|
/**
|
|
6560
6641
|
* 소셜 계정 삭제
|
|
@@ -6921,7 +7002,7 @@ import {
|
|
|
6921
7002
|
} from "@spfn/auth/errors";
|
|
6922
7003
|
|
|
6923
7004
|
// src/server/services/verification.service.ts
|
|
6924
|
-
import { env as
|
|
7005
|
+
import { env as env4 } from "@spfn/auth/config";
|
|
6925
7006
|
import { InvalidVerificationCodeError } from "@spfn/auth/errors";
|
|
6926
7007
|
import jwt2 from "jsonwebtoken";
|
|
6927
7008
|
import { sendEmail, sendSMS } from "@spfn/notification/server";
|
|
@@ -6989,7 +7070,7 @@ async function markCodeAsUsed(codeId) {
|
|
|
6989
7070
|
await verificationCodesRepository.markAsUsed(codeId);
|
|
6990
7071
|
}
|
|
6991
7072
|
function createVerificationToken(payload) {
|
|
6992
|
-
return jwt2.sign(payload,
|
|
7073
|
+
return jwt2.sign(payload, env4.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
|
|
6993
7074
|
expiresIn: VERIFICATION_TOKEN_EXPIRY,
|
|
6994
7075
|
issuer: "spfn-auth",
|
|
6995
7076
|
audience: "spfn-client"
|
|
@@ -6997,7 +7078,7 @@ function createVerificationToken(payload) {
|
|
|
6997
7078
|
}
|
|
6998
7079
|
function validateVerificationToken(token) {
|
|
6999
7080
|
try {
|
|
7000
|
-
const decoded = jwt2.verify(token,
|
|
7081
|
+
const decoded = jwt2.verify(token, env4.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
|
|
7001
7082
|
issuer: "spfn-auth",
|
|
7002
7083
|
audience: "spfn-client"
|
|
7003
7084
|
});
|
|
@@ -7142,7 +7223,7 @@ async function revokeKeyService(params) {
|
|
|
7142
7223
|
init_repositories();
|
|
7143
7224
|
import { ValidationError } from "@spfn/core/errors";
|
|
7144
7225
|
import { ReservedUsernameError, UsernameAlreadyTakenError } from "@spfn/auth/errors";
|
|
7145
|
-
import { env as
|
|
7226
|
+
import { env as env5 } from "@spfn/auth/config";
|
|
7146
7227
|
async function getUserByIdService(userId) {
|
|
7147
7228
|
return await usersRepository.findById(userId);
|
|
7148
7229
|
}
|
|
@@ -7159,7 +7240,7 @@ async function updateUserService(userId, updates) {
|
|
|
7159
7240
|
await usersRepository.updateById(userId, updates);
|
|
7160
7241
|
}
|
|
7161
7242
|
function getReservedUsernames() {
|
|
7162
|
-
const raw =
|
|
7243
|
+
const raw = env5.SPFN_AUTH_RESERVED_USERNAMES ?? "";
|
|
7163
7244
|
if (!raw) {
|
|
7164
7245
|
return /* @__PURE__ */ new Set();
|
|
7165
7246
|
}
|
|
@@ -7171,8 +7252,8 @@ function isReservedUsername(username) {
|
|
|
7171
7252
|
return getReservedUsernames().has(username.toLowerCase());
|
|
7172
7253
|
}
|
|
7173
7254
|
function validateUsernameLength(username) {
|
|
7174
|
-
const min =
|
|
7175
|
-
const max =
|
|
7255
|
+
const min = env5.SPFN_AUTH_USERNAME_MIN_LENGTH ?? 3;
|
|
7256
|
+
const max = env5.SPFN_AUTH_USERNAME_MAX_LENGTH ?? 30;
|
|
7176
7257
|
if (username.length < min) {
|
|
7177
7258
|
throw new ValidationError({
|
|
7178
7259
|
message: `Username must be at least ${min} characters`,
|
|
@@ -7428,7 +7509,7 @@ init_rbac();
|
|
|
7428
7509
|
import { createHash } from "crypto";
|
|
7429
7510
|
|
|
7430
7511
|
// src/server/lib/config.ts
|
|
7431
|
-
import { env as
|
|
7512
|
+
import { env as env6 } from "@spfn/auth/config";
|
|
7432
7513
|
function getCookieSuffix() {
|
|
7433
7514
|
const port = process.env.PORT;
|
|
7434
7515
|
return port ? `_${port}` : "";
|
|
@@ -7490,7 +7571,7 @@ function getSessionTtl(override) {
|
|
|
7490
7571
|
if (globalConfig.sessionTtl !== void 0) {
|
|
7491
7572
|
return parseDuration(globalConfig.sessionTtl);
|
|
7492
7573
|
}
|
|
7493
|
-
const envTtl =
|
|
7574
|
+
const envTtl = env6.SPFN_AUTH_SESSION_TTL;
|
|
7494
7575
|
if (envTtl) {
|
|
7495
7576
|
return parseDuration(envTtl);
|
|
7496
7577
|
}
|
|
@@ -7706,9 +7787,9 @@ init_role_service();
|
|
|
7706
7787
|
|
|
7707
7788
|
// src/server/services/invitation.service.ts
|
|
7708
7789
|
init_repositories();
|
|
7709
|
-
import
|
|
7790
|
+
import crypto4 from "crypto";
|
|
7710
7791
|
function generateInvitationToken() {
|
|
7711
|
-
return
|
|
7792
|
+
return crypto4.randomUUID();
|
|
7712
7793
|
}
|
|
7713
7794
|
function calculateExpiresAt(days = 7) {
|
|
7714
7795
|
const expiresAt = /* @__PURE__ */ new Date();
|
|
@@ -8023,25 +8104,25 @@ async function updateUserProfileService(userId, params) {
|
|
|
8023
8104
|
|
|
8024
8105
|
// src/server/services/oauth.service.ts
|
|
8025
8106
|
init_repositories();
|
|
8026
|
-
import { env as
|
|
8107
|
+
import { env as env9 } from "@spfn/auth/config";
|
|
8027
8108
|
import { ValidationError as ValidationError3 } from "@spfn/core/errors";
|
|
8028
8109
|
|
|
8029
8110
|
// src/server/lib/oauth/google.ts
|
|
8030
|
-
import { env as
|
|
8111
|
+
import { env as env7 } from "@spfn/auth/config";
|
|
8031
8112
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
8032
8113
|
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
8033
8114
|
var GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
|
|
8034
8115
|
function isGoogleOAuthEnabled() {
|
|
8035
|
-
return !!(
|
|
8116
|
+
return !!(env7.SPFN_AUTH_GOOGLE_CLIENT_ID && env7.SPFN_AUTH_GOOGLE_CLIENT_SECRET);
|
|
8036
8117
|
}
|
|
8037
8118
|
function getGoogleOAuthConfig() {
|
|
8038
|
-
const clientId =
|
|
8039
|
-
const clientSecret =
|
|
8119
|
+
const clientId = env7.SPFN_AUTH_GOOGLE_CLIENT_ID;
|
|
8120
|
+
const clientSecret = env7.SPFN_AUTH_GOOGLE_CLIENT_SECRET;
|
|
8040
8121
|
if (!clientId || !clientSecret) {
|
|
8041
8122
|
throw new Error("Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET.");
|
|
8042
8123
|
}
|
|
8043
|
-
const baseUrl =
|
|
8044
|
-
const redirectUri =
|
|
8124
|
+
const baseUrl = env7.NEXT_PUBLIC_SPFN_API_URL || env7.SPFN_API_URL;
|
|
8125
|
+
const redirectUri = env7.SPFN_AUTH_GOOGLE_REDIRECT_URI || `${baseUrl}/_auth/oauth/google/callback`;
|
|
8045
8126
|
return {
|
|
8046
8127
|
clientId,
|
|
8047
8128
|
clientSecret,
|
|
@@ -8049,7 +8130,7 @@ function getGoogleOAuthConfig() {
|
|
|
8049
8130
|
};
|
|
8050
8131
|
}
|
|
8051
8132
|
function getDefaultScopes() {
|
|
8052
|
-
const envScopes =
|
|
8133
|
+
const envScopes = env7.SPFN_AUTH_GOOGLE_SCOPES;
|
|
8053
8134
|
if (envScopes) {
|
|
8054
8135
|
return envScopes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
8055
8136
|
}
|
|
@@ -8127,9 +8208,9 @@ async function refreshAccessToken(refreshToken) {
|
|
|
8127
8208
|
|
|
8128
8209
|
// src/server/lib/oauth/state.ts
|
|
8129
8210
|
import * as jose from "jose";
|
|
8130
|
-
import { env as
|
|
8211
|
+
import { env as env8 } from "@spfn/auth/config";
|
|
8131
8212
|
async function getStateKey() {
|
|
8132
|
-
const secret =
|
|
8213
|
+
const secret = env8.SPFN_AUTH_SESSION_SECRET;
|
|
8133
8214
|
const encoder = new TextEncoder();
|
|
8134
8215
|
const data = encoder.encode(`oauth-state:${secret}`);
|
|
8135
8216
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
@@ -8282,8 +8363,8 @@ async function oauthCallbackService(params) {
|
|
|
8282
8363
|
algorithm: stateData.algorithm
|
|
8283
8364
|
});
|
|
8284
8365
|
await updateLastLoginService(userId);
|
|
8285
|
-
const appUrl =
|
|
8286
|
-
const callbackPath =
|
|
8366
|
+
const appUrl = env9.NEXT_PUBLIC_SPFN_APP_URL || env9.SPFN_APP_URL;
|
|
8367
|
+
const callbackPath = env9.SPFN_AUTH_OAUTH_SUCCESS_URL || "/auth/callback";
|
|
8287
8368
|
const callbackUrl = callbackPath.startsWith("http") ? callbackPath : `${appUrl}${callbackPath}`;
|
|
8288
8369
|
const redirectUrl = buildRedirectUrl(callbackUrl, {
|
|
8289
8370
|
userId: String(userId),
|
|
@@ -8368,7 +8449,7 @@ function buildRedirectUrl(baseUrl, params) {
|
|
|
8368
8449
|
return `${url.pathname}${url.search}`;
|
|
8369
8450
|
}
|
|
8370
8451
|
function buildOAuthErrorUrl(error) {
|
|
8371
|
-
const errorUrl =
|
|
8452
|
+
const errorUrl = env9.SPFN_AUTH_OAUTH_ERROR_URL || "/auth/error?error={error}";
|
|
8372
8453
|
return errorUrl.replace("{error}", encodeURIComponent(error));
|
|
8373
8454
|
}
|
|
8374
8455
|
function isOAuthProviderEnabled(provider) {
|
|
@@ -9158,7 +9239,13 @@ var userRouter = defineRouter3({
|
|
|
9158
9239
|
init_esm();
|
|
9159
9240
|
init_types();
|
|
9160
9241
|
import { Transactional as Transactional2 } from "@spfn/core/db";
|
|
9242
|
+
import { ValidationError as ValidationError4 } from "@spfn/core/errors";
|
|
9161
9243
|
import { defineRouter as defineRouter4, route as route4 } from "@spfn/core/route";
|
|
9244
|
+
var providerParams = Type.Object({
|
|
9245
|
+
provider: Type.Union(SOCIAL_PROVIDERS.map((p) => Type.Literal(p)), {
|
|
9246
|
+
description: "OAuth provider id (google, github, kakao, naver, superself)"
|
|
9247
|
+
})
|
|
9248
|
+
});
|
|
9162
9249
|
var oauthGoogleStart = route4.get("/_auth/oauth/google").input({
|
|
9163
9250
|
query: Type.Object({
|
|
9164
9251
|
state: Type.String({
|
|
@@ -9255,10 +9342,12 @@ var getGoogleOAuthUrl = route4.post("/_auth/oauth/google/url").input({
|
|
|
9255
9342
|
}).skip(["auth"]).handler(async (c) => {
|
|
9256
9343
|
const { body } = await c.data();
|
|
9257
9344
|
if (!isGoogleOAuthEnabled()) {
|
|
9258
|
-
throw new
|
|
9345
|
+
throw new ValidationError4({ message: "Google OAuth is not configured" });
|
|
9259
9346
|
}
|
|
9260
9347
|
if (!body.state) {
|
|
9261
|
-
throw new
|
|
9348
|
+
throw new ValidationError4({
|
|
9349
|
+
message: "OAuth state is required. Ensure the OAuth interceptor is configured."
|
|
9350
|
+
});
|
|
9262
9351
|
}
|
|
9263
9352
|
return { authUrl: getGoogleAuthUrl(body.state) };
|
|
9264
9353
|
});
|
|
@@ -9277,13 +9366,88 @@ var oauthFinalize = route4.post("/_auth/oauth/finalize").input({
|
|
|
9277
9366
|
returnUrl: body.returnUrl || "/"
|
|
9278
9367
|
};
|
|
9279
9368
|
});
|
|
9369
|
+
var oauthProviderStart = route4.get("/_auth/oauth/:provider").input({
|
|
9370
|
+
params: providerParams,
|
|
9371
|
+
query: Type.Object({
|
|
9372
|
+
state: Type.String({
|
|
9373
|
+
description: "Encrypted OAuth state (returnUrl, publicKey, keyId, fingerprint, algorithm)"
|
|
9374
|
+
})
|
|
9375
|
+
})
|
|
9376
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
9377
|
+
const { params, query } = await c.data();
|
|
9378
|
+
const provider = getOAuthProvider(params.provider);
|
|
9379
|
+
if (!provider?.isEnabled()) {
|
|
9380
|
+
return c.redirect(buildOAuthErrorUrl(`OAuth provider '${params.provider}' is not configured`));
|
|
9381
|
+
}
|
|
9382
|
+
return c.redirect(provider.getAuthUrl(query.state));
|
|
9383
|
+
});
|
|
9384
|
+
var oauthProviderCallback = route4.get("/_auth/oauth/:provider/callback").input({
|
|
9385
|
+
params: providerParams,
|
|
9386
|
+
query: Type.Object({
|
|
9387
|
+
code: Type.Optional(Type.String({
|
|
9388
|
+
description: "Authorization code from provider"
|
|
9389
|
+
})),
|
|
9390
|
+
state: Type.Optional(Type.String({
|
|
9391
|
+
description: "OAuth state parameter"
|
|
9392
|
+
})),
|
|
9393
|
+
error: Type.Optional(Type.String({
|
|
9394
|
+
description: "Error code from provider"
|
|
9395
|
+
})),
|
|
9396
|
+
error_description: Type.Optional(Type.String({
|
|
9397
|
+
description: "Error description from provider"
|
|
9398
|
+
}))
|
|
9399
|
+
})
|
|
9400
|
+
}).use([Transactional2()]).skip(["auth"]).handler(async (c) => {
|
|
9401
|
+
const { params, query } = await c.data();
|
|
9402
|
+
if (query.error) {
|
|
9403
|
+
const errorMessage = query.error_description || query.error;
|
|
9404
|
+
return c.redirect(buildOAuthErrorUrl(errorMessage));
|
|
9405
|
+
}
|
|
9406
|
+
if (!query.code || !query.state) {
|
|
9407
|
+
return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
|
|
9408
|
+
}
|
|
9409
|
+
try {
|
|
9410
|
+
const result = await oauthCallbackService({
|
|
9411
|
+
provider: params.provider,
|
|
9412
|
+
code: query.code,
|
|
9413
|
+
state: query.state
|
|
9414
|
+
});
|
|
9415
|
+
return c.redirect(result.redirectUrl);
|
|
9416
|
+
} catch (err) {
|
|
9417
|
+
const message = err instanceof Error ? err.message : "OAuth callback failed";
|
|
9418
|
+
return c.redirect(buildOAuthErrorUrl(message));
|
|
9419
|
+
}
|
|
9420
|
+
});
|
|
9421
|
+
var getProviderOAuthUrl = route4.post("/_auth/oauth/:provider/url").input({
|
|
9422
|
+
params: providerParams,
|
|
9423
|
+
body: Type.Object({
|
|
9424
|
+
returnUrl: Type.Optional(Type.String({
|
|
9425
|
+
description: "URL to redirect after OAuth success"
|
|
9426
|
+
})),
|
|
9427
|
+
state: Type.Optional(Type.String({
|
|
9428
|
+
description: "Encrypted OAuth state (injected by interceptor)"
|
|
9429
|
+
}))
|
|
9430
|
+
})
|
|
9431
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
9432
|
+
const { params, body } = await c.data();
|
|
9433
|
+
const provider = requireEnabledProvider(params.provider);
|
|
9434
|
+
if (!body.state) {
|
|
9435
|
+
throw new ValidationError4({
|
|
9436
|
+
message: "OAuth state is required. Ensure the OAuth interceptor is configured."
|
|
9437
|
+
});
|
|
9438
|
+
}
|
|
9439
|
+
return { authUrl: provider.getAuthUrl(body.state) };
|
|
9440
|
+
});
|
|
9280
9441
|
var oauthRouter = defineRouter4({
|
|
9281
9442
|
oauthGoogleStart,
|
|
9282
9443
|
oauthGoogleCallback,
|
|
9283
9444
|
oauthStart,
|
|
9284
9445
|
oauthProviders,
|
|
9285
9446
|
getGoogleOAuthUrl,
|
|
9286
|
-
oauthFinalize
|
|
9447
|
+
oauthFinalize,
|
|
9448
|
+
oauthProviderStart,
|
|
9449
|
+
oauthProviderCallback,
|
|
9450
|
+
getProviderOAuthUrl
|
|
9287
9451
|
});
|
|
9288
9452
|
|
|
9289
9453
|
// src/server/routes/admin/index.ts
|
|
@@ -9403,6 +9567,9 @@ var mainAuthRouter = defineRouter5({
|
|
|
9403
9567
|
oauthProviders,
|
|
9404
9568
|
getGoogleOAuthUrl,
|
|
9405
9569
|
oauthFinalize,
|
|
9570
|
+
oauthProviderStart,
|
|
9571
|
+
oauthProviderCallback,
|
|
9572
|
+
getProviderOAuthUrl,
|
|
9406
9573
|
// Invitation routes
|
|
9407
9574
|
getInvitation,
|
|
9408
9575
|
acceptInvitation: acceptInvitation2,
|
|
@@ -9432,11 +9599,11 @@ init_types();
|
|
|
9432
9599
|
init_schema3();
|
|
9433
9600
|
|
|
9434
9601
|
// src/server/lib/crypto.ts
|
|
9435
|
-
import
|
|
9602
|
+
import crypto5 from "crypto";
|
|
9436
9603
|
import jwt3 from "jsonwebtoken";
|
|
9437
9604
|
function generateKeyPairES256() {
|
|
9438
|
-
const keyId =
|
|
9439
|
-
const { privateKey, publicKey } =
|
|
9605
|
+
const keyId = crypto5.randomUUID();
|
|
9606
|
+
const { privateKey, publicKey } = crypto5.generateKeyPairSync("ec", {
|
|
9440
9607
|
namedCurve: "P-256",
|
|
9441
9608
|
// ES256
|
|
9442
9609
|
publicKeyEncoding: {
|
|
@@ -9450,7 +9617,7 @@ function generateKeyPairES256() {
|
|
|
9450
9617
|
});
|
|
9451
9618
|
const privateKeyB64 = privateKey.toString("base64");
|
|
9452
9619
|
const publicKeyB64 = publicKey.toString("base64");
|
|
9453
|
-
const fingerprint =
|
|
9620
|
+
const fingerprint = crypto5.createHash("sha256").update(publicKey).digest("hex");
|
|
9454
9621
|
return {
|
|
9455
9622
|
privateKey: privateKeyB64,
|
|
9456
9623
|
publicKey: publicKeyB64,
|
|
@@ -9460,8 +9627,8 @@ function generateKeyPairES256() {
|
|
|
9460
9627
|
};
|
|
9461
9628
|
}
|
|
9462
9629
|
function generateKeyPairRS256() {
|
|
9463
|
-
const keyId =
|
|
9464
|
-
const { privateKey, publicKey } =
|
|
9630
|
+
const keyId = crypto5.randomUUID();
|
|
9631
|
+
const { privateKey, publicKey } = crypto5.generateKeyPairSync("rsa", {
|
|
9465
9632
|
modulusLength: 2048,
|
|
9466
9633
|
publicKeyEncoding: {
|
|
9467
9634
|
type: "spki",
|
|
@@ -9474,7 +9641,7 @@ function generateKeyPairRS256() {
|
|
|
9474
9641
|
});
|
|
9475
9642
|
const privateKeyB64 = privateKey.toString("base64");
|
|
9476
9643
|
const publicKeyB64 = publicKey.toString("base64");
|
|
9477
|
-
const fingerprint =
|
|
9644
|
+
const fingerprint = crypto5.createHash("sha256").update(publicKey).digest("hex");
|
|
9478
9645
|
return {
|
|
9479
9646
|
privateKey: privateKeyB64,
|
|
9480
9647
|
publicKey: publicKeyB64,
|
|
@@ -9489,7 +9656,7 @@ function generateKeyPair(algorithm = "ES256") {
|
|
|
9489
9656
|
function generateClientToken(payload, privateKeyB64, algorithm, options) {
|
|
9490
9657
|
try {
|
|
9491
9658
|
const privateKeyDER = Buffer.from(privateKeyB64, "base64");
|
|
9492
|
-
const privateKeyObject =
|
|
9659
|
+
const privateKeyObject = crypto5.createPrivateKey({
|
|
9493
9660
|
key: privateKeyDER,
|
|
9494
9661
|
format: "der",
|
|
9495
9662
|
type: "pkcs8"
|
|
@@ -9533,10 +9700,10 @@ function shouldRotateKey(createdAt, rotationDays = 90) {
|
|
|
9533
9700
|
|
|
9534
9701
|
// src/server/lib/session.ts
|
|
9535
9702
|
import * as jose2 from "jose";
|
|
9536
|
-
import { env as
|
|
9703
|
+
import { env as env10 } from "@spfn/auth/config";
|
|
9537
9704
|
import { env as coreEnv } from "@spfn/core/config";
|
|
9538
9705
|
async function getSessionSecretKey() {
|
|
9539
|
-
const secret =
|
|
9706
|
+
const secret = env10.SPFN_AUTH_SESSION_SECRET;
|
|
9540
9707
|
const encoder = new TextEncoder();
|
|
9541
9708
|
const data = encoder.encode(secret);
|
|
9542
9709
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
@@ -9618,14 +9785,14 @@ async function shouldRefreshSession(jwt4, thresholdHours = 24) {
|
|
|
9618
9785
|
}
|
|
9619
9786
|
|
|
9620
9787
|
// src/server/setup.ts
|
|
9621
|
-
import { env as
|
|
9788
|
+
import { env as env11 } from "@spfn/auth/config";
|
|
9622
9789
|
import { getRoleByName as getRoleByName2 } from "@spfn/auth/server";
|
|
9623
9790
|
init_repositories();
|
|
9624
9791
|
function parseAdminAccounts() {
|
|
9625
9792
|
const accounts = [];
|
|
9626
|
-
if (
|
|
9793
|
+
if (env11.SPFN_AUTH_ADMIN_ACCOUNTS) {
|
|
9627
9794
|
try {
|
|
9628
|
-
const accountsJson =
|
|
9795
|
+
const accountsJson = env11.SPFN_AUTH_ADMIN_ACCOUNTS;
|
|
9629
9796
|
const parsed = JSON.parse(accountsJson);
|
|
9630
9797
|
if (!Array.isArray(parsed)) {
|
|
9631
9798
|
authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_ACCOUNTS must be an array");
|
|
@@ -9652,11 +9819,11 @@ function parseAdminAccounts() {
|
|
|
9652
9819
|
return accounts;
|
|
9653
9820
|
}
|
|
9654
9821
|
}
|
|
9655
|
-
const adminEmails =
|
|
9822
|
+
const adminEmails = env11.SPFN_AUTH_ADMIN_EMAILS;
|
|
9656
9823
|
if (adminEmails) {
|
|
9657
9824
|
const emails = adminEmails.split(",").map((s) => s.trim());
|
|
9658
|
-
const passwords = (
|
|
9659
|
-
const roles2 = (
|
|
9825
|
+
const passwords = (env11.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
|
|
9826
|
+
const roles2 = (env11.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
|
|
9660
9827
|
if (passwords.length !== emails.length) {
|
|
9661
9828
|
authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_EMAILS and SPFN_AUTH_ADMIN_PASSWORDS length mismatch");
|
|
9662
9829
|
return accounts;
|
|
@@ -9678,8 +9845,8 @@ function parseAdminAccounts() {
|
|
|
9678
9845
|
}
|
|
9679
9846
|
return accounts;
|
|
9680
9847
|
}
|
|
9681
|
-
const adminEmail =
|
|
9682
|
-
const adminPassword =
|
|
9848
|
+
const adminEmail = env11.SPFN_AUTH_ADMIN_EMAIL;
|
|
9849
|
+
const adminPassword = env11.SPFN_AUTH_ADMIN_PASSWORD;
|
|
9683
9850
|
if (adminEmail && adminPassword) {
|
|
9684
9851
|
accounts.push({
|
|
9685
9852
|
email: adminEmail,
|
|
@@ -9876,6 +10043,7 @@ export {
|
|
|
9876
10043
|
registerService,
|
|
9877
10044
|
removePermissionFromRole,
|
|
9878
10045
|
requireAnyPermission,
|
|
10046
|
+
requireEnabledProvider,
|
|
9879
10047
|
requirePermissions,
|
|
9880
10048
|
requireRole,
|
|
9881
10049
|
resendInvitation,
|