@thinkingcat/auth-utils 1.0.18 → 1.0.20

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/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { JWT } from "next-auth/jwt";
2
2
  import type { Session } from "next-auth";
3
- import { NextResponse, NextRequest } from 'next/server';
3
+ import type { NextResponse, NextRequest } from 'next/server';
4
4
  export interface ResponseLike {
5
5
  cookies: {
6
6
  delete(name: string): void;
@@ -239,7 +239,7 @@ export declare function verifyAndRefreshToken(req: NextRequest, secret: string,
239
239
  * @param errorPath 에러 페이지 경로 (기본값: '/error')
240
240
  * @returns NextResponse 리다이렉트 응답
241
241
  */
242
- export declare function redirectToError(req: NextRequest, code: string, message: string, errorPath?: string): NextResponse;
242
+ export declare function redirectToError(req: NextRequest, code: string, message: string, errorPath?: string): Promise<NextResponse>;
243
243
  /**
244
244
  * 인증 쿠키를 삭제하는 헬퍼 함수
245
245
  * @param response NextResponse 객체
@@ -468,6 +468,8 @@ export declare function handleJWTCallback(token: JWT, user?: {
468
468
  serviceId?: string;
469
469
  cookieName?: string;
470
470
  debug?: boolean;
471
+ ssoBaseURL?: string;
472
+ authServiceKey?: string;
471
473
  }): Promise<JWT>;
472
474
  /**
473
475
  * 쿠키에서 커스텀 토큰을 읽어서 NextAuth JWT로 변환하는 헬퍼 함수
@@ -491,7 +493,7 @@ export declare function checkRoleAccess(pathname: string, role: string, roleConf
491
493
  * @param ssoBaseURL SSO 서버 기본 URL (필수)
492
494
  * @returns NextResponse 리다이렉트 응답
493
495
  */
494
- export declare function redirectToSSOLogin(req: NextRequest, serviceId: string, ssoBaseURL: string): NextResponse;
496
+ export declare function redirectToSSOLogin(req: NextRequest, serviceId: string, ssoBaseURL: string): Promise<NextResponse>;
495
497
  /**
496
498
  * 역할별 대시보드 경로로 리다이렉트하는 헬퍼 함수
497
499
  * @param req NextRequest 객체
@@ -500,7 +502,7 @@ export declare function redirectToSSOLogin(req: NextRequest, serviceId: string,
500
502
  * @param defaultPath 기본 경로 (기본값: '/admin')
501
503
  * @returns NextResponse 리다이렉트 응답
502
504
  */
503
- export declare function redirectToRoleDashboard(req: NextRequest, role: string, rolePaths: Record<string, string>, defaultPath?: string): NextResponse;
505
+ export declare function redirectToRoleDashboard(req: NextRequest, role: string, rolePaths: Record<string, string>, defaultPath?: string): Promise<NextResponse>;
504
506
  /**
505
507
  * 토큰이 만료되었는지 확인하는 함수
506
508
  * @param token NextAuth JWT 객체
package/dist/index.js CHANGED
@@ -72,7 +72,6 @@ exports.verifyAndRefreshTokenWithNextAuth = verifyAndRefreshTokenWithNextAuth;
72
72
  exports.createMiddlewareConfig = createMiddlewareConfig;
73
73
  exports.handleMiddleware = handleMiddleware;
74
74
  const jose_1 = require("jose");
75
- const server_1 = require("next/server");
76
75
  // ============================================================================
77
76
  // UTILITY FUNCTIONS
78
77
  // ============================================================================
@@ -99,6 +98,12 @@ async function createHashSHA256(data) {
99
98
  const hashArray = Array.from(new Uint8Array(hashBuffer));
100
99
  return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
101
100
  }
101
+ /**
102
+ * Edge Runtime 호환을 위해 next/server를 동적 import
103
+ */
104
+ async function getNextServer() {
105
+ return await Promise.resolve().then(() => __importStar(require('next/server')));
106
+ }
102
107
  /**
103
108
  * NextAuth 세션 토큰 쿠키를 삭제하는 헬퍼 함수
104
109
  */
@@ -428,7 +433,8 @@ async function createAuthResponse(accessToken, secret, options) {
428
433
  ? createRedirectHTML(redirectPath, displayText)
429
434
  : createRedirectHTML('', displayText).replace("window.location.href = ''", "window.location.reload()");
430
435
  // 6. Response 생성
431
- const response = new server_1.NextResponse(html, {
436
+ const { NextResponse: NextResponseClass } = await getNextServer();
437
+ const response = new NextResponseClass(html, {
432
438
  status: 200,
433
439
  headers: {
434
440
  'Content-Type': 'text/html',
@@ -617,14 +623,16 @@ async function verifyAndRefreshToken(req, secret, options) {
617
623
  }
618
624
  else {
619
625
  debugLog('verifyAndRefreshToken', 'Refresh failed, clearing all cookies');
620
- const response = server_1.NextResponse.next();
626
+ const { NextResponse: NextResponseClass } = await getNextServer();
627
+ const response = NextResponseClass.next();
621
628
  clearAllAuthCookies(response, options.cookiePrefix, options.isProduction);
622
629
  return { isValid: false, response, error: 'REFRESH_FAILED' };
623
630
  }
624
631
  }
625
632
  catch (error) {
626
633
  debugError('verifyAndRefreshToken', 'Token refresh error:', error);
627
- const response = server_1.NextResponse.next();
634
+ const { NextResponse: NextResponseClass } = await getNextServer();
635
+ const response = NextResponseClass.next();
628
636
  clearAllAuthCookies(response, options.cookiePrefix, options.isProduction);
629
637
  return { isValid: false, response, error: 'REFRESH_ERROR' };
630
638
  }
@@ -642,11 +650,12 @@ async function verifyAndRefreshToken(req, secret, options) {
642
650
  * @param errorPath 에러 페이지 경로 (기본값: '/error')
643
651
  * @returns NextResponse 리다이렉트 응답
644
652
  */
645
- function redirectToError(req, code, message, errorPath = '/error') {
653
+ async function redirectToError(req, code, message, errorPath = '/error') {
646
654
  const url = new URL(errorPath, req.url);
647
655
  url.searchParams.set('code', code);
648
656
  url.searchParams.set('message', message);
649
- return server_1.NextResponse.redirect(url);
657
+ const { NextResponse: NextResponseClass } = await getNextServer();
658
+ return NextResponseClass.redirect(url);
650
659
  }
651
660
  /**
652
661
  * 인증 쿠키를 삭제하는 헬퍼 함수
@@ -862,13 +871,14 @@ function mapTokenToSession(session, token) {
862
871
  * @returns 업데이트된 JWT 토큰
863
872
  */
864
873
  async function handleJWTCallback(token, user, account, options) {
865
- const { secret, licenseKey, serviceId, cookieName, debug = false, } = options || {};
874
+ const { secret, licenseKey, serviceId, cookieName, debug = false, ssoBaseURL, authServiceKey, } = options || {};
866
875
  // 디버깅 로그
867
876
  if (debug) {
868
877
  debugLog('handleJWTCallback', 'Token received:', {
869
878
  hasId: !!token.id,
870
879
  hasEmail: !!token.email,
871
880
  hasRole: !!token.role,
881
+ hasExpires: !!token.accessTokenExpires,
872
882
  });
873
883
  }
874
884
  // 1. 초기 로그인 시 (providers를 통한 로그인)
@@ -876,9 +886,58 @@ async function handleJWTCallback(token, user, account, options) {
876
886
  debugLog('handleJWTCallback', 'Initial login, creating token from user data');
877
887
  return createInitialJWTToken(token, user, account);
878
888
  }
879
- // 2. 이미 토큰에 정보가 있으면 그대로 사용
889
+ // 2. 이미 토큰에 정보가 있는 경우 - 만료 체크 및 갱신
880
890
  if (token.id) {
881
- debugLog('handleJWTCallback', 'Token already has id, using existing token');
891
+ const now = Date.now();
892
+ const expires = token.accessTokenExpires;
893
+ // 2-1. 토큰이 만료되지 않았으면 그대로 사용
894
+ if (expires && expires > now) {
895
+ debugLog('handleJWTCallback', 'Token is still valid, using existing token');
896
+ return token;
897
+ }
898
+ // 2-2. 토큰이 만료되었으면 refresh token으로 갱신 시도
899
+ debugLog('handleJWTCallback', 'Token expired or no expiry, attempting refresh');
900
+ const refreshToken = token.refreshToken;
901
+ if (refreshToken && ssoBaseURL && authServiceKey) {
902
+ try {
903
+ debugLog('handleJWTCallback', 'Calling SSO refresh endpoint');
904
+ const response = await fetch(`${ssoBaseURL}/api/sso/refresh`, {
905
+ method: 'POST',
906
+ headers: {
907
+ 'Content-Type': 'application/json',
908
+ 'x-auth-service-key': authServiceKey,
909
+ },
910
+ body: JSON.stringify({ refreshToken }),
911
+ });
912
+ if (response.ok) {
913
+ const result = await response.json();
914
+ if (result.success && result.accessToken) {
915
+ debugLog('handleJWTCallback', 'Successfully refreshed token');
916
+ // 새 액세스 토큰 검증 및 페이로드 추출
917
+ if (secret) {
918
+ const tokenResult = await verifyToken(result.accessToken, secret);
919
+ if (tokenResult) {
920
+ const newJWT = createNextAuthJWT(tokenResult.payload, serviceId || '');
921
+ return {
922
+ ...newJWT,
923
+ refreshToken, // 기존 refresh token 유지
924
+ accessTokenExpires: Date.now() + (15 * 60 * 1000), // 15분
925
+ };
926
+ }
927
+ }
928
+ }
929
+ }
930
+ debugLog('handleJWTCallback', 'Failed to refresh token, SSO response not ok');
931
+ }
932
+ catch (error) {
933
+ console.error('[handleJWTCallback] Error refreshing token:', error);
934
+ }
935
+ }
936
+ else {
937
+ debugLog('handleJWTCallback', 'Missing refresh token or SSO config, cannot refresh');
938
+ }
939
+ // 갱신 실패 시 기존 토큰 반환 (만료되었지만)
940
+ debugLog('handleJWTCallback', 'Returning existing token (possibly expired)');
882
941
  return token;
883
942
  }
884
943
  // 3. 토큰에 id가 없는 경우 - 커스텀 토큰 쿠키에서 정보 읽기
@@ -971,8 +1030,9 @@ function checkRoleAccess(pathname, role, roleConfig) {
971
1030
  * @param ssoBaseURL SSO 서버 기본 URL (필수)
972
1031
  * @returns NextResponse 리다이렉트 응답
973
1032
  */
974
- function redirectToSSOLogin(req, serviceId, ssoBaseURL) {
975
- return server_1.NextResponse.redirect(new URL(`${ssoBaseURL}/auth/login?serviceId=${serviceId}`, req.url));
1033
+ async function redirectToSSOLogin(req, serviceId, ssoBaseURL) {
1034
+ const { NextResponse: NextResponseClass } = await getNextServer();
1035
+ return NextResponseClass.redirect(new URL(`${ssoBaseURL}/auth/login?serviceId=${serviceId}`, req.url));
976
1036
  }
977
1037
  /**
978
1038
  * 역할별 대시보드 경로로 리다이렉트하는 헬퍼 함수
@@ -982,9 +1042,10 @@ function redirectToSSOLogin(req, serviceId, ssoBaseURL) {
982
1042
  * @param defaultPath 기본 경로 (기본값: '/admin')
983
1043
  * @returns NextResponse 리다이렉트 응답
984
1044
  */
985
- function redirectToRoleDashboard(req, role, rolePaths, defaultPath = '/admin') {
1045
+ async function redirectToRoleDashboard(req, role, rolePaths, defaultPath = '/admin') {
986
1046
  const redirectPath = rolePaths[role] || defaultPath;
987
- return server_1.NextResponse.redirect(new URL(redirectPath, req.url));
1047
+ const { NextResponse: NextResponseClass } = await getNextServer();
1048
+ return NextResponseClass.redirect(new URL(redirectPath, req.url));
988
1049
  }
989
1050
  // ============================================================================
990
1051
  // TOKEN VALIDATION UTILITIES
@@ -1250,6 +1311,8 @@ function createMiddlewareConfig(config, defaults) {
1250
1311
  * @returns NextResponse 또는 null (다음 미들웨어로 진행)
1251
1312
  */
1252
1313
  async function handleMiddleware(req, config, options) {
1314
+ // Edge Runtime 호환을 위해 next/server를 한 번만 import
1315
+ const { NextResponse: NextResponseClass } = await getNextServer();
1253
1316
  try {
1254
1317
  const pathname = req.nextUrl.pathname;
1255
1318
  const { secret, isProduction, cookieDomain, getNextAuthToken, licenseKey } = options;
@@ -1282,10 +1345,10 @@ async function handleMiddleware(req, config, options) {
1282
1345
  // 1. API 요청 처리
1283
1346
  if (pathname.startsWith('/api/')) {
1284
1347
  if (config.authApiPaths.includes(pathname)) {
1285
- return server_1.NextResponse.next();
1348
+ return NextResponseClass.next();
1286
1349
  }
1287
1350
  if (config.subscriptionExemptApiPaths.some((path) => pathname.startsWith(path))) {
1288
- return server_1.NextResponse.next();
1351
+ return NextResponseClass.next();
1289
1352
  }
1290
1353
  const authCheck = await verifyAndRefreshTokenWithNextAuth(req, token, secret, {
1291
1354
  cookiePrefix,
@@ -1301,10 +1364,10 @@ async function handleMiddleware(req, config, options) {
1301
1364
  return authCheck.response;
1302
1365
  }
1303
1366
  if (!authCheck.isValid) {
1304
- const response = redirectToError(req, 'UNAUTHORIZED', '인증이 필요합니다.', config.errorPath);
1367
+ const response = await redirectToError(req, 'UNAUTHORIZED', '인증이 필요합니다.', config.errorPath);
1305
1368
  return clearAuthCookies(response, cookiePrefix);
1306
1369
  }
1307
- return server_1.NextResponse.next();
1370
+ return NextResponseClass.next();
1308
1371
  }
1309
1372
  // 2. 루트 경로 처리 - SSO 토큰 처리 (인증 체크보다 먼저!)
1310
1373
  if (pathname === '/') {
@@ -1347,26 +1410,26 @@ async function handleMiddleware(req, config, options) {
1347
1410
  catch (error) {
1348
1411
  debugError('handleMiddleware', 'Error processing token:', error);
1349
1412
  const ssoBaseURL = options.ssoBaseURL;
1350
- return redirectToSSOLogin(req, serviceId, ssoBaseURL);
1413
+ return await redirectToSSOLogin(req, serviceId, ssoBaseURL);
1351
1414
  }
1352
1415
  }
1353
1416
  // 토큰이 없고 이미 인증된 경우 역할별 대시보드로 리다이렉트
1354
1417
  if (token && effectiveRole) {
1355
- return redirectToRoleDashboard(req, effectiveRole, config.rolePaths);
1418
+ return await redirectToRoleDashboard(req, effectiveRole, config.rolePaths);
1356
1419
  }
1357
1420
  // 인증되지 않은 경우 SSO 로그인 페이지로 리다이렉트
1358
1421
  const ssoBaseURL = options.ssoBaseURL;
1359
- return redirectToSSOLogin(req, serviceId, ssoBaseURL);
1422
+ return await redirectToSSOLogin(req, serviceId, ssoBaseURL);
1360
1423
  }
1361
1424
  // 3. 공개 경로 처리
1362
1425
  if (config.publicPaths.some((path) => pathname === path || pathname.startsWith(path))) {
1363
1426
  if (pathname === '/error' || pathname === '/verification') {
1364
- return server_1.NextResponse.next();
1427
+ return NextResponseClass.next();
1365
1428
  }
1366
1429
  if (token && effectiveRole) {
1367
- return redirectToRoleDashboard(req, effectiveRole, config.rolePaths);
1430
+ return await redirectToRoleDashboard(req, effectiveRole, config.rolePaths);
1368
1431
  }
1369
- return server_1.NextResponse.next();
1432
+ return NextResponseClass.next();
1370
1433
  }
1371
1434
  // 4. 인증 체크
1372
1435
  const authCheck = await verifyAndRefreshTokenWithNextAuth(req, token, secret, {
@@ -1384,7 +1447,7 @@ async function handleMiddleware(req, config, options) {
1384
1447
  }
1385
1448
  if (!authCheck.isValid) {
1386
1449
  const ssoBaseURL = options.ssoBaseURL;
1387
- return redirectToSSOLogin(req, serviceId, ssoBaseURL);
1450
+ return await redirectToSSOLogin(req, serviceId, ssoBaseURL);
1388
1451
  }
1389
1452
  // 5. 토큰 확인 및 변환
1390
1453
  let finalToken = token;
@@ -1413,24 +1476,24 @@ async function handleMiddleware(req, config, options) {
1413
1476
  }
1414
1477
  if (!finalToken) {
1415
1478
  const ssoBaseURL = options.ssoBaseURL;
1416
- return redirectToSSOLogin(req, serviceId, ssoBaseURL);
1479
+ return await redirectToSSOLogin(req, serviceId, ssoBaseURL);
1417
1480
  }
1418
1481
  // 6. 토큰 에러 체크
1419
1482
  if (finalToken.error === "RefreshAccessTokenError") {
1420
1483
  const ssoBaseURL = options.ssoBaseURL;
1421
- return redirectToSSOLogin(req, serviceId, ssoBaseURL);
1484
+ return await redirectToSSOLogin(req, serviceId, ssoBaseURL);
1422
1485
  }
1423
1486
  // 7. 토큰 유효성 체크
1424
1487
  if (!finalToken.role || !finalToken.email) {
1425
1488
  const ssoBaseURL = options.ssoBaseURL;
1426
- return redirectToSSOLogin(req, serviceId, ssoBaseURL);
1489
+ return await redirectToSSOLogin(req, serviceId, ssoBaseURL);
1427
1490
  }
1428
1491
  // 8. 역할 기반 접근 제어
1429
1492
  const finalEffectiveRole = finalToken.role || getEffectiveRole(finalToken, serviceId) || effectiveRole || '';
1430
1493
  if (config.roleAccessConfig && Object.keys(config.roleAccessConfig).length > 0 && finalEffectiveRole) {
1431
1494
  const roleCheck = checkRoleAccess(pathname, finalEffectiveRole, config.roleAccessConfig);
1432
1495
  if (!roleCheck.allowed) {
1433
- return redirectToError(req, 'ACCESS_DENIED', roleCheck.message || '접근 권한이 없습니다.', config.errorPath);
1496
+ return await redirectToError(req, 'ACCESS_DENIED', roleCheck.message || '접근 권한이 없습니다.', config.errorPath);
1434
1497
  }
1435
1498
  }
1436
1499
  // 9. 구독 상태 확인 (시스템 관리자 제외)
@@ -1442,13 +1505,13 @@ async function handleMiddleware(req, config, options) {
1442
1505
  }
1443
1506
  const subscriptionCheck = validateServiceSubscription(services, serviceId, ssoBaseURL);
1444
1507
  if (!subscriptionCheck.isValid) {
1445
- return server_1.NextResponse.redirect(subscriptionCheck.redirectUrl);
1508
+ return NextResponseClass.redirect(subscriptionCheck.redirectUrl);
1446
1509
  }
1447
1510
  }
1448
1511
  return null;
1449
1512
  }
1450
1513
  catch (error) {
1451
1514
  debugError('handleMiddleware', 'Middleware error:', error);
1452
- return redirectToError(req, 'INTERNAL_ERROR', '서버 오류가 발생했습니다.', config.errorPath);
1515
+ return await redirectToError(req, 'INTERNAL_ERROR', '서버 오류가 발생했습니다.', config.errorPath);
1453
1516
  }
1454
1517
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinkingcat/auth-utils",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "description": "Authentication utilities for ThinkingCat SSO services with conditional logging",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",