@thinkingcat/auth-utils 1.0.16 → 1.0.18

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.16"
94
+ "@thinkingcat/auth-utils": "^1.0.17"
64
95
  }
65
96
  }
66
97
  ```
@@ -557,7 +588,7 @@ import { createNextAuthBaseConfig } from "@thinkingcat/auth-utils";
557
588
 
558
589
  const baseConfig = createNextAuthBaseConfig({
559
590
  secret: process.env.NEXTAUTH_SECRET!,
560
- isProduction: process.env.NODE_ENV === 'production',
591
+ isProduction: process.env.NODE_ENV === "production",
561
592
  cookieDomain: process.env.COOKIE_DOMAIN,
562
593
  signInPath: "/login",
563
594
  errorPath: "/login",
@@ -595,7 +626,7 @@ JWT 콜백을 위한 통합 헬퍼 함수입니다. 초기 로그인, 토큰 갱
595
626
  - `options.secret`: NextAuth secret (커스텀 토큰 읽기용)
596
627
  - `options.licenseKey`: 라이센스 키 (커스텀 토큰 읽기용)
597
628
  - `options.serviceId`: 서비스 ID (커스텀 토큰 읽기용)
598
- - `options.cookieName`: 커스텀 토큰 쿠키 이름 (기본값: '{serviceId}_access_token')
629
+ - `options.cookieName`: 커스텀 토큰 쿠키 이름 (기본값: '{serviceId}\_access_token')
599
630
  - `options.debug`: 디버깅 로그 출력 여부 (기본값: false)
600
631
 
601
632
  **반환값:**
@@ -1459,10 +1490,32 @@ const response = await handleMiddleware(req, middlewareConfig, {
1459
1490
  ## 📦 패키지 정보
1460
1491
 
1461
1492
  - **패키지명**: `@thinkingcat/auth-utils`
1462
- - **버전**: `1.0.16`
1493
+ - **버전**: `1.0.17`
1463
1494
  - **라이선스**: MIT
1464
1495
  - **저장소**: npm registry
1465
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
+
1466
1519
  ## 🤝 기여 (Contributing)
1467
1520
 
1468
1521
  이슈나 개선 사항이 있으면 GitHub 이슈를 등록해주세요.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { JWT } from "next-auth/jwt";
1
+ import type { JWT } from "next-auth/jwt";
2
2
  import type { Session } from "next-auth";
3
3
  import { NextResponse, NextRequest } from 'next/server';
4
4
  export interface ResponseLike {
@@ -224,6 +224,7 @@ export declare function verifyAndRefreshToken(req: NextRequest, secret: string,
224
224
  ssoBaseURL?: string;
225
225
  authServiceKey?: string;
226
226
  licenseKey: string;
227
+ forceRefresh?: boolean;
227
228
  }): Promise<{
228
229
  isValid: boolean;
229
230
  response?: NextResponse;
package/dist/index.js CHANGED
@@ -71,10 +71,27 @@ exports.checkAuthentication = checkAuthentication;
71
71
  exports.verifyAndRefreshTokenWithNextAuth = verifyAndRefreshTokenWithNextAuth;
72
72
  exports.createMiddlewareConfig = createMiddlewareConfig;
73
73
  exports.handleMiddleware = handleMiddleware;
74
- const jwt_1 = require("next-auth/jwt");
75
74
  const jose_1 = require("jose");
76
75
  const server_1 = require("next/server");
77
- // Edge Runtime 호환을 위해 Web Crypto API 사용
76
+ // ============================================================================
77
+ // UTILITY FUNCTIONS
78
+ // ============================================================================
79
+ /**
80
+ * 조건부 로깅 유틸리티 (환경 변수 AUTH_UTILS_DEBUG=true 시에만 로그 출력)
81
+ */
82
+ function debugLog(context, ...args) {
83
+ if (process.env.AUTH_UTILS_DEBUG === 'true' || process.env.NODE_ENV === 'development') {
84
+ console.log(`[${context}]`, ...args);
85
+ }
86
+ }
87
+ function debugError(context, ...args) {
88
+ if (process.env.AUTH_UTILS_DEBUG === 'true' || process.env.NODE_ENV === 'development') {
89
+ console.error(`[${context}]`, ...args);
90
+ }
91
+ }
92
+ /**
93
+ * Edge Runtime 호환을 위해 Web Crypto API 사용
94
+ */
78
95
  async function createHashSHA256(data) {
79
96
  const encoder = new TextEncoder();
80
97
  const dataBuffer = encoder.encode(data);
@@ -82,6 +99,25 @@ async function createHashSHA256(data) {
82
99
  const hashArray = Array.from(new Uint8Array(hashBuffer));
83
100
  return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
84
101
  }
102
+ /**
103
+ * NextAuth 세션 토큰 쿠키를 삭제하는 헬퍼 함수
104
+ */
105
+ function deleteNextAuthSessionCookie(response, isProduction) {
106
+ const cookieName = isProduction
107
+ ? '__Secure-next-auth.session-token'
108
+ : 'next-auth.session-token';
109
+ response.cookies.delete(cookieName);
110
+ }
111
+ /**
112
+ * 모든 인증 관련 쿠키를 삭제하는 헬퍼 함수
113
+ */
114
+ function clearAllAuthCookies(response, cookiePrefix, isProduction) {
115
+ clearAuthCookies(response, cookiePrefix);
116
+ deleteNextAuthSessionCookie(response, isProduction);
117
+ }
118
+ // ============================================================================
119
+ // JWT CORE FUNCTIONS
120
+ // ============================================================================
85
121
  /**
86
122
  * 토큰 검증 및 디코딩
87
123
  * @param accessToken JWT access token
@@ -126,7 +162,7 @@ function createNextAuthJWT(payload, serviceId) {
126
162
  const jwt = {
127
163
  id: (payload.id || payload.sub),
128
164
  email: payload.email,
129
- name: payload.name,
165
+ name: (payload.name || payload.email || 'User'), // name이 없으면 email 또는 기본값 사용
130
166
  role: effectiveRole, // Role enum 타입 (string으로 캐스팅)
131
167
  services: payload.services,
132
168
  phoneVerified: payload.phoneVerified ?? false,
@@ -170,11 +206,48 @@ function createNextAuthJWT(payload, serviceId) {
170
206
  * @returns 인코딩된 세션 토큰
171
207
  */
172
208
  async function encodeNextAuthToken(jwt, secret, maxAge = 30 * 24 * 60 * 60) {
173
- return await (0, jwt_1.encode)({
174
- token: jwt,
175
- secret: secret,
176
- maxAge: maxAge,
177
- });
209
+ try {
210
+ // next-auth/jwt의 encode 함수를 동적 import로 사용 (Edge Runtime 호환)
211
+ const { encode } = await Promise.resolve().then(() => __importStar(require('next-auth/jwt')));
212
+ return await 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
+ }
178
251
  }
179
252
  function setCustomTokens(response, accessToken, optionsOrRefreshToken, options) {
180
253
  // 옵션 파라미터 처리: refreshToken과 options를 분리
@@ -233,21 +306,15 @@ function setCustomTokens(response, accessToken, optionsOrRefreshToken, options)
233
306
  */
234
307
  function setNextAuthToken(response, sessionToken, options = {}) {
235
308
  const { isProduction = false, cookieDomain, } = options;
236
- const cookieName = isProduction
237
- ? '__Secure-next-auth.session-token'
238
- : 'next-auth.session-token';
309
+ // createNextAuthCookies와 동일한 로직 사용
310
+ const cookies = createNextAuthCookies({ isProduction, cookieDomain });
311
+ const cookieName = cookies.sessionToken.name;
239
312
  response.cookies.delete(cookieName);
240
- const cookieOptions = {
313
+ response.cookies.set(cookieName, sessionToken, {
314
+ ...cookies.sessionToken.options,
241
315
  httpOnly: true,
242
- secure: isProduction,
243
- sameSite: 'lax',
244
316
  maxAge: 30 * 24 * 60 * 60, // 30일
245
- path: '/',
246
- };
247
- if (cookieDomain) {
248
- cookieOptions.domain = cookieDomain;
249
- }
250
- response.cookies.set(cookieName, sessionToken, cookieOptions);
317
+ });
251
318
  }
252
319
  /**
253
320
  * 리다이렉트용 HTML 생성
@@ -345,25 +412,29 @@ async function createAuthResponse(accessToken, secret, options) {
345
412
  throw new Error('Invalid token');
346
413
  }
347
414
  const { payload } = tokenResult;
348
- // 2. 역할 추출
349
- const role = extractRoleFromPayload(payload, serviceId);
350
- // 3. NextAuth JWT 생성 및 인코딩
415
+ // 2. NextAuth JWT 생성
351
416
  const jwt = createNextAuthJWT(payload, serviceId);
352
- const sessionToken = await encodeNextAuthToken(jwt, secret);
353
- // 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 생성
354
426
  const displayText = text || serviceId;
355
427
  const html = redirectPath
356
428
  ? createRedirectHTML(redirectPath, displayText)
357
429
  : createRedirectHTML('', displayText).replace("window.location.href = ''", "window.location.reload()");
358
- // 5. Response 생성
430
+ // 6. Response 생성
359
431
  const response = new server_1.NextResponse(html, {
360
432
  status: 200,
361
433
  headers: {
362
434
  'Content-Type': 'text/html',
363
435
  },
364
436
  });
365
- // 6. 쿠키 설정
366
- // 자체 토큰 설정
437
+ // 4. 쿠키 설정
367
438
  if (refreshToken) {
368
439
  setCustomTokens(response, accessToken, refreshToken, {
369
440
  cookiePrefix,
@@ -376,15 +447,12 @@ async function createAuthResponse(accessToken, secret, options) {
376
447
  isProduction,
377
448
  });
378
449
  }
379
- // NextAuth 토큰 설정
380
- if (sessionToken) {
381
- setNextAuthToken(response, sessionToken, {
382
- isProduction,
383
- cookieDomain,
384
- });
385
- }
450
+ debugLog('createAuthResponse', 'Custom tokens set successfully');
386
451
  return response;
387
452
  }
453
+ // ============================================================================
454
+ // SSO INTEGRATION FUNCTIONS
455
+ // ============================================================================
388
456
  /**
389
457
  * 서비스 구독 유효성 확인 함수
390
458
  * @param services 서비스 정보 배열
@@ -472,12 +540,16 @@ async function getRefreshTokenFromSSO(userId, accessToken, options) {
472
540
  }
473
541
  return null;
474
542
  }
543
+ // ============================================================================
544
+ // TOKEN REFRESH & VERIFICATION FUNCTIONS
545
+ // ============================================================================
475
546
  async function verifyAndRefreshToken(req, secret, options) {
476
- const { cookiePrefix, serviceId, isProduction, cookieDomain, text, ssoBaseURL, authServiceKey, } = options;
547
+ const { cookiePrefix, serviceId, isProduction, cookieDomain, text, ssoBaseURL, authServiceKey, forceRefresh = false, } = options;
477
548
  // 1. access_token 쿠키 확인
549
+ // forceRefresh가 true이면 access token이 있어도 refresh를 시도
478
550
  const accessTokenName = `${cookiePrefix}_access_token`;
479
551
  const accessToken = req.cookies.get(accessTokenName)?.value;
480
- if (accessToken) {
552
+ if (accessToken && !forceRefresh) {
481
553
  try {
482
554
  const secretBytes = new TextEncoder().encode(secret);
483
555
  const { payload } = await (0, jose_1.jwtVerify)(accessToken, secretBytes);
@@ -492,15 +564,25 @@ async function verifyAndRefreshToken(req, secret, options) {
492
564
  // 리프레시 토큰으로 갱신 시도
493
565
  const refreshTokenName = `${cookiePrefix}_refresh_token`;
494
566
  const refreshToken = req.cookies.get(refreshTokenName)?.value;
567
+ debugLog('verifyAndRefreshToken', 'Checking refresh:', {
568
+ hasRefreshToken: !!refreshToken,
569
+ forceRefresh,
570
+ });
495
571
  if (refreshToken) {
496
572
  try {
497
573
  if (!ssoBaseURL || !authServiceKey) {
574
+ debugLog('verifyAndRefreshToken', 'SSO config missing');
498
575
  return { isValid: false, error: 'SSO_CONFIG_MISSING' };
499
576
  }
577
+ debugLog('verifyAndRefreshToken', 'Attempting token refresh...');
500
578
  const refreshResult = await refreshSSOToken(refreshToken, {
501
579
  ssoBaseURL,
502
580
  authServiceKey,
503
581
  });
582
+ debugLog('verifyAndRefreshToken', 'Refresh result:', {
583
+ success: refreshResult.success,
584
+ hasAccessToken: !!refreshResult.accessToken,
585
+ });
504
586
  if (refreshResult.success && refreshResult.accessToken) {
505
587
  const newRefreshToken = refreshResult.refreshToken || refreshToken;
506
588
  try {
@@ -515,6 +597,7 @@ async function verifyAndRefreshToken(req, secret, options) {
515
597
  catch {
516
598
  // 토큰 검증 실패
517
599
  }
600
+ debugLog('verifyAndRefreshToken', 'Creating auth response...');
518
601
  const response = await createAuthResponse(refreshResult.accessToken, secret, {
519
602
  refreshToken: newRefreshToken,
520
603
  redirectPath: '',
@@ -528,21 +611,29 @@ async function verifyAndRefreshToken(req, secret, options) {
528
611
  return { isValid: true, response, payload };
529
612
  }
530
613
  catch (error) {
531
- console.error('Failed to create auth response:', error);
614
+ debugError('verifyAndRefreshToken', 'Failed to create auth response:', error);
532
615
  return { isValid: false, error: 'SESSION_CREATION_FAILED' };
533
616
  }
534
617
  }
535
618
  else {
536
- 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' };
537
623
  }
538
624
  }
539
625
  catch (error) {
540
- console.error('Token refresh error:', error);
541
- 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' };
542
630
  }
543
631
  }
544
632
  return { isValid: false, error: 'NO_TOKEN' };
545
633
  }
634
+ // ============================================================================
635
+ // HELPER FUNCTIONS
636
+ // ============================================================================
546
637
  /**
547
638
  * 에러 페이지로 리다이렉트하는 헬퍼 함수
548
639
  * @param req NextRequest 객체
@@ -602,6 +693,9 @@ function requiresSubscription(pathname, role, subscriptionRequiredPaths, systemA
602
693
  // 구독이 필요한 경로인지 확인
603
694
  return subscriptionRequiredPaths.some(path => pathname.startsWith(path));
604
695
  }
696
+ // ============================================================================
697
+ // NEXTAUTH CONFIGURATION FUNCTIONS
698
+ // ============================================================================
605
699
  // 유효한 라이센스 키 해시 목록
606
700
  const VALID_LICENSE_KEY_HASHES = new Set([
607
701
  '73bce4f3b64804c255cdab450d759a8b53038f9edb59ae42d9988b08dfd007e2',
@@ -616,12 +710,16 @@ const VALID_LICENSE_KEY_HASHES = new Set([
616
710
  function createNextAuthCookies(options) {
617
711
  const { isProduction = false, cookieDomain } = options;
618
712
  const isSecure = isProduction;
713
+ // cookieDomain이 설정되어 있으면 같은 도메인/서브도메인 간 쿠키 공유를 위해 'lax' 사용
714
+ // cookieDomain이 없고 프로덕션 환경이면 크로스 도메인을 위해 'none' 사용
715
+ // 개발 환경이면 항상 'lax' 사용
716
+ const sameSiteValue = cookieDomain ? 'lax' : (isSecure ? 'none' : 'lax');
619
717
  return {
620
718
  sessionToken: {
621
719
  name: isSecure ? `__Secure-next-auth.session-token` : `next-auth.session-token`,
622
720
  options: {
623
721
  httpOnly: true,
624
- sameSite: isSecure ? 'none' : 'lax',
722
+ sameSite: sameSiteValue,
625
723
  path: '/',
626
724
  secure: isSecure,
627
725
  ...(cookieDomain && { domain: cookieDomain }),
@@ -630,7 +728,7 @@ function createNextAuthCookies(options) {
630
728
  callbackUrl: {
631
729
  name: isSecure ? `__Secure-next-auth.callback-url` : `next-auth.callback-url`,
632
730
  options: {
633
- sameSite: isSecure ? 'none' : 'lax',
731
+ sameSite: sameSiteValue,
634
732
  path: '/',
635
733
  secure: isSecure,
636
734
  ...(cookieDomain && { domain: cookieDomain }),
@@ -640,7 +738,7 @@ function createNextAuthCookies(options) {
640
738
  name: isSecure ? `__Secure-next-auth.csrf-token` : `next-auth.csrf-token`,
641
739
  options: {
642
740
  httpOnly: true,
643
- sameSite: isSecure ? 'none' : 'lax',
741
+ sameSite: sameSiteValue,
644
742
  path: '/',
645
743
  secure: isSecure,
646
744
  ...(cookieDomain && { domain: cookieDomain }),
@@ -767,29 +865,37 @@ async function handleJWTCallback(token, user, account, options) {
767
865
  const { secret, licenseKey, serviceId, cookieName, debug = false, } = options || {};
768
866
  // 디버깅 로그
769
867
  if (debug) {
770
- console.log('[JWT Callback] Token received:', {
868
+ debugLog('handleJWTCallback', 'Token received:', {
771
869
  hasId: !!token.id,
772
870
  hasEmail: !!token.email,
773
871
  hasRole: !!token.role,
774
- tokenKeys: Object.keys(token),
775
872
  });
776
873
  }
777
874
  // 1. 초기 로그인 시 (providers를 통한 로그인)
778
875
  if (account && user) {
876
+ debugLog('handleJWTCallback', 'Initial login, creating token from user data');
779
877
  return createInitialJWTToken(token, user, account);
780
878
  }
781
879
  // 2. 이미 토큰에 정보가 있으면 그대로 사용
782
880
  if (token.id) {
881
+ debugLog('handleJWTCallback', 'Token already has id, using existing token');
783
882
  return token;
784
883
  }
785
884
  // 3. 토큰에 id가 없는 경우 - 커스텀 토큰 쿠키에서 정보 읽기
885
+ debugLog('handleJWTCallback', 'Token has no id, checking custom token cookie');
786
886
  if (secret && licenseKey && serviceId) {
787
887
  const cookieNameToUse = cookieName || `${serviceId}_access_token`;
788
888
  const jwt = await getJWTFromCustomTokenCookie(cookieNameToUse, secret, serviceId, licenseKey);
789
889
  if (jwt) {
890
+ debugLog('handleJWTCallback', 'Successfully created JWT from custom token cookie');
790
891
  return jwt;
791
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');
792
897
  }
898
+ debugLog('handleJWTCallback', 'Returning original token');
793
899
  return token;
794
900
  }
795
901
  /**
@@ -802,27 +908,34 @@ async function handleJWTCallback(token, user, account, options) {
802
908
  * @returns NextAuth JWT 객체 또는 null
803
909
  */
804
910
  async function getJWTFromCustomTokenCookie(cookieName, secret, serviceId, licenseKey) {
911
+ debugLog('getJWTFromCustomTokenCookie', `Reading cookie: ${cookieName}`);
805
912
  try {
806
913
  const { cookies } = await Promise.resolve().then(() => __importStar(require('next/headers')));
807
914
  const cookieStore = await cookies();
808
915
  const accessToken = cookieStore.get(cookieName)?.value;
809
916
  if (!accessToken) {
917
+ debugLog('getJWTFromCustomTokenCookie', 'No access token found in cookies');
810
918
  return null;
811
919
  }
812
920
  await checkLicenseKey(licenseKey);
813
921
  const tokenResult = await verifyToken(accessToken, secret);
814
922
  if (!tokenResult) {
923
+ debugLog('getJWTFromCustomTokenCookie', 'Token verification failed');
815
924
  return null;
816
925
  }
817
926
  const { payload } = tokenResult;
818
927
  const jwt = createNextAuthJWT(payload, serviceId);
928
+ debugLog('getJWTFromCustomTokenCookie', 'JWT created successfully from custom token');
819
929
  return jwt;
820
930
  }
821
931
  catch (error) {
822
- console.error(`[getJWTFromCustomTokenCookie] Failed to read ${cookieName}:`, error);
932
+ debugError('getJWTFromCustomTokenCookie', `Failed to read ${cookieName}:`, error);
823
933
  return null;
824
934
  }
825
935
  }
936
+ // ============================================================================
937
+ // LICENSE & AUTHORIZATION FUNCTIONS
938
+ // ============================================================================
826
939
  async function checkLicenseKey(licenseKey) {
827
940
  if (!licenseKey || licenseKey.length < 10) {
828
941
  throw new Error('License key is required');
@@ -848,6 +961,9 @@ function checkRoleAccess(pathname, role, roleConfig) {
848
961
  // 매칭되는 경로가 없으면 허용
849
962
  return { allowed: true };
850
963
  }
964
+ // ============================================================================
965
+ // REDIRECT FUNCTIONS
966
+ // ============================================================================
851
967
  /**
852
968
  * SSO 로그인 페이지로 리다이렉트하는 헬퍼 함수
853
969
  * @param req NextRequest 객체
@@ -870,6 +986,9 @@ function redirectToRoleDashboard(req, role, rolePaths, defaultPath = '/admin') {
870
986
  const redirectPath = rolePaths[role] || defaultPath;
871
987
  return server_1.NextResponse.redirect(new URL(redirectPath, req.url));
872
988
  }
989
+ // ============================================================================
990
+ // TOKEN VALIDATION UTILITIES
991
+ // ============================================================================
873
992
  /**
874
993
  * 토큰이 만료되었는지 확인하는 함수
875
994
  * @param token NextAuth JWT 객체
@@ -923,6 +1042,9 @@ function hasAnyRole(token, roles, serviceId) {
923
1042
  const effectiveRole = getEffectiveRole(token, serviceId);
924
1043
  return roles.includes(effectiveRole || '');
925
1044
  }
1045
+ // ============================================================================
1046
+ // PATH UTILITIES
1047
+ // ============================================================================
926
1048
  /**
927
1049
  * 공개 경로인지 확인하는 함수
928
1050
  * @param pathname 경로명
@@ -951,6 +1073,9 @@ function isProtectedApiPath(pathname, exemptPaths = []) {
951
1073
  return false;
952
1074
  return !exemptPaths.some(path => pathname.startsWith(path));
953
1075
  }
1076
+ // ============================================================================
1077
+ // AUTHENTICATION CHECK FUNCTIONS
1078
+ // ============================================================================
954
1079
  /**
955
1080
  * NextAuth 토큰과 자체 토큰을 모두 확인하는 통합 인증 체크 함수
956
1081
  * @param req NextRequest 객체
@@ -999,12 +1124,59 @@ async function checkAuthentication(req, secret, options) {
999
1124
  * @returns 인증 결과
1000
1125
  */
1001
1126
  async function verifyAndRefreshTokenWithNextAuth(req, nextAuthToken, secret, options) {
1002
- 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');
1003
1163
  return { isValid: true };
1004
1164
  }
1005
- const authCheck = await verifyAndRefreshToken(req, secret, options);
1006
- 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' };
1007
1176
  }
1177
+ // ============================================================================
1178
+ // MIDDLEWARE CONFIGURATION & HANDLER
1179
+ // ============================================================================
1008
1180
  /**
1009
1181
  * 기본 미들웨어 설정을 생성하는 함수
1010
1182
  * @param config 커스텀 설정 (필수: serviceId 포함)
@@ -1081,6 +1253,7 @@ async function handleMiddleware(req, config, options) {
1081
1253
  try {
1082
1254
  const pathname = req.nextUrl.pathname;
1083
1255
  const { secret, isProduction, cookieDomain, getNextAuthToken, licenseKey } = options;
1256
+ debugLog('handleMiddleware', `Processing: ${pathname}`);
1084
1257
  await checkLicenseKey(licenseKey);
1085
1258
  if (!config.serviceId) {
1086
1259
  throw new Error('serviceId is required in middleware config');
@@ -1092,7 +1265,6 @@ async function handleMiddleware(req, config, options) {
1092
1265
  token = await getNextAuthToken(req);
1093
1266
  }
1094
1267
  else {
1095
- // 기본적으로 secret을 사용하여 토큰 가져오기
1096
1268
  try {
1097
1269
  const { getToken } = await Promise.resolve().then(() => __importStar(require('next-auth/jwt')));
1098
1270
  token = await getToken({ req, secret });
@@ -1101,7 +1273,12 @@ async function handleMiddleware(req, config, options) {
1101
1273
  // NextAuth가 없으면 null 유지
1102
1274
  }
1103
1275
  }
1276
+ debugLog('handleMiddleware', 'Token status:', {
1277
+ hasToken: !!token,
1278
+ hasId: !!token?.id,
1279
+ });
1104
1280
  const effectiveRole = getEffectiveRole(token, serviceId);
1281
+ debugLog('handleMiddleware', `Effective role: ${effectiveRole || 'none'}`);
1105
1282
  // 1. API 요청 처리
1106
1283
  if (pathname.startsWith('/api/')) {
1107
1284
  if (config.authApiPaths.includes(pathname)) {
@@ -1133,6 +1310,7 @@ async function handleMiddleware(req, config, options) {
1133
1310
  if (pathname === '/') {
1134
1311
  const tokenParam = req.nextUrl.searchParams.get('token');
1135
1312
  if (tokenParam) {
1313
+ debugLog('handleMiddleware', 'Processing SSO token from query parameter');
1136
1314
  try {
1137
1315
  // 1. 토큰 검증
1138
1316
  const tokenResult = await verifyToken(tokenParam, secret);
@@ -1143,6 +1321,7 @@ async function handleMiddleware(req, config, options) {
1143
1321
  // 2. 역할 추출
1144
1322
  const defaultRole = Object.keys(config.rolePaths)[0] || 'ADMIN';
1145
1323
  const tokenRole = extractRoleFromPayload(payload, serviceId, defaultRole);
1324
+ debugLog('handleMiddleware', `Extracted role: ${tokenRole}`);
1146
1325
  // 3. Refresh token 가져오기 (서버 간 통신)
1147
1326
  const userId = payload.sub || payload.userId || '';
1148
1327
  const ssoBaseURL = options.ssoBaseURL;
@@ -1152,6 +1331,7 @@ async function handleMiddleware(req, config, options) {
1152
1331
  : '';
1153
1332
  // 4. 자체 토큰 생성 및 쿠키 설정
1154
1333
  const redirectPath = config.rolePaths[tokenRole] || config.rolePaths[defaultRole] || '/admin';
1334
+ debugLog('handleMiddleware', `Creating auth response, redirect to: ${redirectPath}`);
1155
1335
  const response = await createAuthResponse(tokenParam, secret, {
1156
1336
  refreshToken: refreshToken || undefined,
1157
1337
  redirectPath,
@@ -1164,8 +1344,8 @@ async function handleMiddleware(req, config, options) {
1164
1344
  });
1165
1345
  return response;
1166
1346
  }
1167
- catch {
1168
- // 토큰 검증 실패 시 SSO 로그인으로 리다이렉트
1347
+ catch (error) {
1348
+ debugError('handleMiddleware', 'Error processing token:', error);
1169
1349
  const ssoBaseURL = options.ssoBaseURL;
1170
1350
  return redirectToSSOLogin(req, serviceId, ssoBaseURL);
1171
1351
  }
@@ -1265,10 +1445,10 @@ async function handleMiddleware(req, config, options) {
1265
1445
  return server_1.NextResponse.redirect(subscriptionCheck.redirectUrl);
1266
1446
  }
1267
1447
  }
1268
- return null; // 다음 미들웨어로 진행
1448
+ return null;
1269
1449
  }
1270
1450
  catch (error) {
1271
- console.error('Middleware error:', error);
1451
+ debugError('handleMiddleware', 'Middleware error:', error);
1272
1452
  return redirectToError(req, 'INTERNAL_ERROR', '서버 오류가 발생했습니다.', config.errorPath);
1273
1453
  }
1274
1454
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@thinkingcat/auth-utils",
3
- "version": "1.0.16",
4
- "description": "Authentication utilities for ThinkingCat SSO services",
3
+ "version": "1.0.18",
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": {
@@ -40,6 +40,3 @@
40
40
  ".env.example"
41
41
  ]
42
42
  }
43
-
44
-
45
-