@spfn/auth 0.2.0-beta.82 → 0.2.0-beta.84
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 +50 -5
- package/dist/{authenticate-B6CbndRk.d.ts → authenticate-ngWdlWWL.d.ts} +71 -1
- package/dist/config.d.ts +20 -0
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +27 -3
- package/dist/server.js +219 -3
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { j as AuthInitOptions, k as OAuthProvider, e as VerificationPurpose, i as PermissionCategory, l as AuthContext } from './authenticate-
|
|
2
|
-
export { C as ChangePasswordParams,
|
|
1
|
+
import { j as AuthInitOptions, k as OAuthProvider, e as VerificationPurpose, i as PermissionCategory, l as AuthContext } from './authenticate-ngWdlWWL.js';
|
|
2
|
+
export { C as ChangePasswordParams, a5 as EmailSchema, I as IssueOneTimeTokenResult, s as LoginParams, L as LoginResult, t as LogoutParams, ac as NativeVerifyOptions, aa as NormalizedIdentity, _ as OAuthCallbackParams, $ as OAuthCallbackResult, ad as OAuthCodeExchangeOptions, a2 as OAuthNativeParams, c as OAuthNativeResult, Z as OAuthStartParams, O as OAuthStartResult, ab as OAuthTokens, a7 as PasswordSchema, a6 as PhoneSchema, q as RegisterParams, E as RegisterPublicKeyParams, a as RegisterResult, G as RevokeKeyParams, F as RotateKeyParams, b as RotateKeyResult, w as SendVerificationCodeParams, S as SendVerificationCodeResult, a8 as TargetTypeSchema, af as UnlinkNotification, ag as UnlinkNotifyRejection, ae as UnlinkNotifyRequest, a0 as UnlinkNotifyResult, g as VERIFICATION_PURPOSES, f as VERIFICATION_TARGET_TYPES, a9 as VerificationPurposeSchema, V as VerificationTargetType, x as VerifyCodeParams, y as VerifyCodeResult, m as authRouter, a3 as authenticate, N as buildOAuthErrorUrl, p as changePasswordService, W as getEnabledOAuthProviders, X as getGoogleAccessToken, ai as getOAuthProvider, aj as getRegisteredProviders, Q as isOAuthProviderEnabled, H as issueOneTimeTokenService, n as loginService, o as logoutService, M as oauthCallbackService, a1 as oauthNativeService, K as oauthStartService, Y as oauthUnlinkNotifyService, a4 as optionalAuth, ah as registerOAuthProvider, z as registerPublicKeyService, r as registerService, T as requireEnabledProvider, D as revokeKeyService, B as rotateKeyService, u as sendVerificationCodeService, v as verifyCodeService, J as verifyOneTimeTokenService } from './authenticate-ngWdlWWL.js';
|
|
3
3
|
import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
|
|
4
4
|
import { K as KeyAlgorithmType, c as InvitationStatus, e as SocialProvider, g as AccountDeletionRequestedBy, h as PurgeStrategy } from './types-1BMx0OX1.js';
|
|
5
5
|
export { b as ACCOUNT_DELETION_REQUESTED_BY, A as ACCOUNT_DELETION_REQUEST_STATUSES, f as AccountDeletionRequestStatus, I as INVITATION_STATUSES, a as KEY_ALGORITHM, P as PURGE_STRATEGIES, S as SOCIAL_PROVIDERS, U as USER_STATUSES, d as UserStatus } from './types-1BMx0OX1.js';
|
|
@@ -6434,6 +6434,29 @@ declare const authDeletionCompletedEvent: _spfn_core_event.EventDef<{
|
|
|
6434
6434
|
userPublicId: string;
|
|
6435
6435
|
purgeStrategy: "anonymize" | "hard-delete";
|
|
6436
6436
|
}>;
|
|
6437
|
+
/**
|
|
6438
|
+
* auth.oauth.unlinked - provider발 연동 해제 이벤트
|
|
6439
|
+
*
|
|
6440
|
+
* 발행 시점:
|
|
6441
|
+
* - provider(카카오·네이버 등)가 unlink-notify 웹훅으로 연동 해제를 알려와
|
|
6442
|
+
* 소셜 계정 연결과 저장 토큰이 삭제된 직후
|
|
6443
|
+
*
|
|
6444
|
+
* 연결 삭제까지는 프레임워크가 수행하고, 그 이후(계정 탈퇴로 이어갈지 등)는
|
|
6445
|
+
* 앱 정책이므로 이 이벤트를 구독해 처리한다.
|
|
6446
|
+
*
|
|
6447
|
+
* @example
|
|
6448
|
+
* ```typescript
|
|
6449
|
+
* oauthUnlinkedEvent.subscribe(async (payload) => {
|
|
6450
|
+
* await requestAccountDeletionService({ userId: payload.userId, requestedBy: 'self' });
|
|
6451
|
+
* });
|
|
6452
|
+
* ```
|
|
6453
|
+
*/
|
|
6454
|
+
declare const oauthUnlinkedEvent: _spfn_core_event.EventDef<{
|
|
6455
|
+
reason?: string | undefined;
|
|
6456
|
+
userId: string;
|
|
6457
|
+
provider: "email" | "phone" | "google" | "apple" | "github" | "kakao" | "naver" | "superself";
|
|
6458
|
+
providerUserId: string;
|
|
6459
|
+
}>;
|
|
6437
6460
|
/**
|
|
6438
6461
|
* Auth event payload types
|
|
6439
6462
|
*/
|
|
@@ -6444,5 +6467,6 @@ type InvitationAcceptedPayload = typeof invitationAcceptedEvent._payload;
|
|
|
6444
6467
|
type AuthDeletionRequestedPayload = typeof authDeletionRequestedEvent._payload;
|
|
6445
6468
|
type AuthDeletionCancelledPayload = typeof authDeletionCancelledEvent._payload;
|
|
6446
6469
|
type AuthDeletionCompletedPayload = typeof authDeletionCompletedEvent._payload;
|
|
6470
|
+
type OAuthUnlinkedPayload = typeof oauthUnlinkedEvent._payload;
|
|
6447
6471
|
|
|
6448
|
-
export { type AccountDeletionPurgeUser, type AccountDeletionRequest, AccountDeletionRequestedBy, AccountDeletionRequestsRepository, type AuthConfig, AuthContext, type AuthDeletionCancelledPayload, type AuthDeletionCompletedPayload, type AuthDeletionConfig, type AuthDeletionRequestedPayload, type AuthLifecycleConfig, type AuthLifecycleOptions, type AuthLoginPayload, type AuthMetadataEntity, AuthMetadataRepository, AuthProviderSchema, type AuthRegisterPayload, type BeforeRegisterContext, COOKIE_NAMES, type CancelAccountDeletionParams, type CancelAccountDeletionResult, type CreateOAuthStateParams, DEFAULT_DELETION_ALLOW_SELF_IMMEDIATE, DEFAULT_DELETION_GRACE_PERIOD_DAYS, DEFAULT_DELETION_PURGE_CRON, DEFAULT_DELETION_PURGE_STRATEGY, DEFAULT_DELETION_SEND_NOTIFICATIONS, type DecryptedOAuthToken, EnvironmentKeyringTokenCipher, type GoogleTokenResponse, type GoogleUserInfo, type Invitation, type InvitationAcceptedPayload, type InvitationCreatedPayload, InvitationStatus, InvitationsRepository, KeyAlgorithmType, type KeyPair, KeysRepository, type NewAccountDeletionRequest, type NewAuthMetadataEntity, type NewInvitation, type NewPermission, type NewPermissionEntity, type NewRole, type NewRoleEntity, type NewRolePermission, type NewUser, type NewUserPermission, type NewUserProfile, type NewUserPublicKey, type NewUserSocialAccount, type NewVerificationCode, OAuthProvider, type OAuthState, type OAuthTokenCipher, type OAuthTokenContext, type OAuthTokenType, type PendingDeletionInfo, type Permission, type PermissionEntity, PermissionsRepository, PurgeStrategy, type PurgeUserResult, type RegisterChannel, type RequestAccountDeletionParams, type RequestAccountDeletionResult, type Role, type RoleEntity, type RoleGuardOptions, type RolePermission, RolePermissionsRepository, RolesRepository, type SessionPayload, SocialAccountsRepository, SocialProvider, type SweepDuePurgesResult, type TokenPayload, type UpdateProfileParams, type User, type UserPermission, UserPermissionsRepository, type UserProfile, UserProfilesRepository, type UserPublicKey, type UserSocialAccount, UsersRepository, type VerificationCode, VerificationCodesRepository, VerificationPurpose, type VerifyIdTokenParams, acceptInvitation, accountDeletionRequests, accountDeletionRequestsRepository, addPermissionToRole, appleProvider, assertCanAssignRole, authDeletionCancelledEvent, authDeletionCompletedEvent, authDeletionRequestedEvent, authJobRouter, authLogger, authLoginEvent, authMetadata, authMetadataRepository, authRegisterEvent, authSchema, cancelAccountDeletionService, cancelInvitation, checkUsernameAvailableService, configureAuth, configureDeletion, configureOAuthTokenCipher, createAuthDeletionJobRouter, createAuthDeletionPurgeJob, createAuthLifecycle, createInvitation, createOAuthState, createRole, decodeToken, decryptToken, deleteInvitation, deleteRole, encryptToken, exchangeCodeForTokens, expireOldInvitations, generateClientToken, generateKeyPair, generateKeyPairES256, generateKeyPairRS256, generateOAuthNonce, generateToken, getAllRoles, getAuth, getAuthConfig, getAuthSessionService, getDeletionConfig, getDummyPasswordHash, getGoogleAuthUrl, getGoogleOAuthConfig, getGoogleUserInfo, getInvitationByToken, getInvitationWithDetails, getKeyId, getKeySize, getLocale, getOneTimeTokenManager, getOptionalAuth, getPendingDeletionInfo, getRole, getRoleByName, getRolePermissions, getSessionTtl, getUser, getUserByEmailService, getUserByIdService, getUserByPhoneService, getUserId, getUserPermissions, getUserProfileService, getUserRole, githubProvider, googleProvider, hasAllPermissions, hasAnyPermission, hasAnyRole, hasPermission, hasRole, hashPassword, initOneTimeTokenManager, initializeAuth, invitationAcceptedEvent, invitationCreatedEvent, invitationsRepository, isEncrypted, isGoogleOAuthEnabled, kakaoProvider, keysRepository, listInvitations, matchOAuthCsrfCookies, naverProvider, oneTimeTokenAuth, parseDuration, permissions, permissionsRepository, purgeUserService, refreshAccessToken, removePermissionFromRole, requestAccountDeletionService, requireAnyPermission, requirePermissions, requireRole, resendInvitation, roleGuard, rolePermissions, rolePermissionsRepository, roles, rolesRepository, runBeforeRegister, setRolePermissions, shouldRotateKey, socialAccountsRepository, sweepDuePurges, updateLastLoginService, updateLocaleService, updateRole, updateUserProfileService, updateUserService, updateUsernameService, userInvitations, userPermissions, userPermissionsRepository, userProfiles, userProfilesRepository, userPublicKeys, userSocialAccounts, users, usersRepository, validateInvitation, validatePasswordStrength, verificationCodes, verificationCodesRepository, verifyClientToken, verifyIdToken, verifyKeyFingerprint, verifyOAuthState, verifyPassword, verifyToken };
|
|
6472
|
+
export { type AccountDeletionPurgeUser, type AccountDeletionRequest, AccountDeletionRequestedBy, AccountDeletionRequestsRepository, type AuthConfig, AuthContext, type AuthDeletionCancelledPayload, type AuthDeletionCompletedPayload, type AuthDeletionConfig, type AuthDeletionRequestedPayload, type AuthLifecycleConfig, type AuthLifecycleOptions, type AuthLoginPayload, type AuthMetadataEntity, AuthMetadataRepository, AuthProviderSchema, type AuthRegisterPayload, type BeforeRegisterContext, COOKIE_NAMES, type CancelAccountDeletionParams, type CancelAccountDeletionResult, type CreateOAuthStateParams, DEFAULT_DELETION_ALLOW_SELF_IMMEDIATE, DEFAULT_DELETION_GRACE_PERIOD_DAYS, DEFAULT_DELETION_PURGE_CRON, DEFAULT_DELETION_PURGE_STRATEGY, DEFAULT_DELETION_SEND_NOTIFICATIONS, type DecryptedOAuthToken, EnvironmentKeyringTokenCipher, type GoogleTokenResponse, type GoogleUserInfo, type Invitation, type InvitationAcceptedPayload, type InvitationCreatedPayload, InvitationStatus, InvitationsRepository, KeyAlgorithmType, type KeyPair, KeysRepository, type NewAccountDeletionRequest, type NewAuthMetadataEntity, type NewInvitation, type NewPermission, type NewPermissionEntity, type NewRole, type NewRoleEntity, type NewRolePermission, type NewUser, type NewUserPermission, type NewUserProfile, type NewUserPublicKey, type NewUserSocialAccount, type NewVerificationCode, OAuthProvider, type OAuthState, type OAuthTokenCipher, type OAuthTokenContext, type OAuthTokenType, type OAuthUnlinkedPayload, type PendingDeletionInfo, type Permission, type PermissionEntity, PermissionsRepository, PurgeStrategy, type PurgeUserResult, type RegisterChannel, type RequestAccountDeletionParams, type RequestAccountDeletionResult, type Role, type RoleEntity, type RoleGuardOptions, type RolePermission, RolePermissionsRepository, RolesRepository, type SessionPayload, SocialAccountsRepository, SocialProvider, type SweepDuePurgesResult, type TokenPayload, type UpdateProfileParams, type User, type UserPermission, UserPermissionsRepository, type UserProfile, UserProfilesRepository, type UserPublicKey, type UserSocialAccount, UsersRepository, type VerificationCode, VerificationCodesRepository, VerificationPurpose, type VerifyIdTokenParams, acceptInvitation, accountDeletionRequests, accountDeletionRequestsRepository, addPermissionToRole, appleProvider, assertCanAssignRole, authDeletionCancelledEvent, authDeletionCompletedEvent, authDeletionRequestedEvent, authJobRouter, authLogger, authLoginEvent, authMetadata, authMetadataRepository, authRegisterEvent, authSchema, cancelAccountDeletionService, cancelInvitation, checkUsernameAvailableService, configureAuth, configureDeletion, configureOAuthTokenCipher, createAuthDeletionJobRouter, createAuthDeletionPurgeJob, createAuthLifecycle, createInvitation, createOAuthState, createRole, decodeToken, decryptToken, deleteInvitation, deleteRole, encryptToken, exchangeCodeForTokens, expireOldInvitations, generateClientToken, generateKeyPair, generateKeyPairES256, generateKeyPairRS256, generateOAuthNonce, generateToken, getAllRoles, getAuth, getAuthConfig, getAuthSessionService, getDeletionConfig, getDummyPasswordHash, getGoogleAuthUrl, getGoogleOAuthConfig, getGoogleUserInfo, getInvitationByToken, getInvitationWithDetails, getKeyId, getKeySize, getLocale, getOneTimeTokenManager, getOptionalAuth, getPendingDeletionInfo, getRole, getRoleByName, getRolePermissions, getSessionTtl, getUser, getUserByEmailService, getUserByIdService, getUserByPhoneService, getUserId, getUserPermissions, getUserProfileService, getUserRole, githubProvider, googleProvider, hasAllPermissions, hasAnyPermission, hasAnyRole, hasPermission, hasRole, hashPassword, initOneTimeTokenManager, initializeAuth, invitationAcceptedEvent, invitationCreatedEvent, invitationsRepository, isEncrypted, isGoogleOAuthEnabled, kakaoProvider, keysRepository, listInvitations, matchOAuthCsrfCookies, naverProvider, oauthUnlinkedEvent, oneTimeTokenAuth, parseDuration, permissions, permissionsRepository, purgeUserService, refreshAccessToken, removePermissionFromRole, requestAccountDeletionService, requireAnyPermission, requirePermissions, requireRole, resendInvitation, roleGuard, rolePermissions, rolePermissionsRepository, roles, rolesRepository, runBeforeRegister, setRolePermissions, shouldRotateKey, socialAccountsRepository, sweepDuePurges, updateLastLoginService, updateLocaleService, updateRole, updateUserProfileService, updateUserService, updateUsernameService, userInvitations, userPermissions, userPermissionsRepository, userProfiles, userProfilesRepository, userPublicKeys, userSocialAccounts, users, usersRepository, validateInvitation, validatePasswordStrength, verificationCodes, verificationCodesRepository, verifyClientToken, verifyIdToken, verifyKeyFingerprint, verifyOAuthState, verifyPassword, verifyToken };
|
package/dist/server.js
CHANGED
|
@@ -6937,6 +6937,14 @@ var init_schema5 = __esm({
|
|
|
6937
6937
|
examples: ["your-kakao-client-secret"]
|
|
6938
6938
|
})
|
|
6939
6939
|
},
|
|
6940
|
+
SPFN_AUTH_KAKAO_ADMIN_KEY: {
|
|
6941
|
+
...envString({
|
|
6942
|
+
description: "Kakao app admin key. Required to verify the User Unlinked webhook (Authorization: KakaoAK header).",
|
|
6943
|
+
required: false,
|
|
6944
|
+
sensitive: true,
|
|
6945
|
+
examples: ["your-kakao-admin-key"]
|
|
6946
|
+
})
|
|
6947
|
+
},
|
|
6940
6948
|
SPFN_AUTH_KAKAO_SCOPES: {
|
|
6941
6949
|
...envString({
|
|
6942
6950
|
description: "Comma-separated Kakao consent scopes. Defaults to account_email.",
|
|
@@ -8398,6 +8406,16 @@ var authDeletionCompletedEvent = defineEvent(
|
|
|
8398
8406
|
purgeStrategy: Type.Union([Type.Literal("anonymize"), Type.Literal("hard-delete")])
|
|
8399
8407
|
})
|
|
8400
8408
|
);
|
|
8409
|
+
var oauthUnlinkedEvent = defineEvent(
|
|
8410
|
+
"auth.oauth.unlinked",
|
|
8411
|
+
Type.Object({
|
|
8412
|
+
userId: Type.String(),
|
|
8413
|
+
provider: AuthProviderSchema,
|
|
8414
|
+
providerUserId: Type.String(),
|
|
8415
|
+
/** provider가 전달한 해제 경로 (kakao referrer_type 등) */
|
|
8416
|
+
reason: Type.Optional(Type.String())
|
|
8417
|
+
})
|
|
8418
|
+
);
|
|
8401
8419
|
|
|
8402
8420
|
// src/server/services/account-deletion.service.ts
|
|
8403
8421
|
var POSTGRES_UNIQUE_VIOLATION = "23505";
|
|
@@ -9521,6 +9539,14 @@ async function verifyOAuthState(encryptedState) {
|
|
|
9521
9539
|
}
|
|
9522
9540
|
|
|
9523
9541
|
// src/server/lib/oauth/provider.ts
|
|
9542
|
+
var UnlinkNotifyRejection = class extends Error {
|
|
9543
|
+
status;
|
|
9544
|
+
constructor(status, message) {
|
|
9545
|
+
super(message);
|
|
9546
|
+
this.name = "UnlinkNotifyRejection";
|
|
9547
|
+
this.status = status;
|
|
9548
|
+
}
|
|
9549
|
+
};
|
|
9524
9550
|
var registry2 = /* @__PURE__ */ new Map();
|
|
9525
9551
|
function registerOAuthProvider(provider) {
|
|
9526
9552
|
registry2.set(provider.id, provider);
|
|
@@ -9835,6 +9861,7 @@ registerOAuthProvider(githubProvider);
|
|
|
9835
9861
|
// src/server/lib/oauth/kakao-provider.ts
|
|
9836
9862
|
init_config();
|
|
9837
9863
|
import { ValidationError as ValidationError7 } from "@spfn/core/errors";
|
|
9864
|
+
import { timingSafeEqual } from "crypto";
|
|
9838
9865
|
var KAKAO_AUTH_URL = "https://kauth.kakao.com/oauth/authorize";
|
|
9839
9866
|
var KAKAO_TOKEN_URL = "https://kauth.kakao.com/oauth/token";
|
|
9840
9867
|
var KAKAO_USERINFO_URL = "https://kapi.kakao.com/v2/user/me";
|
|
@@ -9938,6 +9965,32 @@ var kakaoProvider = {
|
|
|
9938
9965
|
params.set("client_secret", config2.clientSecret);
|
|
9939
9966
|
}
|
|
9940
9967
|
return requestKakaoTokens(params);
|
|
9968
|
+
},
|
|
9969
|
+
/**
|
|
9970
|
+
* 카카오 연결 해제 웹훅(User Unlinked) 검증
|
|
9971
|
+
*
|
|
9972
|
+
* 카카오는 `Authorization: KakaoAK ${대표 어드민 키}` 헤더로 요청하므로
|
|
9973
|
+
* 어드민 키 일치가 곧 발신자 검증이다. 웹훅 규격상 GET/POST 모두 가능하며
|
|
9974
|
+
* 본문 필드는 app_id · user_id · referrer_type.
|
|
9975
|
+
*/
|
|
9976
|
+
async verifyUnlinkNotification(request) {
|
|
9977
|
+
const adminKey = env3.SPFN_AUTH_KAKAO_ADMIN_KEY;
|
|
9978
|
+
if (!adminKey) {
|
|
9979
|
+
throw new UnlinkNotifyRejection(401, "SPFN_AUTH_KAKAO_ADMIN_KEY is not configured");
|
|
9980
|
+
}
|
|
9981
|
+
const expected = Buffer.from(`KakaoAK ${adminKey}`);
|
|
9982
|
+
const actual = Buffer.from(request.authorization ?? "");
|
|
9983
|
+
if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
|
|
9984
|
+
throw new UnlinkNotifyRejection(401, "Kakao admin key mismatch");
|
|
9985
|
+
}
|
|
9986
|
+
const userId = request.fields.user_id;
|
|
9987
|
+
if (!userId) {
|
|
9988
|
+
throw new UnlinkNotifyRejection(400, "Kakao unlink webhook is missing user_id");
|
|
9989
|
+
}
|
|
9990
|
+
return {
|
|
9991
|
+
providerUserId: userId,
|
|
9992
|
+
reason: request.fields.referrer_type
|
|
9993
|
+
};
|
|
9941
9994
|
}
|
|
9942
9995
|
};
|
|
9943
9996
|
registerOAuthProvider(kakaoProvider);
|
|
@@ -9945,6 +9998,7 @@ registerOAuthProvider(kakaoProvider);
|
|
|
9945
9998
|
// src/server/lib/oauth/naver-provider.ts
|
|
9946
9999
|
init_config();
|
|
9947
10000
|
import { ValidationError as ValidationError8 } from "@spfn/core/errors";
|
|
10001
|
+
import { createDecipheriv, createHash as createHash3, createHmac, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
9948
10002
|
var NAVER_AUTH_URL = "https://nid.naver.com/oauth2.0/authorize";
|
|
9949
10003
|
var NAVER_TOKEN_URL = "https://nid.naver.com/oauth2.0/token";
|
|
9950
10004
|
var NAVER_USERINFO_URL = "https://openapi.naver.com/v1/nid/me";
|
|
@@ -9983,6 +10037,25 @@ async function requestNaverTokens(params) {
|
|
|
9983
10037
|
expiresIn
|
|
9984
10038
|
};
|
|
9985
10039
|
}
|
|
10040
|
+
function deriveNaverUnlinkKey(clientSecret) {
|
|
10041
|
+
return createHash3("md5").update(clientSecret).digest().subarray(0, 16);
|
|
10042
|
+
}
|
|
10043
|
+
function decodeBase64Url(value) {
|
|
10044
|
+
return Buffer.from(value.replace(/-/g, "+").replace(/_/g, "/"), "base64");
|
|
10045
|
+
}
|
|
10046
|
+
function decryptNaverUniqueId(encryptUniqueId, key) {
|
|
10047
|
+
try {
|
|
10048
|
+
const payload = decodeBase64Url(encryptUniqueId);
|
|
10049
|
+
const decipher = createDecipheriv("aes-128-cbc", key, payload.subarray(0, 16));
|
|
10050
|
+
const uniqueId = Buffer.concat([
|
|
10051
|
+
decipher.update(payload.subarray(16)),
|
|
10052
|
+
decipher.final()
|
|
10053
|
+
]).toString("utf8");
|
|
10054
|
+
return uniqueId || null;
|
|
10055
|
+
} catch {
|
|
10056
|
+
return null;
|
|
10057
|
+
}
|
|
10058
|
+
}
|
|
9986
10059
|
var naverProvider = {
|
|
9987
10060
|
id: "naver",
|
|
9988
10061
|
isEnabled() {
|
|
@@ -10024,8 +10097,9 @@ var naverProvider = {
|
|
|
10024
10097
|
return {
|
|
10025
10098
|
providerUserId: profile.id,
|
|
10026
10099
|
email: typeof profile.email === "string" ? profile.email : null,
|
|
10027
|
-
//
|
|
10028
|
-
|
|
10100
|
+
// 네이버 프로필 이메일은 네이버 계정 이메일이거나 인증 절차를 거친 연락처
|
|
10101
|
+
// 이메일이다 — 존재하면 검증된 것으로 취급한다(카카오와 같은 신뢰 수준).
|
|
10102
|
+
emailVerified: typeof profile.email === "string",
|
|
10029
10103
|
name: typeof profile.name === "string" ? profile.name : typeof profile.nickname === "string" ? profile.nickname : void 0,
|
|
10030
10104
|
avatar: typeof profile.profile_image === "string" ? profile.profile_image : void 0
|
|
10031
10105
|
};
|
|
@@ -10038,6 +10112,46 @@ var naverProvider = {
|
|
|
10038
10112
|
client_secret: config2.clientSecret,
|
|
10039
10113
|
refresh_token: refreshToken
|
|
10040
10114
|
}));
|
|
10115
|
+
},
|
|
10116
|
+
// 네이버 연결 끊기 알림 규격은 성공 응답으로 204 No Content를 요구한다.
|
|
10117
|
+
unlinkNotifyAckStatus: 204,
|
|
10118
|
+
/**
|
|
10119
|
+
* 네이버 연결 끊기 알림 검증
|
|
10120
|
+
*
|
|
10121
|
+
* 파라미터: clientId · encryptUniqueId · timestamp · signature.
|
|
10122
|
+
* signature = base64url( HMAC-SHA256( "clientId=..&encryptUniqueId=..×tamp=..", key ) ),
|
|
10123
|
+
* encryptUniqueId = base64url( iv(16) + AES-128-CBC-PKCS5(uniqueId, key) ),
|
|
10124
|
+
* key = md5(CLIENT_SECRET)[0..16].
|
|
10125
|
+
*
|
|
10126
|
+
* 복호화된 uniqueId는 프로필 API(/v1/nid/me)의 id와 동일한 이용자 고유 식별자다.
|
|
10127
|
+
* 네이버는 실패 요청을 재시도하지 않으므로 재전송(replay) 방어는 검증 통과 후
|
|
10128
|
+
* 삭제가 멱등이라는 점에 의존한다.
|
|
10129
|
+
*/
|
|
10130
|
+
async verifyUnlinkNotification(request) {
|
|
10131
|
+
const clientId = env3.SPFN_AUTH_NAVER_CLIENT_ID;
|
|
10132
|
+
const clientSecret = env3.SPFN_AUTH_NAVER_CLIENT_SECRET;
|
|
10133
|
+
if (!clientId || !clientSecret) {
|
|
10134
|
+
throw new UnlinkNotifyRejection(403, "Naver OAuth is not configured");
|
|
10135
|
+
}
|
|
10136
|
+
const { clientId: requestClientId, encryptUniqueId, timestamp: timestamp2, signature } = request.fields;
|
|
10137
|
+
if (!requestClientId || !encryptUniqueId || !timestamp2 || !signature) {
|
|
10138
|
+
throw new UnlinkNotifyRejection(400, "Naver unlink notification is missing required parameters");
|
|
10139
|
+
}
|
|
10140
|
+
if (requestClientId !== clientId) {
|
|
10141
|
+
throw new UnlinkNotifyRejection(403, "Naver unlink notification clientId mismatch");
|
|
10142
|
+
}
|
|
10143
|
+
const key = deriveNaverUnlinkKey(clientSecret);
|
|
10144
|
+
const baseString = `clientId=${requestClientId}&encryptUniqueId=${encryptUniqueId}×tamp=${timestamp2}`;
|
|
10145
|
+
const expected = createHmac("sha256", key).update(baseString).digest();
|
|
10146
|
+
const actual = decodeBase64Url(signature);
|
|
10147
|
+
if (expected.length !== actual.length || !timingSafeEqual2(expected, actual)) {
|
|
10148
|
+
throw new UnlinkNotifyRejection(403, "Naver unlink notification signature mismatch");
|
|
10149
|
+
}
|
|
10150
|
+
const uniqueId = decryptNaverUniqueId(encryptUniqueId, key);
|
|
10151
|
+
if (!uniqueId) {
|
|
10152
|
+
throw new UnlinkNotifyRejection(400, "Naver encryptUniqueId cannot be decrypted");
|
|
10153
|
+
}
|
|
10154
|
+
return { providerUserId: uniqueId, reason: "NAVER_UNLINK" };
|
|
10041
10155
|
}
|
|
10042
10156
|
};
|
|
10043
10157
|
registerOAuthProvider(naverProvider);
|
|
@@ -10110,6 +10224,7 @@ async function oauthCallbackService(params) {
|
|
|
10110
10224
|
refreshToken: tokens.refreshToken ?? existingSocialAccount.refreshToken,
|
|
10111
10225
|
tokenExpiresAt: tokenExpiryDate(tokens.expiresIn)
|
|
10112
10226
|
});
|
|
10227
|
+
await backfillVerifiedEmail(userId, identity);
|
|
10113
10228
|
} else {
|
|
10114
10229
|
const result = await createOrLinkUser(provider, identity, tokens, stateData.metadata);
|
|
10115
10230
|
userId = result.userId;
|
|
@@ -10167,6 +10282,30 @@ async function assertActiveForOAuthSession(userId) {
|
|
|
10167
10282
|
}
|
|
10168
10283
|
throw new AccountDisabledError2({ status: user.status });
|
|
10169
10284
|
}
|
|
10285
|
+
async function backfillVerifiedEmail(userId, identity) {
|
|
10286
|
+
if (!identity.email || !identity.emailVerified) {
|
|
10287
|
+
return;
|
|
10288
|
+
}
|
|
10289
|
+
const user = await usersRepository.findById(userId);
|
|
10290
|
+
if (!user || user.email) {
|
|
10291
|
+
return;
|
|
10292
|
+
}
|
|
10293
|
+
const emailOwner = await usersRepository.findByEmail(identity.email);
|
|
10294
|
+
if (emailOwner && emailOwner.id !== userId) {
|
|
10295
|
+
return;
|
|
10296
|
+
}
|
|
10297
|
+
try {
|
|
10298
|
+
await usersRepository.updateById(userId, {
|
|
10299
|
+
email: identity.email,
|
|
10300
|
+
emailVerifiedAt: /* @__PURE__ */ new Date()
|
|
10301
|
+
});
|
|
10302
|
+
} catch (error) {
|
|
10303
|
+
authLogger.service.warn("Verified-email backfill failed; continuing login", {
|
|
10304
|
+
userId: String(userId),
|
|
10305
|
+
error: error instanceof Error ? error.message : String(error)
|
|
10306
|
+
});
|
|
10307
|
+
}
|
|
10308
|
+
}
|
|
10170
10309
|
async function createOrLinkUser(provider, identity, tokens, metadata) {
|
|
10171
10310
|
const existingUser = identity.email ? await usersRepository.findByEmail(identity.email) : null;
|
|
10172
10311
|
let userId;
|
|
@@ -10265,6 +10404,25 @@ async function getGoogleAccessToken(userId) {
|
|
|
10265
10404
|
});
|
|
10266
10405
|
return tokens.access_token;
|
|
10267
10406
|
}
|
|
10407
|
+
async function oauthUnlinkNotifyService(provider, notification) {
|
|
10408
|
+
const oauthProvider = requireEnabledProvider(provider);
|
|
10409
|
+
const ackStatus = oauthProvider.unlinkNotifyAckStatus ?? 200;
|
|
10410
|
+
const account = await socialAccountsRepository.findByProviderAndProviderId(
|
|
10411
|
+
provider,
|
|
10412
|
+
notification.providerUserId
|
|
10413
|
+
);
|
|
10414
|
+
if (!account) {
|
|
10415
|
+
return { ackStatus, handled: false };
|
|
10416
|
+
}
|
|
10417
|
+
await socialAccountsRepository.deleteById(account.id);
|
|
10418
|
+
await oauthUnlinkedEvent.emit({
|
|
10419
|
+
userId: String(account.userId),
|
|
10420
|
+
provider,
|
|
10421
|
+
providerUserId: notification.providerUserId,
|
|
10422
|
+
reason: notification.reason
|
|
10423
|
+
});
|
|
10424
|
+
return { ackStatus, handled: true };
|
|
10425
|
+
}
|
|
10268
10426
|
|
|
10269
10427
|
// src/server/services/oauth-native.service.ts
|
|
10270
10428
|
init_repositories();
|
|
@@ -10293,6 +10451,7 @@ async function persistNativeLogin(identity, params) {
|
|
|
10293
10451
|
let isNewUser = false;
|
|
10294
10452
|
if (existing) {
|
|
10295
10453
|
userId = existing.userId;
|
|
10454
|
+
await backfillVerifiedEmail(userId, identity);
|
|
10296
10455
|
} else {
|
|
10297
10456
|
const result = await createOrLinkUser(params.provider, identity, void 0, params.metadata);
|
|
10298
10457
|
userId = result.userId;
|
|
@@ -11558,6 +11717,56 @@ var oauthNative = route4.post("/_auth/oauth/:provider/native").input({
|
|
|
11558
11717
|
const { params, body } = await c.data();
|
|
11559
11718
|
return await oauthNativeService({ provider: params.provider, ...body });
|
|
11560
11719
|
});
|
|
11720
|
+
async function collectUnlinkNotifyFields(raw) {
|
|
11721
|
+
const fields = { ...raw.req.query() };
|
|
11722
|
+
if (raw.req.method === "GET") {
|
|
11723
|
+
return fields;
|
|
11724
|
+
}
|
|
11725
|
+
const contentType = raw.req.header("content-type") ?? "";
|
|
11726
|
+
if (contentType.includes("application/json")) {
|
|
11727
|
+
const body2 = await raw.req.json().catch(() => null);
|
|
11728
|
+
for (const [key, value] of Object.entries(body2 ?? {})) {
|
|
11729
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
11730
|
+
fields[key] = String(value);
|
|
11731
|
+
}
|
|
11732
|
+
}
|
|
11733
|
+
return fields;
|
|
11734
|
+
}
|
|
11735
|
+
const body = await raw.req.parseBody().catch(() => ({}));
|
|
11736
|
+
for (const [key, value] of Object.entries(body)) {
|
|
11737
|
+
if (typeof value === "string") {
|
|
11738
|
+
fields[key] = value;
|
|
11739
|
+
}
|
|
11740
|
+
}
|
|
11741
|
+
return fields;
|
|
11742
|
+
}
|
|
11743
|
+
async function processUnlinkNotify(provider, raw) {
|
|
11744
|
+
const oauthProvider = getOAuthProvider(provider);
|
|
11745
|
+
if (!oauthProvider?.isEnabled() || !oauthProvider.verifyUnlinkNotification) {
|
|
11746
|
+
return 404;
|
|
11747
|
+
}
|
|
11748
|
+
const request = {
|
|
11749
|
+
authorization: raw.req.header("authorization") ?? null,
|
|
11750
|
+
fields: await collectUnlinkNotifyFields(raw)
|
|
11751
|
+
};
|
|
11752
|
+
try {
|
|
11753
|
+
const notification = await oauthProvider.verifyUnlinkNotification(request);
|
|
11754
|
+
const result = await oauthUnlinkNotifyService(provider, notification);
|
|
11755
|
+
return result.ackStatus;
|
|
11756
|
+
} catch (error) {
|
|
11757
|
+
return error instanceof UnlinkNotifyRejection ? error.status : 400;
|
|
11758
|
+
}
|
|
11759
|
+
}
|
|
11760
|
+
var oauthUnlinkNotify = route4.post("/_auth/oauth/:provider/unlink-notify").input({ params: providerParams }).use([rateLimitPolicy4("oauth-unlink-notify", { limit: 300, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
11761
|
+
const { params } = await c.data();
|
|
11762
|
+
const status = await processUnlinkNotify(params.provider, c.raw);
|
|
11763
|
+
return status === 204 ? c.noContent() : c.json({}, status);
|
|
11764
|
+
});
|
|
11765
|
+
var oauthUnlinkNotifyGet = route4.get("/_auth/oauth/:provider/unlink-notify").input({ params: providerParams }).use([rateLimitPolicy4("oauth-unlink-notify", { limit: 300, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
11766
|
+
const { params } = await c.data();
|
|
11767
|
+
const status = await processUnlinkNotify(params.provider, c.raw);
|
|
11768
|
+
return status === 204 ? c.noContent() : c.json({}, status);
|
|
11769
|
+
});
|
|
11561
11770
|
var oauthRouter = defineRouter4({
|
|
11562
11771
|
oauthGoogleStart,
|
|
11563
11772
|
oauthGoogleCallback,
|
|
@@ -11568,7 +11777,9 @@ var oauthRouter = defineRouter4({
|
|
|
11568
11777
|
oauthProviderStart,
|
|
11569
11778
|
oauthProviderCallback,
|
|
11570
11779
|
getProviderOAuthUrl,
|
|
11571
|
-
oauthNative
|
|
11780
|
+
oauthNative,
|
|
11781
|
+
oauthUnlinkNotify,
|
|
11782
|
+
oauthUnlinkNotifyGet
|
|
11572
11783
|
});
|
|
11573
11784
|
|
|
11574
11785
|
// src/server/routes/admin/index.ts
|
|
@@ -11729,6 +11940,8 @@ var mainAuthRouter = defineRouter6({
|
|
|
11729
11940
|
oauthProviderCallback,
|
|
11730
11941
|
getProviderOAuthUrl,
|
|
11731
11942
|
oauthNative,
|
|
11943
|
+
oauthUnlinkNotify,
|
|
11944
|
+
oauthUnlinkNotifyGet,
|
|
11732
11945
|
// Invitation routes
|
|
11733
11946
|
getInvitation,
|
|
11734
11947
|
acceptInvitation: acceptInvitation2,
|
|
@@ -12130,6 +12343,7 @@ export {
|
|
|
12130
12343
|
SocialAccountsRepository,
|
|
12131
12344
|
TargetTypeSchema,
|
|
12132
12345
|
USER_STATUSES,
|
|
12346
|
+
UnlinkNotifyRejection,
|
|
12133
12347
|
UserPermissionsRepository,
|
|
12134
12348
|
UserProfilesRepository,
|
|
12135
12349
|
UsersRepository,
|
|
@@ -12243,6 +12457,8 @@ export {
|
|
|
12243
12457
|
oauthCallbackService,
|
|
12244
12458
|
oauthNativeService,
|
|
12245
12459
|
oauthStartService,
|
|
12460
|
+
oauthUnlinkNotifyService,
|
|
12461
|
+
oauthUnlinkedEvent,
|
|
12246
12462
|
oneTimeTokenAuth,
|
|
12247
12463
|
optionalAuth,
|
|
12248
12464
|
parseDuration,
|