@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 +6 -4
- package/dist/index.js +93 -30
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1348
|
+
return NextResponseClass.next();
|
|
1286
1349
|
}
|
|
1287
1350
|
if (config.subscriptionExemptApiPaths.some((path) => pathname.startsWith(path))) {
|
|
1288
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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