@spfn/auth 0.2.0-beta.82 → 0.2.0-beta.83
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 +43 -1
- 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 +190 -1
- 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() {
|
|
@@ -10038,6 +10111,46 @@ var naverProvider = {
|
|
|
10038
10111
|
client_secret: config2.clientSecret,
|
|
10039
10112
|
refresh_token: refreshToken
|
|
10040
10113
|
}));
|
|
10114
|
+
},
|
|
10115
|
+
// 네이버 연결 끊기 알림 규격은 성공 응답으로 204 No Content를 요구한다.
|
|
10116
|
+
unlinkNotifyAckStatus: 204,
|
|
10117
|
+
/**
|
|
10118
|
+
* 네이버 연결 끊기 알림 검증
|
|
10119
|
+
*
|
|
10120
|
+
* 파라미터: clientId · encryptUniqueId · timestamp · signature.
|
|
10121
|
+
* signature = base64url( HMAC-SHA256( "clientId=..&encryptUniqueId=..×tamp=..", key ) ),
|
|
10122
|
+
* encryptUniqueId = base64url( iv(16) + AES-128-CBC-PKCS5(uniqueId, key) ),
|
|
10123
|
+
* key = md5(CLIENT_SECRET)[0..16].
|
|
10124
|
+
*
|
|
10125
|
+
* 복호화된 uniqueId는 프로필 API(/v1/nid/me)의 id와 동일한 이용자 고유 식별자다.
|
|
10126
|
+
* 네이버는 실패 요청을 재시도하지 않으므로 재전송(replay) 방어는 검증 통과 후
|
|
10127
|
+
* 삭제가 멱등이라는 점에 의존한다.
|
|
10128
|
+
*/
|
|
10129
|
+
async verifyUnlinkNotification(request) {
|
|
10130
|
+
const clientId = env3.SPFN_AUTH_NAVER_CLIENT_ID;
|
|
10131
|
+
const clientSecret = env3.SPFN_AUTH_NAVER_CLIENT_SECRET;
|
|
10132
|
+
if (!clientId || !clientSecret) {
|
|
10133
|
+
throw new UnlinkNotifyRejection(403, "Naver OAuth is not configured");
|
|
10134
|
+
}
|
|
10135
|
+
const { clientId: requestClientId, encryptUniqueId, timestamp: timestamp2, signature } = request.fields;
|
|
10136
|
+
if (!requestClientId || !encryptUniqueId || !timestamp2 || !signature) {
|
|
10137
|
+
throw new UnlinkNotifyRejection(400, "Naver unlink notification is missing required parameters");
|
|
10138
|
+
}
|
|
10139
|
+
if (requestClientId !== clientId) {
|
|
10140
|
+
throw new UnlinkNotifyRejection(403, "Naver unlink notification clientId mismatch");
|
|
10141
|
+
}
|
|
10142
|
+
const key = deriveNaverUnlinkKey(clientSecret);
|
|
10143
|
+
const baseString = `clientId=${requestClientId}&encryptUniqueId=${encryptUniqueId}×tamp=${timestamp2}`;
|
|
10144
|
+
const expected = createHmac("sha256", key).update(baseString).digest();
|
|
10145
|
+
const actual = decodeBase64Url(signature);
|
|
10146
|
+
if (expected.length !== actual.length || !timingSafeEqual2(expected, actual)) {
|
|
10147
|
+
throw new UnlinkNotifyRejection(403, "Naver unlink notification signature mismatch");
|
|
10148
|
+
}
|
|
10149
|
+
const uniqueId = decryptNaverUniqueId(encryptUniqueId, key);
|
|
10150
|
+
if (!uniqueId) {
|
|
10151
|
+
throw new UnlinkNotifyRejection(400, "Naver encryptUniqueId cannot be decrypted");
|
|
10152
|
+
}
|
|
10153
|
+
return { providerUserId: uniqueId, reason: "NAVER_UNLINK" };
|
|
10041
10154
|
}
|
|
10042
10155
|
};
|
|
10043
10156
|
registerOAuthProvider(naverProvider);
|
|
@@ -10265,6 +10378,25 @@ async function getGoogleAccessToken(userId) {
|
|
|
10265
10378
|
});
|
|
10266
10379
|
return tokens.access_token;
|
|
10267
10380
|
}
|
|
10381
|
+
async function oauthUnlinkNotifyService(provider, notification) {
|
|
10382
|
+
const oauthProvider = requireEnabledProvider(provider);
|
|
10383
|
+
const ackStatus = oauthProvider.unlinkNotifyAckStatus ?? 200;
|
|
10384
|
+
const account = await socialAccountsRepository.findByProviderAndProviderId(
|
|
10385
|
+
provider,
|
|
10386
|
+
notification.providerUserId
|
|
10387
|
+
);
|
|
10388
|
+
if (!account) {
|
|
10389
|
+
return { ackStatus, handled: false };
|
|
10390
|
+
}
|
|
10391
|
+
await socialAccountsRepository.deleteById(account.id);
|
|
10392
|
+
await oauthUnlinkedEvent.emit({
|
|
10393
|
+
userId: String(account.userId),
|
|
10394
|
+
provider,
|
|
10395
|
+
providerUserId: notification.providerUserId,
|
|
10396
|
+
reason: notification.reason
|
|
10397
|
+
});
|
|
10398
|
+
return { ackStatus, handled: true };
|
|
10399
|
+
}
|
|
10268
10400
|
|
|
10269
10401
|
// src/server/services/oauth-native.service.ts
|
|
10270
10402
|
init_repositories();
|
|
@@ -11558,6 +11690,56 @@ var oauthNative = route4.post("/_auth/oauth/:provider/native").input({
|
|
|
11558
11690
|
const { params, body } = await c.data();
|
|
11559
11691
|
return await oauthNativeService({ provider: params.provider, ...body });
|
|
11560
11692
|
});
|
|
11693
|
+
async function collectUnlinkNotifyFields(raw) {
|
|
11694
|
+
const fields = { ...raw.req.query() };
|
|
11695
|
+
if (raw.req.method === "GET") {
|
|
11696
|
+
return fields;
|
|
11697
|
+
}
|
|
11698
|
+
const contentType = raw.req.header("content-type") ?? "";
|
|
11699
|
+
if (contentType.includes("application/json")) {
|
|
11700
|
+
const body2 = await raw.req.json().catch(() => null);
|
|
11701
|
+
for (const [key, value] of Object.entries(body2 ?? {})) {
|
|
11702
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
11703
|
+
fields[key] = String(value);
|
|
11704
|
+
}
|
|
11705
|
+
}
|
|
11706
|
+
return fields;
|
|
11707
|
+
}
|
|
11708
|
+
const body = await raw.req.parseBody().catch(() => ({}));
|
|
11709
|
+
for (const [key, value] of Object.entries(body)) {
|
|
11710
|
+
if (typeof value === "string") {
|
|
11711
|
+
fields[key] = value;
|
|
11712
|
+
}
|
|
11713
|
+
}
|
|
11714
|
+
return fields;
|
|
11715
|
+
}
|
|
11716
|
+
async function processUnlinkNotify(provider, raw) {
|
|
11717
|
+
const oauthProvider = getOAuthProvider(provider);
|
|
11718
|
+
if (!oauthProvider?.isEnabled() || !oauthProvider.verifyUnlinkNotification) {
|
|
11719
|
+
return 404;
|
|
11720
|
+
}
|
|
11721
|
+
const request = {
|
|
11722
|
+
authorization: raw.req.header("authorization") ?? null,
|
|
11723
|
+
fields: await collectUnlinkNotifyFields(raw)
|
|
11724
|
+
};
|
|
11725
|
+
try {
|
|
11726
|
+
const notification = await oauthProvider.verifyUnlinkNotification(request);
|
|
11727
|
+
const result = await oauthUnlinkNotifyService(provider, notification);
|
|
11728
|
+
return result.ackStatus;
|
|
11729
|
+
} catch (error) {
|
|
11730
|
+
return error instanceof UnlinkNotifyRejection ? error.status : 400;
|
|
11731
|
+
}
|
|
11732
|
+
}
|
|
11733
|
+
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) => {
|
|
11734
|
+
const { params } = await c.data();
|
|
11735
|
+
const status = await processUnlinkNotify(params.provider, c.raw);
|
|
11736
|
+
return status === 204 ? c.noContent() : c.json({}, status);
|
|
11737
|
+
});
|
|
11738
|
+
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) => {
|
|
11739
|
+
const { params } = await c.data();
|
|
11740
|
+
const status = await processUnlinkNotify(params.provider, c.raw);
|
|
11741
|
+
return status === 204 ? c.noContent() : c.json({}, status);
|
|
11742
|
+
});
|
|
11561
11743
|
var oauthRouter = defineRouter4({
|
|
11562
11744
|
oauthGoogleStart,
|
|
11563
11745
|
oauthGoogleCallback,
|
|
@@ -11568,7 +11750,9 @@ var oauthRouter = defineRouter4({
|
|
|
11568
11750
|
oauthProviderStart,
|
|
11569
11751
|
oauthProviderCallback,
|
|
11570
11752
|
getProviderOAuthUrl,
|
|
11571
|
-
oauthNative
|
|
11753
|
+
oauthNative,
|
|
11754
|
+
oauthUnlinkNotify,
|
|
11755
|
+
oauthUnlinkNotifyGet
|
|
11572
11756
|
});
|
|
11573
11757
|
|
|
11574
11758
|
// src/server/routes/admin/index.ts
|
|
@@ -11729,6 +11913,8 @@ var mainAuthRouter = defineRouter6({
|
|
|
11729
11913
|
oauthProviderCallback,
|
|
11730
11914
|
getProviderOAuthUrl,
|
|
11731
11915
|
oauthNative,
|
|
11916
|
+
oauthUnlinkNotify,
|
|
11917
|
+
oauthUnlinkNotifyGet,
|
|
11732
11918
|
// Invitation routes
|
|
11733
11919
|
getInvitation,
|
|
11734
11920
|
acceptInvitation: acceptInvitation2,
|
|
@@ -12130,6 +12316,7 @@ export {
|
|
|
12130
12316
|
SocialAccountsRepository,
|
|
12131
12317
|
TargetTypeSchema,
|
|
12132
12318
|
USER_STATUSES,
|
|
12319
|
+
UnlinkNotifyRejection,
|
|
12133
12320
|
UserPermissionsRepository,
|
|
12134
12321
|
UserProfilesRepository,
|
|
12135
12322
|
UsersRepository,
|
|
@@ -12243,6 +12430,8 @@ export {
|
|
|
12243
12430
|
oauthCallbackService,
|
|
12244
12431
|
oauthNativeService,
|
|
12245
12432
|
oauthStartService,
|
|
12433
|
+
oauthUnlinkNotifyService,
|
|
12434
|
+
oauthUnlinkedEvent,
|
|
12246
12435
|
oneTimeTokenAuth,
|
|
12247
12436
|
optionalAuth,
|
|
12248
12437
|
parseDuration,
|