@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 +219 -15
- package/dist/index.d.ts +39 -20
- package/dist/index.js +282 -78
- package/package.json +2 -2
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.
|
|
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
|
|
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"
|
|
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.
|
|
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
|
|
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
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
309
|
+
// createNextAuthCookies와 동일한 로직 사용
|
|
310
|
+
const cookies = createNextAuthCookies({ isProduction, cookieDomain });
|
|
311
|
+
const cookieName = cookies.sessionToken.name;
|
|
246
312
|
response.cookies.delete(cookieName);
|
|
247
|
-
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
548
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
982
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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": {
|