@thinkingcat/auth-utils 1.0.14 → 1.0.17

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 CHANGED
@@ -6,6 +6,7 @@ ThinkingCat SSO 서비스를 위한 인증 유틸리티 패키지입니다. JWT
6
6
 
7
7
  - [📦 설치 (Installation)](#-설치-installation)
8
8
  - [📋 요구사항 (Requirements)](#-요구사항-requirements)
9
+ - [🐛 디버깅 (Debugging)](#-디버깅-debugging)
9
10
  - [⚙️ Next.js 설정 (Next.js Configuration)](#️-nextjs-설정-nextjs-configuration)
10
11
  - [🚀 빠른 시작 (Quick Start)](#-빠른-시작-quick-start)
11
12
  - [📚 주요 기능 (Features)](#-주요-기능-features)
@@ -47,6 +48,36 @@ pnpm add @thinkingcat/auth-utils
47
48
  - **Node.js**: >= 18.0.0
48
49
  - **TypeScript**: 권장 (타입 지원)
49
50
 
51
+ ## 🐛 디버깅 (Debugging)
52
+
53
+ 이 패키지는 조건부 로깅 시스템을 사용합니다. 기본적으로 프로덕션 환경에서는 로그가 출력되지 않으며, 개발 환경에서만 로그가 출력됩니다.
54
+
55
+ ### 디버그 로그 활성화
56
+
57
+ 환경 변수 `AUTH_UTILS_DEBUG=true`를 설정하여 모든 환경에서 디버그 로그를 활성화할 수 있습니다:
58
+
59
+ ```bash
60
+ # .env.local 또는 환경 변수 설정
61
+ AUTH_UTILS_DEBUG=true
62
+ ```
63
+
64
+ ### 로그 출력 조건
65
+
66
+ - `NODE_ENV === 'development'`: 자동으로 로그 출력
67
+ - `AUTH_UTILS_DEBUG === 'true'`: 모든 환경에서 로그 출력
68
+ - 그 외: 로그 출력 안 함 (성능 최적화)
69
+
70
+ ### 디버그 로그 예시
71
+
72
+ ```typescript
73
+ // 개발 환경 또는 AUTH_UTILS_DEBUG=true일 때만 출력됨
74
+ [handleMiddleware] Processing: /admin
75
+ [verifyAndRefreshToken] Checking refresh: { hasRefreshToken: true, forceRefresh: false }
76
+ [createAuthResponse] JWT created: { hasId: true, hasEmail: true, hasRole: true }
77
+ ```
78
+
79
+ 이 기능으로 프로덕션 환경에서 불필요한 로그 출력을 방지하여 성능을 최적화합니다.
80
+
50
81
  ## ⚙️ Next.js 설정 (Next.js Configuration)
51
82
 
52
83
  ### npm에 배포된 패키지를 사용하는 경우 (권장)
@@ -60,7 +91,7 @@ npm install @thinkingcat/auth-utils
60
91
  ```json
61
92
  {
62
93
  "dependencies": {
63
- "@thinkingcat/auth-utils": "^1.0.12"
94
+ "@thinkingcat/auth-utils": "^1.0.17"
64
95
  }
65
96
  }
66
97
  ```
@@ -140,6 +171,15 @@ import {
140
171
  handleMiddleware,
141
172
  verifyAndRefreshTokenWithNextAuth,
142
173
 
174
+ // NextAuth 설정 및 콜백
175
+ createNextAuthBaseConfig,
176
+ createNextAuthCookies,
177
+ handleJWTCallback,
178
+ createInitialJWTToken,
179
+ createEmptySession,
180
+ mapTokenToSession,
181
+ getJWTFromCustomTokenCookie,
182
+
143
183
  // 라이센스
144
184
  checkLicenseKey,
145
185
 
@@ -249,7 +289,7 @@ const role = extractRoleFromPayload(payload, "myservice", "ADMIN");
249
289
  // 'ADMIN', 'TEACHER', 'STUDENT' 등
250
290
  ```
251
291
 
252
- #### `createNextAuthJWT(payload: JWTPayload, serviceId: string, includeAcademies?: boolean)`
292
+ #### `createNextAuthJWT(payload: JWTPayload, serviceId: string)`
253
293
 
254
294
  NextAuth와 호환되는 JWT 객체를 생성합니다.
255
295
 
@@ -257,7 +297,6 @@ NextAuth와 호환되는 JWT 객체를 생성합니다.
257
297
 
258
298
  - `payload`: 원본 JWT payload (JWTPayload 타입)
259
299
  - `serviceId`: 서비스 ID (필수)
260
- - `includeAcademies`: academies 정보 포함 여부 (기본값: false)
261
300
 
262
301
  **반환값:**
263
302
 
@@ -266,8 +305,7 @@ NextAuth와 호환되는 JWT 객체를 생성합니다.
266
305
  **사용 예시:**
267
306
 
268
307
  ```typescript
269
- const jwt = createNextAuthJWT(payload, "myservice", true);
270
- // academies 정보가 포함된 JWT 객체
308
+ const jwt = createNextAuthJWT(payload, "myservice");
271
309
  ```
272
310
 
273
311
  #### `encodeNextAuthToken(jwt: JWT, secret: string, maxAge?: number)`
@@ -522,6 +560,159 @@ SSO 서버에서 토큰을 검증합니다.
522
560
 
523
561
  - `{ isValid: boolean; redirectUrl?: string }`
524
562
 
563
+ ### NextAuth 설정 및 콜백
564
+
565
+ #### `createNextAuthBaseConfig(options)`
566
+
567
+ NextAuth 기본 설정을 생성합니다.
568
+
569
+ **파라미터:**
570
+
571
+ - `options.secret`: NextAuth secret (필수)
572
+ - `options.isProduction`: 프로덕션 환경 여부 (기본값: false)
573
+ - `options.cookieDomain`: 쿠키 도메인 (선택)
574
+ - `options.signInPath`: 로그인 페이지 경로 (기본값: '/login')
575
+ - `options.errorPath`: 에러 페이지 경로 (기본값: '/login')
576
+ - `options.nextAuthUrl`: NextAuth URL (선택)
577
+ - `options.sessionMaxAge`: 세션 최대 유지 시간 (초, 기본값: 30일)
578
+ - `options.jwtMaxAge`: JWT 최대 유지 시간 (초, 기본값: 30일)
579
+
580
+ **반환값:**
581
+
582
+ - NextAuth 기본 설정 객체
583
+
584
+ **사용 예시:**
585
+
586
+ ```typescript
587
+ import { createNextAuthBaseConfig } from "@thinkingcat/auth-utils";
588
+
589
+ const baseConfig = createNextAuthBaseConfig({
590
+ secret: process.env.NEXTAUTH_SECRET!,
591
+ isProduction: process.env.NODE_ENV === "production",
592
+ cookieDomain: process.env.COOKIE_DOMAIN,
593
+ signInPath: "/login",
594
+ errorPath: "/login",
595
+ nextAuthUrl: process.env.NEXTAUTH_URL,
596
+ });
597
+
598
+ export const authOptions: NextAuthOptions = {
599
+ ...baseConfig,
600
+ // ... 기타 설정
601
+ };
602
+ ```
603
+
604
+ #### `createNextAuthCookies(options)`
605
+
606
+ NextAuth 쿠키 설정을 생성합니다.
607
+
608
+ **파라미터:**
609
+
610
+ - `options.isProduction`: 프로덕션 환경 여부 (기본값: false)
611
+ - `options.cookieDomain`: 쿠키 도메인 (선택)
612
+
613
+ **반환값:**
614
+
615
+ - NextAuth 쿠키 설정 객체
616
+
617
+ #### `handleJWTCallback(token, user?, account?, options?)`
618
+
619
+ JWT 콜백을 위한 통합 헬퍼 함수입니다. 초기 로그인, 토큰 갱신, 커스텀 토큰 읽기를 모두 처리합니다.
620
+
621
+ **파라미터:**
622
+
623
+ - `token`: 기존 JWT 토큰
624
+ - `user`: 사용자 정보 (초기 로그인 시)
625
+ - `account`: 계정 정보 (초기 로그인 시)
626
+ - `options.secret`: NextAuth secret (커스텀 토큰 읽기용)
627
+ - `options.licenseKey`: 라이센스 키 (커스텀 토큰 읽기용)
628
+ - `options.serviceId`: 서비스 ID (커스텀 토큰 읽기용)
629
+ - `options.cookieName`: 커스텀 토큰 쿠키 이름 (기본값: '{serviceId}\_access_token')
630
+ - `options.debug`: 디버깅 로그 출력 여부 (기본값: false)
631
+
632
+ **반환값:**
633
+
634
+ - 업데이트된 JWT 토큰
635
+
636
+ **사용 예시:**
637
+
638
+ ```typescript
639
+ import { handleJWTCallback } from "@thinkingcat/auth-utils";
640
+
641
+ async jwt({ token, user, account }) {
642
+ return handleJWTCallback(
643
+ token,
644
+ user ? {
645
+ id: user.id,
646
+ email: user.email,
647
+ role: user.role,
648
+ // ... 기타 사용자 정보
649
+ } : null,
650
+ account,
651
+ {
652
+ secret: process.env.NEXTAUTH_SECRET!,
653
+ licenseKey: process.env.LICENSE_KEY!,
654
+ serviceId: 'myservice',
655
+ cookieName: 'myservice_access_token',
656
+ debug: true,
657
+ }
658
+ );
659
+ }
660
+ ```
661
+
662
+ #### `createInitialJWTToken(token, user, account?)`
663
+
664
+ JWT 콜백에서 초기 로그인 시 토큰 생성 헬퍼입니다.
665
+
666
+ **파라미터:**
667
+
668
+ - `token`: 기존 토큰
669
+ - `user`: 사용자 정보
670
+ - `account`: 계정 정보 (선택)
671
+
672
+ **반환값:**
673
+
674
+ - 업데이트된 JWT 토큰
675
+
676
+ #### `createEmptySession(session)`
677
+
678
+ Session 콜백에서 빈 세션 반환 헬퍼입니다.
679
+
680
+ **파라미터:**
681
+
682
+ - `session`: 기존 세션
683
+
684
+ **반환값:**
685
+
686
+ - 빈 세션 객체
687
+
688
+ #### `mapTokenToSession(session, token)`
689
+
690
+ Session 콜백에서 토큰 정보를 세션에 매핑하는 헬퍼입니다.
691
+
692
+ **파라미터:**
693
+
694
+ - `session`: 기존 세션
695
+ - `token`: JWT 토큰
696
+
697
+ **반환값:**
698
+
699
+ - 업데이트된 세션
700
+
701
+ #### `getJWTFromCustomTokenCookie(cookieName, secret, serviceId, licenseKey)`
702
+
703
+ 쿠키에서 커스텀 토큰을 읽어서 NextAuth JWT로 변환하는 헬퍼 함수입니다.
704
+
705
+ **파라미터:**
706
+
707
+ - `cookieName`: 쿠키 이름 (예: 'checkon_access_token')
708
+ - `secret`: JWT 서명에 사용할 secret key
709
+ - `serviceId`: 서비스 ID (필수)
710
+ - `licenseKey`: 라이센스 키 (필수)
711
+
712
+ **반환값:**
713
+
714
+ - NextAuth JWT 객체 또는 null
715
+
525
716
  ### 미들웨어
526
717
 
527
718
  #### `createMiddlewareConfig(config: Partial<MiddlewareConfig> & { serviceId: string }, defaults?)`
@@ -1087,13 +1278,6 @@ interface JWTPayload {
1087
1278
  // 서비스 정보
1088
1279
  services?: ServiceInfo[];
1089
1280
 
1090
- // 학원 정보
1091
- academies?: Array<{
1092
- id: string;
1093
- name: string;
1094
- role: string;
1095
- }>;
1096
-
1097
1281
  // 인증 상태
1098
1282
  phoneVerified?: boolean;
1099
1283
  emailVerified?: boolean;
@@ -1101,8 +1285,6 @@ interface JWTPayload {
1101
1285
 
1102
1286
  // 선택적 필드
1103
1287
  phone?: string;
1104
- academyId?: string;
1105
- academyName?: string;
1106
1288
  isPasswordReset?: boolean;
1107
1289
  decryptedEmail?: string;
1108
1290
  decryptedPhone?: string;
@@ -1308,10 +1490,32 @@ const response = await handleMiddleware(req, middlewareConfig, {
1308
1490
  ## 📦 패키지 정보
1309
1491
 
1310
1492
  - **패키지명**: `@thinkingcat/auth-utils`
1311
- - **버전**: `1.0.12`
1493
+ - **버전**: `1.0.17`
1312
1494
  - **라이선스**: MIT
1313
1495
  - **저장소**: npm registry
1314
1496
 
1497
+ ## 📝 변경 이력 (Changelog)
1498
+
1499
+ ### v1.0.17 (2024-11-15)
1500
+
1501
+ **새로운 기능:**
1502
+
1503
+ - 조건부 로깅 시스템 추가 (`debugLog`, `debugError`)
1504
+ - 환경 변수 `AUTH_UTILS_DEBUG` 지원
1505
+ - 프로덕션 환경에서 로그 출력 최적화
1506
+
1507
+ **개선 사항:**
1508
+
1509
+ - 코드 구조 개선 (15개 기능별 섹션으로 그룹화)
1510
+ - 중복 코드 제거 (`deleteNextAuthSessionCookie`, `clearAllAuthCookies` 헬퍼 추가)
1511
+ - 로그 메시지 간소화 및 가독성 향상
1512
+ - 불필요한 주석 제거
1513
+
1514
+ **성능 최적화:**
1515
+
1516
+ - 프로덕션 환경에서 로그 출력 비활성화로 성능 향상
1517
+ - 조건부 로깅으로 런타임 오버헤드 감소
1518
+
1315
1519
  ## 🤝 기여 (Contributing)
1316
1520
 
1317
1521
  이슈나 개선 사항이 있으면 GitHub 이슈를 등록해주세요.
package/dist/index.d.ts CHANGED
@@ -69,17 +69,10 @@ export interface JWTPayload {
69
69
  iat?: number;
70
70
  exp?: number;
71
71
  services?: ServiceInfo[];
72
- academies?: Array<{
73
- id: string;
74
- name: string;
75
- role: string;
76
- }>;
77
72
  phoneVerified?: boolean;
78
73
  emailVerified?: boolean;
79
74
  smsVerified?: boolean;
80
75
  phone?: string;
81
- academyId?: string;
82
- academyName?: string;
83
76
  isPasswordReset?: boolean;
84
77
  decryptedEmail?: string;
85
78
  decryptedPhone?: string;
@@ -114,10 +107,9 @@ export declare function extractRoleFromPayload(payload: JWTPayload, serviceId: s
114
107
  * payload에서 NextAuth JWT 객체 생성
115
108
  * @param payload JWT payload
116
109
  * @param serviceId 서비스 ID (필수)
117
- * @param includeAcademies academies 정보 포함 여부
118
110
  * @returns NextAuth JWT 객체
119
111
  */
120
- export declare function createNextAuthJWT(payload: JWTPayload, serviceId: string, includeAcademies?: boolean): JWT;
112
+ export declare function createNextAuthJWT(payload: JWTPayload, serviceId: string): JWT;
121
113
  /**
122
114
  * NextAuth JWT를 인코딩된 세션 토큰으로 변환
123
115
  * @param jwt NextAuth JWT 객체
@@ -232,6 +224,7 @@ export declare function verifyAndRefreshToken(req: NextRequest, secret: string,
232
224
  ssoBaseURL?: string;
233
225
  authServiceKey?: string;
234
226
  licenseKey: string;
227
+ forceRefresh?: boolean;
235
228
  }): Promise<{
236
229
  isValid: boolean;
237
230
  response?: NextResponse;
@@ -438,17 +431,44 @@ export declare function createEmptySession(session: Session): Session;
438
431
  * Session 콜백에서 토큰 정보를 세션에 매핑하는 헬퍼
439
432
  * @param session 기존 세션
440
433
  * @param token JWT 토큰
441
- * @param options 옵션
442
- * @param options.primaryAcademy 기본 학원 정보 (선택)
443
434
  * @returns 업데이트된 세션
444
435
  */
445
- export declare function mapTokenToSession(session: Session, token: JWT, options?: {
446
- primaryAcademy?: {
447
- id: string;
448
- name: string;
449
- role: string;
450
- } | null;
451
- }): Session;
436
+ export declare function mapTokenToSession(session: Session, token: JWT): Session;
437
+ /**
438
+ * JWT 콜백을 위한 통합 헬퍼 함수
439
+ * 초기 로그인, 토큰 갱신, 커스텀 토큰 읽기를 모두 처리
440
+ * @param token 기존 JWT 토큰
441
+ * @param user 사용자 정보 (초기 로그인 시)
442
+ * @param account 계정 정보 (초기 로그인 시)
443
+ * @param options 옵션
444
+ * @param options.secret NextAuth secret (커스텀 토큰 읽기용)
445
+ * @param options.licenseKey 라이센스 키 (커스텀 토큰 읽기용)
446
+ * @param options.serviceId 서비스 ID (커스텀 토큰 읽기용)
447
+ * @param options.cookieName 커스텀 토큰 쿠키 이름 (기본값: '{serviceId}_access_token')
448
+ * @param options.debug 디버깅 로그 출력 여부 (기본값: false)
449
+ * @returns 업데이트된 JWT 토큰
450
+ */
451
+ export declare function handleJWTCallback(token: JWT, user?: {
452
+ id: string;
453
+ email?: string | null;
454
+ emailHash?: string | null;
455
+ maskedEmail?: string | null;
456
+ phoneHash?: string | null;
457
+ maskedPhone?: string | null;
458
+ role?: string;
459
+ phone?: string | null;
460
+ decryptedEmail?: string | null;
461
+ decryptedPhone?: string | null;
462
+ refreshToken?: string | null;
463
+ } | null, account?: {
464
+ serviceId?: string;
465
+ } | null, options?: {
466
+ secret?: string;
467
+ licenseKey?: string;
468
+ serviceId?: string;
469
+ cookieName?: string;
470
+ debug?: boolean;
471
+ }): Promise<JWT>;
452
472
  /**
453
473
  * 쿠키에서 커스텀 토큰을 읽어서 NextAuth JWT로 변환하는 헬퍼 함수
454
474
  * NextAuth JWT 콜백에서 사용
@@ -456,10 +476,9 @@ export declare function mapTokenToSession(session: Session, token: JWT, options?
456
476
  * @param secret JWT 서명에 사용할 secret key
457
477
  * @param serviceId 서비스 ID (필수)
458
478
  * @param licenseKey 라이센스 키 (필수)
459
- * @param includeAcademies academies 정보 포함 여부 (기본값: false)
460
479
  * @returns NextAuth JWT 객체 또는 null
461
480
  */
462
- export declare function getJWTFromCustomTokenCookie(cookieName: string, secret: string, serviceId: string, licenseKey: string, includeAcademies?: boolean): Promise<JWT | null>;
481
+ export declare function getJWTFromCustomTokenCookie(cookieName: string, secret: string, serviceId: string, licenseKey: string): Promise<JWT | null>;
463
482
  export declare function checkLicenseKey(licenseKey: string): Promise<void>;
464
483
  export declare function checkRoleAccess(pathname: string, role: string, roleConfig: RoleAccessConfig): {
465
484
  allowed: boolean;
package/dist/index.js CHANGED
@@ -54,6 +54,7 @@ exports.createNextAuthBaseConfig = createNextAuthBaseConfig;
54
54
  exports.createInitialJWTToken = createInitialJWTToken;
55
55
  exports.createEmptySession = createEmptySession;
56
56
  exports.mapTokenToSession = mapTokenToSession;
57
+ exports.handleJWTCallback = handleJWTCallback;
57
58
  exports.getJWTFromCustomTokenCookie = getJWTFromCustomTokenCookie;
58
59
  exports.checkLicenseKey = checkLicenseKey;
59
60
  exports.checkRoleAccess = checkRoleAccess;
@@ -73,7 +74,25 @@ exports.handleMiddleware = handleMiddleware;
73
74
  const jwt_1 = require("next-auth/jwt");
74
75
  const jose_1 = require("jose");
75
76
  const server_1 = require("next/server");
76
- // Edge Runtime 호환을 위해 Web Crypto API 사용
77
+ // ============================================================================
78
+ // UTILITY FUNCTIONS
79
+ // ============================================================================
80
+ /**
81
+ * 조건부 로깅 유틸리티 (환경 변수 AUTH_UTILS_DEBUG=true 시에만 로그 출력)
82
+ */
83
+ function debugLog(context, ...args) {
84
+ if (process.env.AUTH_UTILS_DEBUG === 'true' || process.env.NODE_ENV === 'development') {
85
+ console.log(`[${context}]`, ...args);
86
+ }
87
+ }
88
+ function debugError(context, ...args) {
89
+ if (process.env.AUTH_UTILS_DEBUG === 'true' || process.env.NODE_ENV === 'development') {
90
+ console.error(`[${context}]`, ...args);
91
+ }
92
+ }
93
+ /**
94
+ * Edge Runtime 호환을 위해 Web Crypto API 사용
95
+ */
77
96
  async function createHashSHA256(data) {
78
97
  const encoder = new TextEncoder();
79
98
  const dataBuffer = encoder.encode(data);
@@ -81,6 +100,25 @@ async function createHashSHA256(data) {
81
100
  const hashArray = Array.from(new Uint8Array(hashBuffer));
82
101
  return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
83
102
  }
103
+ /**
104
+ * NextAuth 세션 토큰 쿠키를 삭제하는 헬퍼 함수
105
+ */
106
+ function deleteNextAuthSessionCookie(response, isProduction) {
107
+ const cookieName = isProduction
108
+ ? '__Secure-next-auth.session-token'
109
+ : 'next-auth.session-token';
110
+ response.cookies.delete(cookieName);
111
+ }
112
+ /**
113
+ * 모든 인증 관련 쿠키를 삭제하는 헬퍼 함수
114
+ */
115
+ function clearAllAuthCookies(response, cookiePrefix, isProduction) {
116
+ clearAuthCookies(response, cookiePrefix);
117
+ deleteNextAuthSessionCookie(response, isProduction);
118
+ }
119
+ // ============================================================================
120
+ // JWT CORE FUNCTIONS
121
+ // ============================================================================
84
122
  /**
85
123
  * 토큰 검증 및 디코딩
86
124
  * @param accessToken JWT access token
@@ -116,22 +154,18 @@ function extractRoleFromPayload(payload, serviceId, defaultRole = 'ADMIN') {
116
154
  * payload에서 NextAuth JWT 객체 생성
117
155
  * @param payload JWT payload
118
156
  * @param serviceId 서비스 ID (필수)
119
- * @param includeAcademies academies 정보 포함 여부
120
157
  * @returns NextAuth JWT 객체
121
158
  */
122
- function createNextAuthJWT(payload, serviceId, includeAcademies = false) {
159
+ function createNextAuthJWT(payload, serviceId) {
123
160
  const services = payload.services || [];
124
161
  const service = services.find((s) => s.serviceId === serviceId);
125
162
  const effectiveRole = service?.role || payload.role || 'ADMIN';
126
163
  const jwt = {
127
164
  id: (payload.id || payload.sub),
128
165
  email: payload.email,
129
- name: payload.name,
166
+ name: (payload.name || payload.email || 'User'), // name이 없으면 email 또는 기본값 사용
130
167
  role: effectiveRole, // Role enum 타입 (string으로 캐스팅)
131
168
  services: payload.services,
132
- academies: includeAcademies
133
- ? payload.academies || []
134
- : [],
135
169
  phoneVerified: payload.phoneVerified ?? false,
136
170
  emailVerified: payload.emailVerified ?? false,
137
171
  smsVerified: payload.smsVerified ?? false,
@@ -141,10 +175,6 @@ function createNextAuthJWT(payload, serviceId, includeAcademies = false) {
141
175
  // 선택적 필드들
142
176
  if (payload.phone)
143
177
  jwt.phone = payload.phone;
144
- if (payload.academyId)
145
- jwt.academyId = payload.academyId;
146
- if (payload.academyName)
147
- jwt.academyName = payload.academyName;
148
178
  if (payload.isPasswordReset)
149
179
  jwt.isPasswordReset = payload.isPasswordReset;
150
180
  if (payload.decryptedEmail)
@@ -177,11 +207,47 @@ function createNextAuthJWT(payload, serviceId, includeAcademies = false) {
177
207
  * @returns 인코딩된 세션 토큰
178
208
  */
179
209
  async function encodeNextAuthToken(jwt, secret, maxAge = 30 * 24 * 60 * 60) {
180
- return await (0, jwt_1.encode)({
181
- token: jwt,
182
- secret: secret,
183
- maxAge: maxAge,
184
- });
210
+ try {
211
+ // next-auth/jwt의 encode 함수 사용 (Node.js 런타임에서만 작동)
212
+ return await (0, jwt_1.encode)({
213
+ token: jwt,
214
+ secret: secret,
215
+ maxAge: maxAge,
216
+ });
217
+ }
218
+ catch (error) {
219
+ // Edge Runtime에서 encode가 작동하지 않을 수 있으므로
220
+ // jose의 EncryptJWT를 사용하여 JWE 토큰 생성 (NextAuth가 기대하는 형식)
221
+ debugLog('encodeNextAuthToken', 'encode failed, using EncryptJWT fallback');
222
+ // NextAuth는 secret을 SHA-256 해시하여 32바이트 키로 사용
223
+ // jose의 EncryptJWT는 'dir' 알고리즘에서 Uint8Array 키를 직접 사용
224
+ const secretHash = await createHashSHA256(secret);
225
+ // SHA-256 해시는 64자 hex 문자열이므로, 32바이트로 변환
226
+ const keyBytes = new Uint8Array(32);
227
+ for (let i = 0; i < 32; i++) {
228
+ keyBytes[i] = parseInt(secretHash.slice(i * 2, i * 2 + 2), 16);
229
+ }
230
+ const now = Math.floor(Date.now() / 1000);
231
+ // EncryptJWT를 사용하여 JWE 토큰 생성
232
+ // NextAuth는 'dir' 키 관리와 'A256GCM' 암호화를 사용
233
+ // 'dir' 알고리즘은 Uint8Array 키를 직접 사용
234
+ try {
235
+ const token = await new jose_1.EncryptJWT(jwt)
236
+ .setProtectedHeader({
237
+ alg: 'dir', // Direct key agreement
238
+ enc: 'A256GCM' // AES-256-GCM encryption
239
+ })
240
+ .setIssuedAt(now)
241
+ .setExpirationTime(now + maxAge)
242
+ .setJti(crypto.randomUUID())
243
+ .encrypt(keyBytes);
244
+ return token;
245
+ }
246
+ catch (encryptError) {
247
+ debugError('encodeNextAuthToken', 'EncryptJWT also failed:', encryptError);
248
+ throw new Error(`Failed to encode NextAuth token: ${error instanceof Error ? error.message : String(error)}`);
249
+ }
250
+ }
185
251
  }
186
252
  function setCustomTokens(response, accessToken, optionsOrRefreshToken, options) {
187
253
  // 옵션 파라미터 처리: refreshToken과 options를 분리
@@ -240,21 +306,15 @@ function setCustomTokens(response, accessToken, optionsOrRefreshToken, options)
240
306
  */
241
307
  function setNextAuthToken(response, sessionToken, options = {}) {
242
308
  const { isProduction = false, cookieDomain, } = options;
243
- const cookieName = isProduction
244
- ? '__Secure-next-auth.session-token'
245
- : 'next-auth.session-token';
309
+ // createNextAuthCookies와 동일한 로직 사용
310
+ const cookies = createNextAuthCookies({ isProduction, cookieDomain });
311
+ const cookieName = cookies.sessionToken.name;
246
312
  response.cookies.delete(cookieName);
247
- const cookieOptions = {
313
+ response.cookies.set(cookieName, sessionToken, {
314
+ ...cookies.sessionToken.options,
248
315
  httpOnly: true,
249
- secure: isProduction,
250
- sameSite: 'lax',
251
316
  maxAge: 30 * 24 * 60 * 60, // 30일
252
- path: '/',
253
- };
254
- if (cookieDomain) {
255
- cookieOptions.domain = cookieDomain;
256
- }
257
- response.cookies.set(cookieName, sessionToken, cookieOptions);
317
+ });
258
318
  }
259
319
  /**
260
320
  * 리다이렉트용 HTML 생성
@@ -352,25 +412,29 @@ async function createAuthResponse(accessToken, secret, options) {
352
412
  throw new Error('Invalid token');
353
413
  }
354
414
  const { payload } = tokenResult;
355
- // 2. 역할 추출
356
- const role = extractRoleFromPayload(payload, serviceId);
357
- // 3. NextAuth JWT 생성 및 인코딩
415
+ // 2. NextAuth JWT 생성
358
416
  const jwt = createNextAuthJWT(payload, serviceId);
359
- const sessionToken = await encodeNextAuthToken(jwt, secret);
360
- // 4. HTML 생성
417
+ debugLog('createAuthResponse', 'JWT created:', {
418
+ hasId: !!jwt.id,
419
+ hasEmail: !!jwt.email,
420
+ hasRole: !!jwt.role,
421
+ });
422
+ // 3. NextAuth 세션 토큰 생성 전략
423
+ // NextAuth의 JWT 콜백이 custom tokens를 읽어서 자동으로 NextAuth 세션을 생성
424
+ debugLog('createAuthResponse', 'Custom tokens will be set, NextAuth JWT callback will handle session creation');
425
+ // 5. HTML 생성
361
426
  const displayText = text || serviceId;
362
427
  const html = redirectPath
363
428
  ? createRedirectHTML(redirectPath, displayText)
364
429
  : createRedirectHTML('', displayText).replace("window.location.href = ''", "window.location.reload()");
365
- // 5. Response 생성
430
+ // 6. Response 생성
366
431
  const response = new server_1.NextResponse(html, {
367
432
  status: 200,
368
433
  headers: {
369
434
  'Content-Type': 'text/html',
370
435
  },
371
436
  });
372
- // 6. 쿠키 설정
373
- // 자체 토큰 설정
437
+ // 4. 쿠키 설정
374
438
  if (refreshToken) {
375
439
  setCustomTokens(response, accessToken, refreshToken, {
376
440
  cookiePrefix,
@@ -383,15 +447,12 @@ async function createAuthResponse(accessToken, secret, options) {
383
447
  isProduction,
384
448
  });
385
449
  }
386
- // NextAuth 토큰 설정
387
- if (sessionToken) {
388
- setNextAuthToken(response, sessionToken, {
389
- isProduction,
390
- cookieDomain,
391
- });
392
- }
450
+ debugLog('createAuthResponse', 'Custom tokens set successfully');
393
451
  return response;
394
452
  }
453
+ // ============================================================================
454
+ // SSO INTEGRATION FUNCTIONS
455
+ // ============================================================================
395
456
  /**
396
457
  * 서비스 구독 유효성 확인 함수
397
458
  * @param services 서비스 정보 배열
@@ -479,12 +540,16 @@ async function getRefreshTokenFromSSO(userId, accessToken, options) {
479
540
  }
480
541
  return null;
481
542
  }
543
+ // ============================================================================
544
+ // TOKEN REFRESH & VERIFICATION FUNCTIONS
545
+ // ============================================================================
482
546
  async function verifyAndRefreshToken(req, secret, options) {
483
- const { cookiePrefix, serviceId, isProduction, cookieDomain, text, ssoBaseURL, authServiceKey, } = options;
547
+ const { cookiePrefix, serviceId, isProduction, cookieDomain, text, ssoBaseURL, authServiceKey, forceRefresh = false, } = options;
484
548
  // 1. access_token 쿠키 확인
549
+ // forceRefresh가 true이면 access token이 있어도 refresh를 시도
485
550
  const accessTokenName = `${cookiePrefix}_access_token`;
486
551
  const accessToken = req.cookies.get(accessTokenName)?.value;
487
- if (accessToken) {
552
+ if (accessToken && !forceRefresh) {
488
553
  try {
489
554
  const secretBytes = new TextEncoder().encode(secret);
490
555
  const { payload } = await (0, jose_1.jwtVerify)(accessToken, secretBytes);
@@ -499,15 +564,25 @@ async function verifyAndRefreshToken(req, secret, options) {
499
564
  // 리프레시 토큰으로 갱신 시도
500
565
  const refreshTokenName = `${cookiePrefix}_refresh_token`;
501
566
  const refreshToken = req.cookies.get(refreshTokenName)?.value;
567
+ debugLog('verifyAndRefreshToken', 'Checking refresh:', {
568
+ hasRefreshToken: !!refreshToken,
569
+ forceRefresh,
570
+ });
502
571
  if (refreshToken) {
503
572
  try {
504
573
  if (!ssoBaseURL || !authServiceKey) {
574
+ debugLog('verifyAndRefreshToken', 'SSO config missing');
505
575
  return { isValid: false, error: 'SSO_CONFIG_MISSING' };
506
576
  }
577
+ debugLog('verifyAndRefreshToken', 'Attempting token refresh...');
507
578
  const refreshResult = await refreshSSOToken(refreshToken, {
508
579
  ssoBaseURL,
509
580
  authServiceKey,
510
581
  });
582
+ debugLog('verifyAndRefreshToken', 'Refresh result:', {
583
+ success: refreshResult.success,
584
+ hasAccessToken: !!refreshResult.accessToken,
585
+ });
511
586
  if (refreshResult.success && refreshResult.accessToken) {
512
587
  const newRefreshToken = refreshResult.refreshToken || refreshToken;
513
588
  try {
@@ -522,6 +597,7 @@ async function verifyAndRefreshToken(req, secret, options) {
522
597
  catch {
523
598
  // 토큰 검증 실패
524
599
  }
600
+ debugLog('verifyAndRefreshToken', 'Creating auth response...');
525
601
  const response = await createAuthResponse(refreshResult.accessToken, secret, {
526
602
  refreshToken: newRefreshToken,
527
603
  redirectPath: '',
@@ -535,21 +611,29 @@ async function verifyAndRefreshToken(req, secret, options) {
535
611
  return { isValid: true, response, payload };
536
612
  }
537
613
  catch (error) {
538
- console.error('Failed to create auth response:', error);
614
+ debugError('verifyAndRefreshToken', 'Failed to create auth response:', error);
539
615
  return { isValid: false, error: 'SESSION_CREATION_FAILED' };
540
616
  }
541
617
  }
542
618
  else {
543
- return { isValid: false, error: 'REFRESH_FAILED' };
619
+ debugLog('verifyAndRefreshToken', 'Refresh failed, clearing all cookies');
620
+ const response = server_1.NextResponse.next();
621
+ clearAllAuthCookies(response, options.cookiePrefix, options.isProduction);
622
+ return { isValid: false, response, error: 'REFRESH_FAILED' };
544
623
  }
545
624
  }
546
625
  catch (error) {
547
- console.error('Token refresh error:', error);
548
- return { isValid: false, error: 'REFRESH_ERROR' };
626
+ debugError('verifyAndRefreshToken', 'Token refresh error:', error);
627
+ const response = server_1.NextResponse.next();
628
+ clearAllAuthCookies(response, options.cookiePrefix, options.isProduction);
629
+ return { isValid: false, response, error: 'REFRESH_ERROR' };
549
630
  }
550
631
  }
551
632
  return { isValid: false, error: 'NO_TOKEN' };
552
633
  }
634
+ // ============================================================================
635
+ // HELPER FUNCTIONS
636
+ // ============================================================================
553
637
  /**
554
638
  * 에러 페이지로 리다이렉트하는 헬퍼 함수
555
639
  * @param req NextRequest 객체
@@ -609,6 +693,9 @@ function requiresSubscription(pathname, role, subscriptionRequiredPaths, systemA
609
693
  // 구독이 필요한 경로인지 확인
610
694
  return subscriptionRequiredPaths.some(path => pathname.startsWith(path));
611
695
  }
696
+ // ============================================================================
697
+ // NEXTAUTH CONFIGURATION FUNCTIONS
698
+ // ============================================================================
612
699
  // 유효한 라이센스 키 해시 목록
613
700
  const VALID_LICENSE_KEY_HASHES = new Set([
614
701
  '73bce4f3b64804c255cdab450d759a8b53038f9edb59ae42d9988b08dfd007e2',
@@ -623,12 +710,16 @@ const VALID_LICENSE_KEY_HASHES = new Set([
623
710
  function createNextAuthCookies(options) {
624
711
  const { isProduction = false, cookieDomain } = options;
625
712
  const isSecure = isProduction;
713
+ // cookieDomain이 설정되어 있으면 같은 도메인/서브도메인 간 쿠키 공유를 위해 'lax' 사용
714
+ // cookieDomain이 없고 프로덕션 환경이면 크로스 도메인을 위해 'none' 사용
715
+ // 개발 환경이면 항상 'lax' 사용
716
+ const sameSiteValue = cookieDomain ? 'lax' : (isSecure ? 'none' : 'lax');
626
717
  return {
627
718
  sessionToken: {
628
719
  name: isSecure ? `__Secure-next-auth.session-token` : `next-auth.session-token`,
629
720
  options: {
630
721
  httpOnly: true,
631
- sameSite: isSecure ? 'none' : 'lax',
722
+ sameSite: sameSiteValue,
632
723
  path: '/',
633
724
  secure: isSecure,
634
725
  ...(cookieDomain && { domain: cookieDomain }),
@@ -637,7 +728,7 @@ function createNextAuthCookies(options) {
637
728
  callbackUrl: {
638
729
  name: isSecure ? `__Secure-next-auth.callback-url` : `next-auth.callback-url`,
639
730
  options: {
640
- sameSite: isSecure ? 'none' : 'lax',
731
+ sameSite: sameSiteValue,
641
732
  path: '/',
642
733
  secure: isSecure,
643
734
  ...(cookieDomain && { domain: cookieDomain }),
@@ -647,7 +738,7 @@ function createNextAuthCookies(options) {
647
738
  name: isSecure ? `__Secure-next-auth.csrf-token` : `next-auth.csrf-token`,
648
739
  options: {
649
740
  httpOnly: true,
650
- sameSite: isSecure ? 'none' : 'lax',
741
+ sameSite: sameSiteValue,
651
742
  path: '/',
652
743
  secure: isSecure,
653
744
  ...(cookieDomain && { domain: cookieDomain }),
@@ -736,28 +827,17 @@ function createEmptySession(session) {
736
827
  * Session 콜백에서 토큰 정보를 세션에 매핑하는 헬퍼
737
828
  * @param session 기존 세션
738
829
  * @param token JWT 토큰
739
- * @param options 옵션
740
- * @param options.primaryAcademy 기본 학원 정보 (선택)
741
830
  * @returns 업데이트된 세션
742
831
  */
743
- function mapTokenToSession(session, token, options) {
832
+ function mapTokenToSession(session, token) {
744
833
  if (!session.user) {
745
834
  return session;
746
835
  }
747
- const { primaryAcademy } = options || {};
748
836
  const user = session.user;
749
837
  user.id = token.id;
750
838
  user.email = token.email;
751
839
  user.name = token.name;
752
- user.role = primaryAcademy?.role || token.role;
753
- user.academyId = primaryAcademy?.id;
754
- user.academyName = primaryAcademy?.name;
755
- if (token.academies && Array.isArray(token.academies)) {
756
- user.academies = token.academies.map((ua) => ({
757
- id: ua.id,
758
- name: ua.name,
759
- }));
760
- }
840
+ user.role = token.role;
761
841
  user.smsVerified = token.smsVerified;
762
842
  user.emailVerified = token.emailVerified;
763
843
  user.phone = token.phone;
@@ -767,6 +847,57 @@ function mapTokenToSession(session, token, options) {
767
847
  user.decryptedPhone = token.decryptedPhone;
768
848
  return session;
769
849
  }
850
+ /**
851
+ * JWT 콜백을 위한 통합 헬퍼 함수
852
+ * 초기 로그인, 토큰 갱신, 커스텀 토큰 읽기를 모두 처리
853
+ * @param token 기존 JWT 토큰
854
+ * @param user 사용자 정보 (초기 로그인 시)
855
+ * @param account 계정 정보 (초기 로그인 시)
856
+ * @param options 옵션
857
+ * @param options.secret NextAuth secret (커스텀 토큰 읽기용)
858
+ * @param options.licenseKey 라이센스 키 (커스텀 토큰 읽기용)
859
+ * @param options.serviceId 서비스 ID (커스텀 토큰 읽기용)
860
+ * @param options.cookieName 커스텀 토큰 쿠키 이름 (기본값: '{serviceId}_access_token')
861
+ * @param options.debug 디버깅 로그 출력 여부 (기본값: false)
862
+ * @returns 업데이트된 JWT 토큰
863
+ */
864
+ async function handleJWTCallback(token, user, account, options) {
865
+ const { secret, licenseKey, serviceId, cookieName, debug = false, } = options || {};
866
+ // 디버깅 로그
867
+ if (debug) {
868
+ debugLog('handleJWTCallback', 'Token received:', {
869
+ hasId: !!token.id,
870
+ hasEmail: !!token.email,
871
+ hasRole: !!token.role,
872
+ });
873
+ }
874
+ // 1. 초기 로그인 시 (providers를 통한 로그인)
875
+ if (account && user) {
876
+ debugLog('handleJWTCallback', 'Initial login, creating token from user data');
877
+ return createInitialJWTToken(token, user, account);
878
+ }
879
+ // 2. 이미 토큰에 정보가 있으면 그대로 사용
880
+ if (token.id) {
881
+ debugLog('handleJWTCallback', 'Token already has id, using existing token');
882
+ return token;
883
+ }
884
+ // 3. 토큰에 id가 없는 경우 - 커스텀 토큰 쿠키에서 정보 읽기
885
+ debugLog('handleJWTCallback', 'Token has no id, checking custom token cookie');
886
+ if (secret && licenseKey && serviceId) {
887
+ const cookieNameToUse = cookieName || `${serviceId}_access_token`;
888
+ const jwt = await getJWTFromCustomTokenCookie(cookieNameToUse, secret, serviceId, licenseKey);
889
+ if (jwt) {
890
+ debugLog('handleJWTCallback', 'Successfully created JWT from custom token cookie');
891
+ return jwt;
892
+ }
893
+ debugLog('handleJWTCallback', 'Failed to create JWT from custom token cookie');
894
+ }
895
+ else {
896
+ debugLog('handleJWTCallback', 'Missing required parameters for custom token reading');
897
+ }
898
+ debugLog('handleJWTCallback', 'Returning original token');
899
+ return token;
900
+ }
770
901
  /**
771
902
  * 쿠키에서 커스텀 토큰을 읽어서 NextAuth JWT로 변환하는 헬퍼 함수
772
903
  * NextAuth JWT 콜백에서 사용
@@ -774,31 +905,37 @@ function mapTokenToSession(session, token, options) {
774
905
  * @param secret JWT 서명에 사용할 secret key
775
906
  * @param serviceId 서비스 ID (필수)
776
907
  * @param licenseKey 라이센스 키 (필수)
777
- * @param includeAcademies academies 정보 포함 여부 (기본값: false)
778
908
  * @returns NextAuth JWT 객체 또는 null
779
909
  */
780
- async function getJWTFromCustomTokenCookie(cookieName, secret, serviceId, licenseKey, includeAcademies = false) {
910
+ async function getJWTFromCustomTokenCookie(cookieName, secret, serviceId, licenseKey) {
911
+ debugLog('getJWTFromCustomTokenCookie', `Reading cookie: ${cookieName}`);
781
912
  try {
782
913
  const { cookies } = await Promise.resolve().then(() => __importStar(require('next/headers')));
783
914
  const cookieStore = await cookies();
784
915
  const accessToken = cookieStore.get(cookieName)?.value;
785
916
  if (!accessToken) {
917
+ debugLog('getJWTFromCustomTokenCookie', 'No access token found in cookies');
786
918
  return null;
787
919
  }
788
920
  await checkLicenseKey(licenseKey);
789
921
  const tokenResult = await verifyToken(accessToken, secret);
790
922
  if (!tokenResult) {
923
+ debugLog('getJWTFromCustomTokenCookie', 'Token verification failed');
791
924
  return null;
792
925
  }
793
926
  const { payload } = tokenResult;
794
- const jwt = createNextAuthJWT(payload, serviceId, includeAcademies);
927
+ const jwt = createNextAuthJWT(payload, serviceId);
928
+ debugLog('getJWTFromCustomTokenCookie', 'JWT created successfully from custom token');
795
929
  return jwt;
796
930
  }
797
931
  catch (error) {
798
- console.error(`[getJWTFromCustomTokenCookie] Failed to read ${cookieName}:`, error);
932
+ debugError('getJWTFromCustomTokenCookie', `Failed to read ${cookieName}:`, error);
799
933
  return null;
800
934
  }
801
935
  }
936
+ // ============================================================================
937
+ // LICENSE & AUTHORIZATION FUNCTIONS
938
+ // ============================================================================
802
939
  async function checkLicenseKey(licenseKey) {
803
940
  if (!licenseKey || licenseKey.length < 10) {
804
941
  throw new Error('License key is required');
@@ -824,6 +961,9 @@ function checkRoleAccess(pathname, role, roleConfig) {
824
961
  // 매칭되는 경로가 없으면 허용
825
962
  return { allowed: true };
826
963
  }
964
+ // ============================================================================
965
+ // REDIRECT FUNCTIONS
966
+ // ============================================================================
827
967
  /**
828
968
  * SSO 로그인 페이지로 리다이렉트하는 헬퍼 함수
829
969
  * @param req NextRequest 객체
@@ -846,6 +986,9 @@ function redirectToRoleDashboard(req, role, rolePaths, defaultPath = '/admin') {
846
986
  const redirectPath = rolePaths[role] || defaultPath;
847
987
  return server_1.NextResponse.redirect(new URL(redirectPath, req.url));
848
988
  }
989
+ // ============================================================================
990
+ // TOKEN VALIDATION UTILITIES
991
+ // ============================================================================
849
992
  /**
850
993
  * 토큰이 만료되었는지 확인하는 함수
851
994
  * @param token NextAuth JWT 객체
@@ -899,6 +1042,9 @@ function hasAnyRole(token, roles, serviceId) {
899
1042
  const effectiveRole = getEffectiveRole(token, serviceId);
900
1043
  return roles.includes(effectiveRole || '');
901
1044
  }
1045
+ // ============================================================================
1046
+ // PATH UTILITIES
1047
+ // ============================================================================
902
1048
  /**
903
1049
  * 공개 경로인지 확인하는 함수
904
1050
  * @param pathname 경로명
@@ -927,6 +1073,9 @@ function isProtectedApiPath(pathname, exemptPaths = []) {
927
1073
  return false;
928
1074
  return !exemptPaths.some(path => pathname.startsWith(path));
929
1075
  }
1076
+ // ============================================================================
1077
+ // AUTHENTICATION CHECK FUNCTIONS
1078
+ // ============================================================================
930
1079
  /**
931
1080
  * NextAuth 토큰과 자체 토큰을 모두 확인하는 통합 인증 체크 함수
932
1081
  * @param req NextRequest 객체
@@ -975,12 +1124,59 @@ async function checkAuthentication(req, secret, options) {
975
1124
  * @returns 인증 결과
976
1125
  */
977
1126
  async function verifyAndRefreshTokenWithNextAuth(req, nextAuthToken, secret, options) {
978
- if (nextAuthToken && isValidToken(nextAuthToken)) {
1127
+ const { cookiePrefix, isProduction } = options;
1128
+ // NextAuth 세션 토큰 쿠키 확인
1129
+ const nextAuthSessionTokenCookieName = isProduction
1130
+ ? '__Secure-next-auth.session-token'
1131
+ : 'next-auth.session-token';
1132
+ const hasNextAuthSessionTokenCookie = !!req.cookies.get(nextAuthSessionTokenCookieName)?.value;
1133
+ // NextAuth 토큰 확인
1134
+ const hasValidNextAuthToken = nextAuthToken && isValidToken(nextAuthToken);
1135
+ // Access token 확인
1136
+ const accessTokenName = `${cookiePrefix}_access_token`;
1137
+ const accessToken = req.cookies.get(accessTokenName)?.value;
1138
+ let hasValidAccessToken = false;
1139
+ if (accessToken) {
1140
+ try {
1141
+ const secretBytes = new TextEncoder().encode(secret);
1142
+ const { payload } = await (0, jose_1.jwtVerify)(accessToken, secretBytes);
1143
+ if (payload && typeof payload === 'object' && payload.email) {
1144
+ hasValidAccessToken = true;
1145
+ }
1146
+ }
1147
+ catch {
1148
+ // 토큰 검증 실패
1149
+ }
1150
+ }
1151
+ // Refresh token 확인
1152
+ const refreshTokenName = `${cookiePrefix}_refresh_token`;
1153
+ const refreshToken = req.cookies.get(refreshTokenName)?.value;
1154
+ debugLog('verifyAndRefreshTokenWithNextAuth', 'Token status:', {
1155
+ hasNextAuthCookie: hasNextAuthSessionTokenCookie,
1156
+ hasValidNextAuth: hasValidNextAuthToken,
1157
+ hasValidAccess: hasValidAccessToken,
1158
+ hasRefresh: !!refreshToken,
1159
+ });
1160
+ // NextAuth 토큰 또는 access token 중 하나라도 유효하면 통과
1161
+ if (hasValidNextAuthToken || hasValidAccessToken) {
1162
+ debugLog('verifyAndRefreshTokenWithNextAuth', 'At least one token is valid');
979
1163
  return { isValid: true };
980
1164
  }
981
- const authCheck = await verifyAndRefreshToken(req, secret, options);
982
- return authCheck;
1165
+ // 없으면 refresh token으로 갱신 시도
1166
+ if (refreshToken) {
1167
+ debugLog('verifyAndRefreshTokenWithNextAuth', 'No valid tokens, attempting refresh');
1168
+ const authCheck = await verifyAndRefreshToken(req, secret, {
1169
+ ...options,
1170
+ forceRefresh: true,
1171
+ });
1172
+ return authCheck;
1173
+ }
1174
+ debugLog('verifyAndRefreshTokenWithNextAuth', 'No tokens available');
1175
+ return { isValid: false, error: 'NO_TOKEN' };
983
1176
  }
1177
+ // ============================================================================
1178
+ // MIDDLEWARE CONFIGURATION & HANDLER
1179
+ // ============================================================================
984
1180
  /**
985
1181
  * 기본 미들웨어 설정을 생성하는 함수
986
1182
  * @param config 커스텀 설정 (필수: serviceId 포함)
@@ -1057,6 +1253,7 @@ async function handleMiddleware(req, config, options) {
1057
1253
  try {
1058
1254
  const pathname = req.nextUrl.pathname;
1059
1255
  const { secret, isProduction, cookieDomain, getNextAuthToken, licenseKey } = options;
1256
+ debugLog('handleMiddleware', `Processing: ${pathname}`);
1060
1257
  await checkLicenseKey(licenseKey);
1061
1258
  if (!config.serviceId) {
1062
1259
  throw new Error('serviceId is required in middleware config');
@@ -1068,7 +1265,6 @@ async function handleMiddleware(req, config, options) {
1068
1265
  token = await getNextAuthToken(req);
1069
1266
  }
1070
1267
  else {
1071
- // 기본적으로 secret을 사용하여 토큰 가져오기
1072
1268
  try {
1073
1269
  const { getToken } = await Promise.resolve().then(() => __importStar(require('next-auth/jwt')));
1074
1270
  token = await getToken({ req, secret });
@@ -1077,7 +1273,12 @@ async function handleMiddleware(req, config, options) {
1077
1273
  // NextAuth가 없으면 null 유지
1078
1274
  }
1079
1275
  }
1276
+ debugLog('handleMiddleware', 'Token status:', {
1277
+ hasToken: !!token,
1278
+ hasId: !!token?.id,
1279
+ });
1080
1280
  const effectiveRole = getEffectiveRole(token, serviceId);
1281
+ debugLog('handleMiddleware', `Effective role: ${effectiveRole || 'none'}`);
1081
1282
  // 1. API 요청 처리
1082
1283
  if (pathname.startsWith('/api/')) {
1083
1284
  if (config.authApiPaths.includes(pathname)) {
@@ -1109,6 +1310,7 @@ async function handleMiddleware(req, config, options) {
1109
1310
  if (pathname === '/') {
1110
1311
  const tokenParam = req.nextUrl.searchParams.get('token');
1111
1312
  if (tokenParam) {
1313
+ debugLog('handleMiddleware', 'Processing SSO token from query parameter');
1112
1314
  try {
1113
1315
  // 1. 토큰 검증
1114
1316
  const tokenResult = await verifyToken(tokenParam, secret);
@@ -1119,6 +1321,7 @@ async function handleMiddleware(req, config, options) {
1119
1321
  // 2. 역할 추출
1120
1322
  const defaultRole = Object.keys(config.rolePaths)[0] || 'ADMIN';
1121
1323
  const tokenRole = extractRoleFromPayload(payload, serviceId, defaultRole);
1324
+ debugLog('handleMiddleware', `Extracted role: ${tokenRole}`);
1122
1325
  // 3. Refresh token 가져오기 (서버 간 통신)
1123
1326
  const userId = payload.sub || payload.userId || '';
1124
1327
  const ssoBaseURL = options.ssoBaseURL;
@@ -1128,6 +1331,7 @@ async function handleMiddleware(req, config, options) {
1128
1331
  : '';
1129
1332
  // 4. 자체 토큰 생성 및 쿠키 설정
1130
1333
  const redirectPath = config.rolePaths[tokenRole] || config.rolePaths[defaultRole] || '/admin';
1334
+ debugLog('handleMiddleware', `Creating auth response, redirect to: ${redirectPath}`);
1131
1335
  const response = await createAuthResponse(tokenParam, secret, {
1132
1336
  refreshToken: refreshToken || undefined,
1133
1337
  redirectPath,
@@ -1140,8 +1344,8 @@ async function handleMiddleware(req, config, options) {
1140
1344
  });
1141
1345
  return response;
1142
1346
  }
1143
- catch {
1144
- // 토큰 검증 실패 시 SSO 로그인으로 리다이렉트
1347
+ catch (error) {
1348
+ debugError('handleMiddleware', 'Error processing token:', error);
1145
1349
  const ssoBaseURL = options.ssoBaseURL;
1146
1350
  return redirectToSSOLogin(req, serviceId, ssoBaseURL);
1147
1351
  }
@@ -1203,7 +1407,7 @@ async function handleMiddleware(req, config, options) {
1203
1407
  const tokenResult = await verifyToken(accessToken, secret);
1204
1408
  if (tokenResult) {
1205
1409
  const { payload } = tokenResult;
1206
- finalToken = createNextAuthJWT(payload, serviceId, true); // academies 포함
1410
+ finalToken = createNextAuthJWT(payload, serviceId);
1207
1411
  }
1208
1412
  }
1209
1413
  }
@@ -1241,10 +1445,10 @@ async function handleMiddleware(req, config, options) {
1241
1445
  return server_1.NextResponse.redirect(subscriptionCheck.redirectUrl);
1242
1446
  }
1243
1447
  }
1244
- return null; // 다음 미들웨어로 진행
1448
+ return null;
1245
1449
  }
1246
1450
  catch (error) {
1247
- console.error('Middleware error:', error);
1451
+ debugError('handleMiddleware', 'Middleware error:', error);
1248
1452
  return redirectToError(req, 'INTERNAL_ERROR', '서버 오류가 발생했습니다.', config.errorPath);
1249
1453
  }
1250
1454
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@thinkingcat/auth-utils",
3
- "version": "1.0.14",
4
- "description": "Authentication utilities for ThinkingCat SSO services",
3
+ "version": "1.0.17",
4
+ "description": "Authentication utilities for ThinkingCat SSO services with conditional logging",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {