@spfn/auth 0.2.0-beta.11 → 0.2.0-beta.13
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/README.md +338 -8
- package/dist/{authenticate-CU6_zQaa.d.ts → authenticate-Cz2FjLdB.d.ts} +113 -1
- package/dist/config.d.ts +120 -0
- package/dist/config.js +72 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +46 -2
- package/dist/nextjs/api.js +186 -0
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/client.js +80 -0
- package/dist/nextjs/client.js.map +1 -0
- package/dist/nextjs/server.d.ts +68 -2
- package/dist/nextjs/server.js +125 -2
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +243 -3
- package/dist/server.js +594 -23
- package/dist/server.js.map +1 -1
- package/package.json +10 -3
package/dist/server.js
CHANGED
|
@@ -1359,7 +1359,7 @@ var init_literal2 = __esm({
|
|
|
1359
1359
|
});
|
|
1360
1360
|
|
|
1361
1361
|
// ../../node_modules/.pnpm/@sinclair+typebox@0.34.41/node_modules/@sinclair/typebox/build/esm/type/boolean/boolean.mjs
|
|
1362
|
-
function
|
|
1362
|
+
function Boolean2(options) {
|
|
1363
1363
|
return CreateType({ [Kind]: "Boolean", type: "boolean" }, options);
|
|
1364
1364
|
}
|
|
1365
1365
|
var init_boolean = __esm({
|
|
@@ -1441,7 +1441,7 @@ var init_string2 = __esm({
|
|
|
1441
1441
|
// ../../node_modules/.pnpm/@sinclair+typebox@0.34.41/node_modules/@sinclair/typebox/build/esm/type/template-literal/syntax.mjs
|
|
1442
1442
|
function* FromUnion(syntax) {
|
|
1443
1443
|
const trim = syntax.trim().replace(/"|'/g, "");
|
|
1444
|
-
return trim === "boolean" ? yield
|
|
1444
|
+
return trim === "boolean" ? yield Boolean2() : trim === "number" ? yield Number2() : trim === "bigint" ? yield BigInt() : trim === "string" ? yield String2() : yield (() => {
|
|
1445
1445
|
const literals = trim.split("|").map((literal) => Literal(literal.trim()));
|
|
1446
1446
|
return literals.length === 0 ? Never() : literals.length === 1 ? literals[0] : UnionEvaluated(literals);
|
|
1447
1447
|
})();
|
|
@@ -4244,7 +4244,7 @@ __export(type_exports3, {
|
|
|
4244
4244
|
AsyncIterator: () => AsyncIterator,
|
|
4245
4245
|
Awaited: () => Awaited,
|
|
4246
4246
|
BigInt: () => BigInt,
|
|
4247
|
-
Boolean: () =>
|
|
4247
|
+
Boolean: () => Boolean2,
|
|
4248
4248
|
Capitalize: () => Capitalize,
|
|
4249
4249
|
Composite: () => Composite,
|
|
4250
4250
|
Const: () => Const,
|
|
@@ -6407,6 +6407,96 @@ var init_invitations_repository = __esm({
|
|
|
6407
6407
|
}
|
|
6408
6408
|
});
|
|
6409
6409
|
|
|
6410
|
+
// src/server/repositories/social-accounts.repository.ts
|
|
6411
|
+
import { eq as eq10, and as and7 } from "drizzle-orm";
|
|
6412
|
+
import { BaseRepository as BaseRepository10 } from "@spfn/core/db";
|
|
6413
|
+
var SocialAccountsRepository, socialAccountsRepository;
|
|
6414
|
+
var init_social_accounts_repository = __esm({
|
|
6415
|
+
"src/server/repositories/social-accounts.repository.ts"() {
|
|
6416
|
+
"use strict";
|
|
6417
|
+
init_entities();
|
|
6418
|
+
SocialAccountsRepository = class extends BaseRepository10 {
|
|
6419
|
+
/**
|
|
6420
|
+
* provider와 providerUserId로 소셜 계정 조회
|
|
6421
|
+
* Read replica 사용
|
|
6422
|
+
*/
|
|
6423
|
+
async findByProviderAndProviderId(provider, providerUserId) {
|
|
6424
|
+
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
6425
|
+
and7(
|
|
6426
|
+
eq10(userSocialAccounts.provider, provider),
|
|
6427
|
+
eq10(userSocialAccounts.providerUserId, providerUserId)
|
|
6428
|
+
)
|
|
6429
|
+
).limit(1);
|
|
6430
|
+
return result[0] ?? null;
|
|
6431
|
+
}
|
|
6432
|
+
/**
|
|
6433
|
+
* userId로 모든 소셜 계정 조회
|
|
6434
|
+
* Read replica 사용
|
|
6435
|
+
*/
|
|
6436
|
+
async findByUserId(userId) {
|
|
6437
|
+
return await this.readDb.select().from(userSocialAccounts).where(eq10(userSocialAccounts.userId, userId));
|
|
6438
|
+
}
|
|
6439
|
+
/**
|
|
6440
|
+
* userId와 provider로 소셜 계정 조회
|
|
6441
|
+
* Read replica 사용
|
|
6442
|
+
*/
|
|
6443
|
+
async findByUserIdAndProvider(userId, provider) {
|
|
6444
|
+
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
6445
|
+
and7(
|
|
6446
|
+
eq10(userSocialAccounts.userId, userId),
|
|
6447
|
+
eq10(userSocialAccounts.provider, provider)
|
|
6448
|
+
)
|
|
6449
|
+
).limit(1);
|
|
6450
|
+
return result[0] ?? null;
|
|
6451
|
+
}
|
|
6452
|
+
/**
|
|
6453
|
+
* 소셜 계정 생성
|
|
6454
|
+
* Write primary 사용
|
|
6455
|
+
*/
|
|
6456
|
+
async create(data) {
|
|
6457
|
+
return await this._create(userSocialAccounts, {
|
|
6458
|
+
...data,
|
|
6459
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
6460
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
6461
|
+
});
|
|
6462
|
+
}
|
|
6463
|
+
/**
|
|
6464
|
+
* 토큰 정보 업데이트
|
|
6465
|
+
* Write primary 사용
|
|
6466
|
+
*/
|
|
6467
|
+
async updateTokens(id11, data) {
|
|
6468
|
+
const result = await this.db.update(userSocialAccounts).set({
|
|
6469
|
+
...data,
|
|
6470
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
6471
|
+
}).where(eq10(userSocialAccounts.id, id11)).returning();
|
|
6472
|
+
return result[0] ?? null;
|
|
6473
|
+
}
|
|
6474
|
+
/**
|
|
6475
|
+
* 소셜 계정 삭제
|
|
6476
|
+
* Write primary 사용
|
|
6477
|
+
*/
|
|
6478
|
+
async deleteById(id11) {
|
|
6479
|
+
const result = await this.db.delete(userSocialAccounts).where(eq10(userSocialAccounts.id, id11)).returning();
|
|
6480
|
+
return result[0] ?? null;
|
|
6481
|
+
}
|
|
6482
|
+
/**
|
|
6483
|
+
* userId와 provider로 소셜 계정 삭제
|
|
6484
|
+
* Write primary 사용
|
|
6485
|
+
*/
|
|
6486
|
+
async deleteByUserIdAndProvider(userId, provider) {
|
|
6487
|
+
const result = await this.db.delete(userSocialAccounts).where(
|
|
6488
|
+
and7(
|
|
6489
|
+
eq10(userSocialAccounts.userId, userId),
|
|
6490
|
+
eq10(userSocialAccounts.provider, provider)
|
|
6491
|
+
)
|
|
6492
|
+
).returning();
|
|
6493
|
+
return result[0] ?? null;
|
|
6494
|
+
}
|
|
6495
|
+
};
|
|
6496
|
+
socialAccountsRepository = new SocialAccountsRepository();
|
|
6497
|
+
}
|
|
6498
|
+
});
|
|
6499
|
+
|
|
6410
6500
|
// src/server/repositories/index.ts
|
|
6411
6501
|
var init_repositories = __esm({
|
|
6412
6502
|
"src/server/repositories/index.ts"() {
|
|
@@ -6420,6 +6510,7 @@ var init_repositories = __esm({
|
|
|
6420
6510
|
init_user_permissions_repository();
|
|
6421
6511
|
init_user_profiles_repository();
|
|
6422
6512
|
init_invitations_repository();
|
|
6513
|
+
init_social_accounts_repository();
|
|
6423
6514
|
}
|
|
6424
6515
|
});
|
|
6425
6516
|
|
|
@@ -6546,7 +6637,7 @@ var init_role_service = __esm({
|
|
|
6546
6637
|
import "@spfn/auth/config";
|
|
6547
6638
|
|
|
6548
6639
|
// src/server/routes/index.ts
|
|
6549
|
-
import { defineRouter as
|
|
6640
|
+
import { defineRouter as defineRouter5 } from "@spfn/core/route";
|
|
6550
6641
|
|
|
6551
6642
|
// src/server/routes/auth/index.ts
|
|
6552
6643
|
init_schema3();
|
|
@@ -7075,7 +7166,9 @@ var COOKIE_NAMES = {
|
|
|
7075
7166
|
/** Encrypted session data (userId, privateKey, keyId, algorithm) */
|
|
7076
7167
|
SESSION: "spfn_session",
|
|
7077
7168
|
/** Current key ID (for key rotation) */
|
|
7078
|
-
SESSION_KEY_ID: "spfn_session_key_id"
|
|
7169
|
+
SESSION_KEY_ID: "spfn_session_key_id",
|
|
7170
|
+
/** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
|
|
7171
|
+
OAUTH_PENDING: "spfn_oauth_pending"
|
|
7079
7172
|
};
|
|
7080
7173
|
function parseDuration(duration) {
|
|
7081
7174
|
if (typeof duration === "number") {
|
|
@@ -7560,6 +7653,334 @@ async function updateUserProfileService(userId, params) {
|
|
|
7560
7653
|
return profile;
|
|
7561
7654
|
}
|
|
7562
7655
|
|
|
7656
|
+
// src/server/services/oauth.service.ts
|
|
7657
|
+
init_repositories();
|
|
7658
|
+
import { env as env7 } from "@spfn/auth/config";
|
|
7659
|
+
import { ValidationError as ValidationError2 } from "@spfn/core/errors";
|
|
7660
|
+
|
|
7661
|
+
// src/server/lib/oauth/google.ts
|
|
7662
|
+
import { env as env5 } from "@spfn/auth/config";
|
|
7663
|
+
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
7664
|
+
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
7665
|
+
var GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
|
|
7666
|
+
function isGoogleOAuthEnabled() {
|
|
7667
|
+
return !!(env5.SPFN_AUTH_GOOGLE_CLIENT_ID && env5.SPFN_AUTH_GOOGLE_CLIENT_SECRET);
|
|
7668
|
+
}
|
|
7669
|
+
function getGoogleOAuthConfig() {
|
|
7670
|
+
const clientId = env5.SPFN_AUTH_GOOGLE_CLIENT_ID;
|
|
7671
|
+
const clientSecret = env5.SPFN_AUTH_GOOGLE_CLIENT_SECRET;
|
|
7672
|
+
if (!clientId || !clientSecret) {
|
|
7673
|
+
throw new Error("Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET.");
|
|
7674
|
+
}
|
|
7675
|
+
const redirectUri = env5.SPFN_AUTH_GOOGLE_REDIRECT_URI || `${env5.SPFN_API_URL}/_auth/oauth/google/callback`;
|
|
7676
|
+
return {
|
|
7677
|
+
clientId,
|
|
7678
|
+
clientSecret,
|
|
7679
|
+
redirectUri
|
|
7680
|
+
};
|
|
7681
|
+
}
|
|
7682
|
+
function getDefaultScopes() {
|
|
7683
|
+
const envScopes = env5.SPFN_AUTH_GOOGLE_SCOPES;
|
|
7684
|
+
if (envScopes) {
|
|
7685
|
+
return envScopes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
7686
|
+
}
|
|
7687
|
+
return ["email", "profile"];
|
|
7688
|
+
}
|
|
7689
|
+
function getGoogleAuthUrl(state, scopes) {
|
|
7690
|
+
const resolvedScopes = scopes ?? getDefaultScopes();
|
|
7691
|
+
const config = getGoogleOAuthConfig();
|
|
7692
|
+
const params = new URLSearchParams({
|
|
7693
|
+
client_id: config.clientId,
|
|
7694
|
+
redirect_uri: config.redirectUri,
|
|
7695
|
+
response_type: "code",
|
|
7696
|
+
scope: resolvedScopes.join(" "),
|
|
7697
|
+
state,
|
|
7698
|
+
access_type: "offline",
|
|
7699
|
+
// refresh_token 받기 위해
|
|
7700
|
+
prompt: "consent"
|
|
7701
|
+
// 매번 동의 화면 표시 (refresh_token 보장)
|
|
7702
|
+
});
|
|
7703
|
+
return `${GOOGLE_AUTH_URL}?${params.toString()}`;
|
|
7704
|
+
}
|
|
7705
|
+
async function exchangeCodeForTokens(code) {
|
|
7706
|
+
const config = getGoogleOAuthConfig();
|
|
7707
|
+
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
7708
|
+
method: "POST",
|
|
7709
|
+
headers: {
|
|
7710
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
7711
|
+
},
|
|
7712
|
+
body: new URLSearchParams({
|
|
7713
|
+
client_id: config.clientId,
|
|
7714
|
+
client_secret: config.clientSecret,
|
|
7715
|
+
redirect_uri: config.redirectUri,
|
|
7716
|
+
grant_type: "authorization_code",
|
|
7717
|
+
code
|
|
7718
|
+
})
|
|
7719
|
+
});
|
|
7720
|
+
if (!response.ok) {
|
|
7721
|
+
const error = await response.text();
|
|
7722
|
+
throw new Error(`Failed to exchange code for tokens: ${error}`);
|
|
7723
|
+
}
|
|
7724
|
+
return response.json();
|
|
7725
|
+
}
|
|
7726
|
+
async function getGoogleUserInfo(accessToken) {
|
|
7727
|
+
const response = await fetch(GOOGLE_USERINFO_URL, {
|
|
7728
|
+
headers: {
|
|
7729
|
+
Authorization: `Bearer ${accessToken}`
|
|
7730
|
+
}
|
|
7731
|
+
});
|
|
7732
|
+
if (!response.ok) {
|
|
7733
|
+
const error = await response.text();
|
|
7734
|
+
throw new Error(`Failed to get user info: ${error}`);
|
|
7735
|
+
}
|
|
7736
|
+
return response.json();
|
|
7737
|
+
}
|
|
7738
|
+
async function refreshAccessToken(refreshToken) {
|
|
7739
|
+
const config = getGoogleOAuthConfig();
|
|
7740
|
+
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
7741
|
+
method: "POST",
|
|
7742
|
+
headers: {
|
|
7743
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
7744
|
+
},
|
|
7745
|
+
body: new URLSearchParams({
|
|
7746
|
+
client_id: config.clientId,
|
|
7747
|
+
client_secret: config.clientSecret,
|
|
7748
|
+
refresh_token: refreshToken,
|
|
7749
|
+
grant_type: "refresh_token"
|
|
7750
|
+
})
|
|
7751
|
+
});
|
|
7752
|
+
if (!response.ok) {
|
|
7753
|
+
const error = await response.text();
|
|
7754
|
+
throw new Error(`Failed to refresh access token: ${error}`);
|
|
7755
|
+
}
|
|
7756
|
+
return response.json();
|
|
7757
|
+
}
|
|
7758
|
+
|
|
7759
|
+
// src/server/lib/oauth/state.ts
|
|
7760
|
+
import * as jose from "jose";
|
|
7761
|
+
import { env as env6 } from "@spfn/auth/config";
|
|
7762
|
+
async function getStateKey() {
|
|
7763
|
+
const secret = env6.SPFN_AUTH_SESSION_SECRET;
|
|
7764
|
+
const encoder = new TextEncoder();
|
|
7765
|
+
const data = encoder.encode(`oauth-state:${secret}`);
|
|
7766
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
7767
|
+
return new Uint8Array(hashBuffer);
|
|
7768
|
+
}
|
|
7769
|
+
function generateNonce() {
|
|
7770
|
+
const array = new Uint8Array(16);
|
|
7771
|
+
crypto.getRandomValues(array);
|
|
7772
|
+
return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
7773
|
+
}
|
|
7774
|
+
async function createOAuthState(params) {
|
|
7775
|
+
const key = await getStateKey();
|
|
7776
|
+
const state = {
|
|
7777
|
+
returnUrl: params.returnUrl,
|
|
7778
|
+
nonce: generateNonce(),
|
|
7779
|
+
provider: params.provider,
|
|
7780
|
+
publicKey: params.publicKey,
|
|
7781
|
+
keyId: params.keyId,
|
|
7782
|
+
fingerprint: params.fingerprint,
|
|
7783
|
+
algorithm: params.algorithm
|
|
7784
|
+
};
|
|
7785
|
+
const jwe = await new jose.EncryptJWT({ state }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime("10m").encrypt(key);
|
|
7786
|
+
return encodeURIComponent(jwe);
|
|
7787
|
+
}
|
|
7788
|
+
async function verifyOAuthState(encryptedState) {
|
|
7789
|
+
const key = await getStateKey();
|
|
7790
|
+
const jwe = decodeURIComponent(encryptedState);
|
|
7791
|
+
const { payload } = await jose.jwtDecrypt(jwe, key);
|
|
7792
|
+
return payload.state;
|
|
7793
|
+
}
|
|
7794
|
+
|
|
7795
|
+
// src/server/services/oauth.service.ts
|
|
7796
|
+
async function oauthStartService(params) {
|
|
7797
|
+
const { provider, returnUrl, publicKey, keyId, fingerprint, algorithm } = params;
|
|
7798
|
+
if (provider === "google") {
|
|
7799
|
+
if (!isGoogleOAuthEnabled()) {
|
|
7800
|
+
throw new ValidationError2({
|
|
7801
|
+
message: "Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET."
|
|
7802
|
+
});
|
|
7803
|
+
}
|
|
7804
|
+
const state = await createOAuthState({
|
|
7805
|
+
provider: "google",
|
|
7806
|
+
returnUrl,
|
|
7807
|
+
publicKey,
|
|
7808
|
+
keyId,
|
|
7809
|
+
fingerprint,
|
|
7810
|
+
algorithm
|
|
7811
|
+
});
|
|
7812
|
+
const authUrl = getGoogleAuthUrl(state);
|
|
7813
|
+
return { authUrl };
|
|
7814
|
+
}
|
|
7815
|
+
throw new ValidationError2({
|
|
7816
|
+
message: `Unsupported OAuth provider: ${provider}`
|
|
7817
|
+
});
|
|
7818
|
+
}
|
|
7819
|
+
async function oauthCallbackService(params) {
|
|
7820
|
+
const { provider, code, state } = params;
|
|
7821
|
+
const stateData = await verifyOAuthState(state);
|
|
7822
|
+
if (stateData.provider !== provider) {
|
|
7823
|
+
throw new ValidationError2({
|
|
7824
|
+
message: "OAuth state provider mismatch"
|
|
7825
|
+
});
|
|
7826
|
+
}
|
|
7827
|
+
if (provider === "google") {
|
|
7828
|
+
return handleGoogleCallback(code, stateData);
|
|
7829
|
+
}
|
|
7830
|
+
throw new ValidationError2({
|
|
7831
|
+
message: `Unsupported OAuth provider: ${provider}`
|
|
7832
|
+
});
|
|
7833
|
+
}
|
|
7834
|
+
async function handleGoogleCallback(code, stateData) {
|
|
7835
|
+
const tokens = await exchangeCodeForTokens(code);
|
|
7836
|
+
const googleUser = await getGoogleUserInfo(tokens.access_token);
|
|
7837
|
+
const existingSocialAccount = await socialAccountsRepository.findByProviderAndProviderId(
|
|
7838
|
+
"google",
|
|
7839
|
+
googleUser.id
|
|
7840
|
+
);
|
|
7841
|
+
let userId;
|
|
7842
|
+
let isNewUser = false;
|
|
7843
|
+
if (existingSocialAccount) {
|
|
7844
|
+
userId = existingSocialAccount.userId;
|
|
7845
|
+
await socialAccountsRepository.updateTokens(existingSocialAccount.id, {
|
|
7846
|
+
accessToken: tokens.access_token,
|
|
7847
|
+
refreshToken: tokens.refresh_token ?? existingSocialAccount.refreshToken,
|
|
7848
|
+
tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
|
|
7849
|
+
});
|
|
7850
|
+
} else {
|
|
7851
|
+
const result = await createOrLinkUser(googleUser, tokens);
|
|
7852
|
+
userId = result.userId;
|
|
7853
|
+
isNewUser = result.isNewUser;
|
|
7854
|
+
}
|
|
7855
|
+
await registerPublicKeyService({
|
|
7856
|
+
userId,
|
|
7857
|
+
keyId: stateData.keyId,
|
|
7858
|
+
publicKey: stateData.publicKey,
|
|
7859
|
+
fingerprint: stateData.fingerprint,
|
|
7860
|
+
algorithm: stateData.algorithm
|
|
7861
|
+
});
|
|
7862
|
+
await updateLastLoginService(userId);
|
|
7863
|
+
const appUrl = env7.SPFN_APP_URL;
|
|
7864
|
+
const callbackPath = env7.SPFN_AUTH_OAUTH_SUCCESS_URL || "/auth/callback";
|
|
7865
|
+
const callbackUrl = callbackPath.startsWith("http") ? callbackPath : `${appUrl}${callbackPath}`;
|
|
7866
|
+
const redirectUrl = buildRedirectUrl(callbackUrl, {
|
|
7867
|
+
userId: String(userId),
|
|
7868
|
+
keyId: stateData.keyId,
|
|
7869
|
+
returnUrl: stateData.returnUrl,
|
|
7870
|
+
isNewUser: String(isNewUser)
|
|
7871
|
+
});
|
|
7872
|
+
return {
|
|
7873
|
+
redirectUrl,
|
|
7874
|
+
userId: String(userId),
|
|
7875
|
+
keyId: stateData.keyId,
|
|
7876
|
+
isNewUser
|
|
7877
|
+
};
|
|
7878
|
+
}
|
|
7879
|
+
async function createOrLinkUser(googleUser, tokens) {
|
|
7880
|
+
const existingUser = googleUser.email ? await usersRepository.findByEmail(googleUser.email) : null;
|
|
7881
|
+
let userId;
|
|
7882
|
+
let isNewUser = false;
|
|
7883
|
+
if (existingUser) {
|
|
7884
|
+
if (!googleUser.verified_email) {
|
|
7885
|
+
throw new ValidationError2({
|
|
7886
|
+
message: "Cannot link to existing account with unverified email. Please verify your email with Google first."
|
|
7887
|
+
});
|
|
7888
|
+
}
|
|
7889
|
+
userId = existingUser.id;
|
|
7890
|
+
if (!existingUser.emailVerifiedAt) {
|
|
7891
|
+
await usersRepository.updateById(existingUser.id, {
|
|
7892
|
+
emailVerifiedAt: /* @__PURE__ */ new Date()
|
|
7893
|
+
});
|
|
7894
|
+
}
|
|
7895
|
+
} else {
|
|
7896
|
+
const { getRoleByName: getRoleByName3 } = await Promise.resolve().then(() => (init_role_service(), role_service_exports));
|
|
7897
|
+
const userRole = await getRoleByName3("user");
|
|
7898
|
+
if (!userRole) {
|
|
7899
|
+
throw new Error("Default user role not found. Run initializeAuth() first.");
|
|
7900
|
+
}
|
|
7901
|
+
const newUser = await usersRepository.create({
|
|
7902
|
+
email: googleUser.verified_email ? googleUser.email : null,
|
|
7903
|
+
phone: null,
|
|
7904
|
+
passwordHash: null,
|
|
7905
|
+
// OAuth 사용자는 비밀번호 없음
|
|
7906
|
+
passwordChangeRequired: false,
|
|
7907
|
+
roleId: userRole.id,
|
|
7908
|
+
status: "active",
|
|
7909
|
+
emailVerifiedAt: googleUser.verified_email ? /* @__PURE__ */ new Date() : null
|
|
7910
|
+
});
|
|
7911
|
+
userId = newUser.id;
|
|
7912
|
+
isNewUser = true;
|
|
7913
|
+
}
|
|
7914
|
+
await socialAccountsRepository.create({
|
|
7915
|
+
userId,
|
|
7916
|
+
provider: "google",
|
|
7917
|
+
providerUserId: googleUser.id,
|
|
7918
|
+
providerEmail: googleUser.email,
|
|
7919
|
+
accessToken: tokens.access_token,
|
|
7920
|
+
refreshToken: tokens.refresh_token ?? null,
|
|
7921
|
+
tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
|
|
7922
|
+
});
|
|
7923
|
+
return { userId, isNewUser };
|
|
7924
|
+
}
|
|
7925
|
+
function buildRedirectUrl(baseUrl, params) {
|
|
7926
|
+
const url = new URL(baseUrl, "http://placeholder");
|
|
7927
|
+
for (const [key, value] of Object.entries(params)) {
|
|
7928
|
+
url.searchParams.set(key, value);
|
|
7929
|
+
}
|
|
7930
|
+
if (baseUrl.startsWith("http")) {
|
|
7931
|
+
return url.toString();
|
|
7932
|
+
}
|
|
7933
|
+
return `${url.pathname}${url.search}`;
|
|
7934
|
+
}
|
|
7935
|
+
function buildOAuthErrorUrl(error) {
|
|
7936
|
+
const errorUrl = env7.SPFN_AUTH_OAUTH_ERROR_URL || "/auth/error?error={error}";
|
|
7937
|
+
return errorUrl.replace("{error}", encodeURIComponent(error));
|
|
7938
|
+
}
|
|
7939
|
+
function isOAuthProviderEnabled(provider) {
|
|
7940
|
+
switch (provider) {
|
|
7941
|
+
case "google":
|
|
7942
|
+
return isGoogleOAuthEnabled();
|
|
7943
|
+
case "github":
|
|
7944
|
+
case "kakao":
|
|
7945
|
+
case "naver":
|
|
7946
|
+
return false;
|
|
7947
|
+
default:
|
|
7948
|
+
return false;
|
|
7949
|
+
}
|
|
7950
|
+
}
|
|
7951
|
+
function getEnabledOAuthProviders() {
|
|
7952
|
+
const providers = [];
|
|
7953
|
+
if (isGoogleOAuthEnabled()) {
|
|
7954
|
+
providers.push("google");
|
|
7955
|
+
}
|
|
7956
|
+
return providers;
|
|
7957
|
+
}
|
|
7958
|
+
var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
|
|
7959
|
+
async function getGoogleAccessToken(userId) {
|
|
7960
|
+
const account = await socialAccountsRepository.findByUserIdAndProvider(userId, "google");
|
|
7961
|
+
if (!account) {
|
|
7962
|
+
throw new ValidationError2({
|
|
7963
|
+
message: "No Google account linked. User must sign in with Google first."
|
|
7964
|
+
});
|
|
7965
|
+
}
|
|
7966
|
+
const isExpired = !account.tokenExpiresAt || account.tokenExpiresAt.getTime() < Date.now() + TOKEN_EXPIRY_BUFFER_MS;
|
|
7967
|
+
if (!isExpired && account.accessToken) {
|
|
7968
|
+
return account.accessToken;
|
|
7969
|
+
}
|
|
7970
|
+
if (!account.refreshToken) {
|
|
7971
|
+
throw new ValidationError2({
|
|
7972
|
+
message: "Google refresh token not available. User must re-authenticate with Google."
|
|
7973
|
+
});
|
|
7974
|
+
}
|
|
7975
|
+
const tokens = await refreshAccessToken(account.refreshToken);
|
|
7976
|
+
await socialAccountsRepository.updateTokens(account.id, {
|
|
7977
|
+
accessToken: tokens.access_token,
|
|
7978
|
+
refreshToken: tokens.refresh_token ?? account.refreshToken,
|
|
7979
|
+
tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
|
|
7980
|
+
});
|
|
7981
|
+
return tokens.access_token;
|
|
7982
|
+
}
|
|
7983
|
+
|
|
7563
7984
|
// src/server/routes/auth/index.ts
|
|
7564
7985
|
init_esm();
|
|
7565
7986
|
import { Transactional } from "@spfn/core/db";
|
|
@@ -8170,8 +8591,135 @@ var userRouter = defineRouter3({
|
|
|
8170
8591
|
updateUserProfile
|
|
8171
8592
|
});
|
|
8172
8593
|
|
|
8594
|
+
// src/server/routes/oauth/index.ts
|
|
8595
|
+
init_esm();
|
|
8596
|
+
init_types();
|
|
8597
|
+
import { Transactional as Transactional2 } from "@spfn/core/db";
|
|
8598
|
+
import { defineRouter as defineRouter4, route as route4 } from "@spfn/core/route";
|
|
8599
|
+
var oauthGoogleStart = route4.get("/_auth/oauth/google").input({
|
|
8600
|
+
query: Type.Object({
|
|
8601
|
+
state: Type.String({
|
|
8602
|
+
description: "Encrypted OAuth state (returnUrl, publicKey, keyId, fingerprint, algorithm)"
|
|
8603
|
+
})
|
|
8604
|
+
})
|
|
8605
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
8606
|
+
const { query } = await c.data();
|
|
8607
|
+
if (!isGoogleOAuthEnabled()) {
|
|
8608
|
+
return c.redirect(buildOAuthErrorUrl("Google OAuth is not configured"));
|
|
8609
|
+
}
|
|
8610
|
+
const authUrl = getGoogleAuthUrl(query.state);
|
|
8611
|
+
return c.redirect(authUrl);
|
|
8612
|
+
});
|
|
8613
|
+
var oauthGoogleCallback = route4.get("/_auth/oauth/google/callback").input({
|
|
8614
|
+
query: Type.Object({
|
|
8615
|
+
code: Type.Optional(Type.String({
|
|
8616
|
+
description: "Authorization code from Google"
|
|
8617
|
+
})),
|
|
8618
|
+
state: Type.Optional(Type.String({
|
|
8619
|
+
description: "OAuth state parameter"
|
|
8620
|
+
})),
|
|
8621
|
+
error: Type.Optional(Type.String({
|
|
8622
|
+
description: "Error code from Google"
|
|
8623
|
+
})),
|
|
8624
|
+
error_description: Type.Optional(Type.String({
|
|
8625
|
+
description: "Error description from Google"
|
|
8626
|
+
}))
|
|
8627
|
+
})
|
|
8628
|
+
}).use([Transactional2()]).skip(["auth"]).handler(async (c) => {
|
|
8629
|
+
const { query } = await c.data();
|
|
8630
|
+
if (query.error) {
|
|
8631
|
+
const errorMessage = query.error_description || query.error;
|
|
8632
|
+
return c.redirect(buildOAuthErrorUrl(errorMessage));
|
|
8633
|
+
}
|
|
8634
|
+
if (!query.code || !query.state) {
|
|
8635
|
+
return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
|
|
8636
|
+
}
|
|
8637
|
+
try {
|
|
8638
|
+
const result = await oauthCallbackService({
|
|
8639
|
+
provider: "google",
|
|
8640
|
+
code: query.code,
|
|
8641
|
+
state: query.state
|
|
8642
|
+
});
|
|
8643
|
+
return c.redirect(result.redirectUrl);
|
|
8644
|
+
} catch (err) {
|
|
8645
|
+
const message = err instanceof Error ? err.message : "OAuth callback failed";
|
|
8646
|
+
return c.redirect(buildOAuthErrorUrl(message));
|
|
8647
|
+
}
|
|
8648
|
+
});
|
|
8649
|
+
var oauthStart = route4.post("/_auth/oauth/start").input({
|
|
8650
|
+
body: Type.Object({
|
|
8651
|
+
provider: Type.Union(SOCIAL_PROVIDERS.map((p) => Type.Literal(p)), {
|
|
8652
|
+
description: "OAuth provider (google, github, kakao, naver)"
|
|
8653
|
+
}),
|
|
8654
|
+
returnUrl: Type.String({
|
|
8655
|
+
description: "URL to redirect after OAuth success"
|
|
8656
|
+
}),
|
|
8657
|
+
publicKey: Type.String({
|
|
8658
|
+
description: "Client public key (Base64 DER)"
|
|
8659
|
+
}),
|
|
8660
|
+
keyId: Type.String({
|
|
8661
|
+
description: "Key identifier (UUID)"
|
|
8662
|
+
}),
|
|
8663
|
+
fingerprint: Type.String({
|
|
8664
|
+
description: "Key fingerprint (SHA-256 hex)"
|
|
8665
|
+
}),
|
|
8666
|
+
algorithm: Type.Union(KEY_ALGORITHM.map((a) => Type.Literal(a)), {
|
|
8667
|
+
description: "Key algorithm (ES256 or RS256)"
|
|
8668
|
+
})
|
|
8669
|
+
})
|
|
8670
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
8671
|
+
const { body } = await c.data();
|
|
8672
|
+
const result = await oauthStartService(body);
|
|
8673
|
+
return result;
|
|
8674
|
+
});
|
|
8675
|
+
var oauthProviders = route4.get("/_auth/oauth/providers").skip(["auth"]).handler(async () => {
|
|
8676
|
+
return {
|
|
8677
|
+
providers: getEnabledOAuthProviders()
|
|
8678
|
+
};
|
|
8679
|
+
});
|
|
8680
|
+
var getGoogleOAuthUrl = route4.post("/_auth/oauth/google/url").input({
|
|
8681
|
+
body: Type.Object({
|
|
8682
|
+
returnUrl: Type.Optional(Type.String({
|
|
8683
|
+
description: "URL to redirect after OAuth success"
|
|
8684
|
+
})),
|
|
8685
|
+
state: Type.Optional(Type.String({
|
|
8686
|
+
description: "Encrypted OAuth state (injected by interceptor)"
|
|
8687
|
+
}))
|
|
8688
|
+
})
|
|
8689
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
8690
|
+
const { body } = await c.data();
|
|
8691
|
+
if (!isGoogleOAuthEnabled()) {
|
|
8692
|
+
throw new Error("Google OAuth is not configured");
|
|
8693
|
+
}
|
|
8694
|
+
if (!body.state) {
|
|
8695
|
+
throw new Error("OAuth state is required. Ensure the OAuth interceptor is configured.");
|
|
8696
|
+
}
|
|
8697
|
+
return { authUrl: getGoogleAuthUrl(body.state) };
|
|
8698
|
+
});
|
|
8699
|
+
var oauthFinalize = route4.post("/_auth/oauth/finalize").input({
|
|
8700
|
+
body: Type.Object({
|
|
8701
|
+
userId: Type.String({ description: "User ID from OAuth callback" }),
|
|
8702
|
+
keyId: Type.String({ description: "Key ID from OAuth state" }),
|
|
8703
|
+
returnUrl: Type.Optional(Type.String({ description: "URL to redirect after login" }))
|
|
8704
|
+
})
|
|
8705
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
8706
|
+
const { body } = await c.data();
|
|
8707
|
+
return {
|
|
8708
|
+
success: true,
|
|
8709
|
+
returnUrl: body.returnUrl || "/"
|
|
8710
|
+
};
|
|
8711
|
+
});
|
|
8712
|
+
var oauthRouter = defineRouter4({
|
|
8713
|
+
oauthGoogleStart,
|
|
8714
|
+
oauthGoogleCallback,
|
|
8715
|
+
oauthStart,
|
|
8716
|
+
oauthProviders,
|
|
8717
|
+
getGoogleOAuthUrl,
|
|
8718
|
+
oauthFinalize
|
|
8719
|
+
});
|
|
8720
|
+
|
|
8173
8721
|
// src/server/routes/index.ts
|
|
8174
|
-
var mainAuthRouter =
|
|
8722
|
+
var mainAuthRouter = defineRouter5({
|
|
8175
8723
|
// Auth routes
|
|
8176
8724
|
checkAccountExists,
|
|
8177
8725
|
sendVerificationCode,
|
|
@@ -8182,6 +8730,13 @@ var mainAuthRouter = defineRouter4({
|
|
|
8182
8730
|
rotateKey,
|
|
8183
8731
|
changePassword,
|
|
8184
8732
|
getAuthSession,
|
|
8733
|
+
// OAuth routes
|
|
8734
|
+
oauthGoogleStart,
|
|
8735
|
+
oauthGoogleCallback,
|
|
8736
|
+
oauthStart,
|
|
8737
|
+
oauthProviders,
|
|
8738
|
+
getGoogleOAuthUrl,
|
|
8739
|
+
oauthFinalize,
|
|
8185
8740
|
// Invitation routes
|
|
8186
8741
|
getInvitation,
|
|
8187
8742
|
acceptInvitation: acceptInvitation2,
|
|
@@ -8302,11 +8857,11 @@ function shouldRotateKey(createdAt, rotationDays = 90) {
|
|
|
8302
8857
|
}
|
|
8303
8858
|
|
|
8304
8859
|
// src/server/lib/session.ts
|
|
8305
|
-
import * as
|
|
8306
|
-
import { env as
|
|
8860
|
+
import * as jose2 from "jose";
|
|
8861
|
+
import { env as env8 } from "@spfn/auth/config";
|
|
8307
8862
|
import { env as coreEnv } from "@spfn/core/config";
|
|
8308
8863
|
async function getSessionSecretKey() {
|
|
8309
|
-
const secret =
|
|
8864
|
+
const secret = env8.SPFN_AUTH_SESSION_SECRET;
|
|
8310
8865
|
const encoder = new TextEncoder();
|
|
8311
8866
|
const data = encoder.encode(secret);
|
|
8312
8867
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
@@ -8314,24 +8869,24 @@ async function getSessionSecretKey() {
|
|
|
8314
8869
|
}
|
|
8315
8870
|
async function sealSession(data, ttl = 60 * 60 * 24 * 7) {
|
|
8316
8871
|
const secret = await getSessionSecretKey();
|
|
8317
|
-
return await new
|
|
8872
|
+
return await new jose2.EncryptJWT({ data }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${ttl}s`).setIssuer("spfn-auth").setAudience("spfn-client").encrypt(secret);
|
|
8318
8873
|
}
|
|
8319
8874
|
async function unsealSession(jwt4) {
|
|
8320
8875
|
try {
|
|
8321
8876
|
const secret = await getSessionSecretKey();
|
|
8322
|
-
const { payload } = await
|
|
8877
|
+
const { payload } = await jose2.jwtDecrypt(jwt4, secret, {
|
|
8323
8878
|
issuer: "spfn-auth",
|
|
8324
8879
|
audience: "spfn-client"
|
|
8325
8880
|
});
|
|
8326
8881
|
return payload.data;
|
|
8327
8882
|
} catch (err) {
|
|
8328
|
-
if (err instanceof
|
|
8883
|
+
if (err instanceof jose2.errors.JWTExpired) {
|
|
8329
8884
|
throw new Error("Session expired");
|
|
8330
8885
|
}
|
|
8331
|
-
if (err instanceof
|
|
8886
|
+
if (err instanceof jose2.errors.JWEDecryptionFailed) {
|
|
8332
8887
|
throw new Error("Invalid session");
|
|
8333
8888
|
}
|
|
8334
|
-
if (err instanceof
|
|
8889
|
+
if (err instanceof jose2.errors.JWTClaimValidationFailed) {
|
|
8335
8890
|
throw new Error("Session validation failed");
|
|
8336
8891
|
}
|
|
8337
8892
|
throw new Error("Failed to unseal session");
|
|
@@ -8340,7 +8895,7 @@ async function unsealSession(jwt4) {
|
|
|
8340
8895
|
async function getSessionInfo(jwt4) {
|
|
8341
8896
|
const secret = await getSessionSecretKey();
|
|
8342
8897
|
try {
|
|
8343
|
-
const { payload } = await
|
|
8898
|
+
const { payload } = await jose2.jwtDecrypt(jwt4, secret);
|
|
8344
8899
|
return {
|
|
8345
8900
|
issuedAt: new Date(payload.iat * 1e3),
|
|
8346
8901
|
expiresAt: new Date(payload.exp * 1e3),
|
|
@@ -8364,14 +8919,14 @@ async function shouldRefreshSession(jwt4, thresholdHours = 24) {
|
|
|
8364
8919
|
}
|
|
8365
8920
|
|
|
8366
8921
|
// src/server/setup.ts
|
|
8367
|
-
import { env as
|
|
8922
|
+
import { env as env9 } from "@spfn/auth/config";
|
|
8368
8923
|
import { getRoleByName as getRoleByName2 } from "@spfn/auth/server";
|
|
8369
8924
|
init_repositories();
|
|
8370
8925
|
function parseAdminAccounts() {
|
|
8371
8926
|
const accounts = [];
|
|
8372
|
-
if (
|
|
8927
|
+
if (env9.SPFN_AUTH_ADMIN_ACCOUNTS) {
|
|
8373
8928
|
try {
|
|
8374
|
-
const accountsJson =
|
|
8929
|
+
const accountsJson = env9.SPFN_AUTH_ADMIN_ACCOUNTS;
|
|
8375
8930
|
const parsed = JSON.parse(accountsJson);
|
|
8376
8931
|
if (!Array.isArray(parsed)) {
|
|
8377
8932
|
authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_ACCOUNTS must be an array");
|
|
@@ -8398,11 +8953,11 @@ function parseAdminAccounts() {
|
|
|
8398
8953
|
return accounts;
|
|
8399
8954
|
}
|
|
8400
8955
|
}
|
|
8401
|
-
const adminEmails =
|
|
8956
|
+
const adminEmails = env9.SPFN_AUTH_ADMIN_EMAILS;
|
|
8402
8957
|
if (adminEmails) {
|
|
8403
8958
|
const emails = adminEmails.split(",").map((s) => s.trim());
|
|
8404
|
-
const passwords = (
|
|
8405
|
-
const roles2 = (
|
|
8959
|
+
const passwords = (env9.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
|
|
8960
|
+
const roles2 = (env9.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
|
|
8406
8961
|
if (passwords.length !== emails.length) {
|
|
8407
8962
|
authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_EMAILS and SPFN_AUTH_ADMIN_PASSWORDS length mismatch");
|
|
8408
8963
|
return accounts;
|
|
@@ -8424,8 +8979,8 @@ function parseAdminAccounts() {
|
|
|
8424
8979
|
}
|
|
8425
8980
|
return accounts;
|
|
8426
8981
|
}
|
|
8427
|
-
const adminEmail =
|
|
8428
|
-
const adminPassword =
|
|
8982
|
+
const adminEmail = env9.SPFN_AUTH_ADMIN_EMAIL;
|
|
8983
|
+
const adminPassword = env9.SPFN_AUTH_ADMIN_PASSWORD;
|
|
8429
8984
|
if (adminEmail && adminPassword) {
|
|
8430
8985
|
accounts.push({
|
|
8431
8986
|
email: adminEmail,
|
|
@@ -8515,6 +9070,7 @@ export {
|
|
|
8515
9070
|
RolePermissionsRepository,
|
|
8516
9071
|
RolesRepository,
|
|
8517
9072
|
SOCIAL_PROVIDERS,
|
|
9073
|
+
SocialAccountsRepository,
|
|
8518
9074
|
TargetTypeSchema,
|
|
8519
9075
|
USER_STATUSES,
|
|
8520
9076
|
UserPermissionsRepository,
|
|
@@ -8530,16 +9086,19 @@ export {
|
|
|
8530
9086
|
mainAuthRouter as authRouter,
|
|
8531
9087
|
authSchema,
|
|
8532
9088
|
authenticate,
|
|
9089
|
+
buildOAuthErrorUrl,
|
|
8533
9090
|
cancelInvitation,
|
|
8534
9091
|
changePasswordService,
|
|
8535
9092
|
checkAccountExistsService,
|
|
8536
9093
|
configureAuth,
|
|
8537
9094
|
createAuthLifecycle,
|
|
8538
9095
|
createInvitation,
|
|
9096
|
+
createOAuthState,
|
|
8539
9097
|
createRole,
|
|
8540
9098
|
decodeToken,
|
|
8541
9099
|
deleteInvitation,
|
|
8542
9100
|
deleteRole,
|
|
9101
|
+
exchangeCodeForTokens,
|
|
8543
9102
|
expireOldInvitations,
|
|
8544
9103
|
generateClientToken,
|
|
8545
9104
|
generateKeyPair,
|
|
@@ -8550,6 +9109,11 @@ export {
|
|
|
8550
9109
|
getAuth,
|
|
8551
9110
|
getAuthConfig,
|
|
8552
9111
|
getAuthSessionService,
|
|
9112
|
+
getEnabledOAuthProviders,
|
|
9113
|
+
getGoogleAccessToken,
|
|
9114
|
+
getGoogleAuthUrl,
|
|
9115
|
+
getGoogleOAuthConfig,
|
|
9116
|
+
getGoogleUserInfo,
|
|
8553
9117
|
getInvitationByToken,
|
|
8554
9118
|
getInvitationWithDetails,
|
|
8555
9119
|
getKeyId,
|
|
@@ -8574,13 +9138,18 @@ export {
|
|
|
8574
9138
|
hashPassword,
|
|
8575
9139
|
initializeAuth,
|
|
8576
9140
|
invitationsRepository,
|
|
9141
|
+
isGoogleOAuthEnabled,
|
|
9142
|
+
isOAuthProviderEnabled,
|
|
8577
9143
|
keysRepository,
|
|
8578
9144
|
listInvitations,
|
|
8579
9145
|
loginService,
|
|
8580
9146
|
logoutService,
|
|
9147
|
+
oauthCallbackService,
|
|
9148
|
+
oauthStartService,
|
|
8581
9149
|
parseDuration,
|
|
8582
9150
|
permissions,
|
|
8583
9151
|
permissionsRepository,
|
|
9152
|
+
refreshAccessToken,
|
|
8584
9153
|
registerPublicKeyService,
|
|
8585
9154
|
registerService,
|
|
8586
9155
|
removePermissionFromRole,
|
|
@@ -8600,6 +9169,7 @@ export {
|
|
|
8600
9169
|
setRolePermissions,
|
|
8601
9170
|
shouldRefreshSession,
|
|
8602
9171
|
shouldRotateKey,
|
|
9172
|
+
socialAccountsRepository,
|
|
8603
9173
|
unsealSession,
|
|
8604
9174
|
updateLastLoginService,
|
|
8605
9175
|
updateRole,
|
|
@@ -8621,6 +9191,7 @@ export {
|
|
|
8621
9191
|
verifyClientToken,
|
|
8622
9192
|
verifyCodeService,
|
|
8623
9193
|
verifyKeyFingerprint,
|
|
9194
|
+
verifyOAuthState,
|
|
8624
9195
|
verifyPassword,
|
|
8625
9196
|
verifyToken
|
|
8626
9197
|
};
|