@thinkingcat/auth-utils 1.0.4

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 ADDED
@@ -0,0 +1,950 @@
1
+ # @thinkingcat/auth-token-utils
2
+
3
+ ThinkingCat SSO 서비스를 위한 인증 토큰 유틸리티 패키지입니다. JWT 토큰 검증, NextAuth 세션 생성, 쿠키 설정 등의 기능을 제공합니다.
4
+
5
+ ## 📑 목차 (Table of Contents)
6
+
7
+ - [📦 설치 (Installation)](#-설치-installation)
8
+ - [📋 요구사항 (Requirements)](#-요구사항-requirements)
9
+ - [⚙️ Next.js 설정 (Next.js Configuration)](#️-nextjs-설정-nextjs-configuration)
10
+ - [🚀 빠른 시작 (Quick Start)](#-빠른-시작-quick-start)
11
+ - [📚 주요 기능 (Features)](#-주요-기능-features)
12
+ - [🔧 API 레퍼런스](#-api-레퍼런스)
13
+ - [1. verifyToken](#1-verifytokenaccesstoken-string-secret-string)
14
+ - [2. extractRoleFromPayload](#2-extractrolefrompayloadpayload-jwtpayload-serviceid-string-defaultrole-string)
15
+ - [3. createNextAuthJWT](#3-createnextauthjwtpayload-jwtpayload-includeacademies-boolean)
16
+ - [4. encodeNextAuthToken](#4-encodenextauthtokenjwt-jwt-secret-string-maxage-number)
17
+ - [5. setCustomTokens](#5-setcustomtokensresponse-responselike-accesstoken-string-optionsorrefreshtoken-options)
18
+ - [6. setNextAuthToken](#6-setnextauthtokenresponse-responselike-sessiontoken-string-options)
19
+ - [7. createRedirectHTML](#7-createredirecthtmlredirectpath-string-text-string)
20
+ - [8. createAuthResponse](#8-createauthresponseaccesstoken-string-secret-string-options)
21
+ - [💡 사용 시나리오](#-사용-시나리오)
22
+ - [시나리오 1: 자체 토큰만 사용하는 서비스](#시나리오-1-자체-토큰만-사용하는-서비스)
23
+ - [시나리오 2: NextAuth만 사용하는 서비스](#시나리오-2-nextauth만-사용하는-서비스)
24
+ - [시나리오 3: 자체 토큰 + NextAuth 모두 사용 (check-on 예시)](#시나리오-3-자체-토큰--nextauth-모두-사용-check-on-예시)
25
+ - [시나리오 4: Middleware에서 사용](#시나리오-4-middleware에서-사용)
26
+ - [🔒 보안 고려사항](#-보안-고려사항)
27
+ - [📝 타입 정의](#-타입-정의)
28
+ - [🐛 문제 해결 (Troubleshooting)](#-문제-해결-troubleshooting)
29
+ - [📦 패키지 정보](#-패키지-정보)
30
+ - [🤝 기여 (Contributing)](#-기여-contributing)
31
+ - [📄 라이선스 (License)](#-라이선스-license)
32
+
33
+ ## 📦 설치 (Installation)
34
+
35
+ ### npm 사용
36
+
37
+ ```bash
38
+ npm install @thinkingcat/auth-token-utils
39
+ ```
40
+
41
+ ### yarn 사용
42
+
43
+ ```bash
44
+ yarn add @thinkingcat/auth-token-utils
45
+ ```
46
+
47
+ ### pnpm 사용
48
+
49
+ ```bash
50
+ pnpm add @thinkingcat/auth-token-utils
51
+ ```
52
+
53
+ ## 📋 요구사항 (Requirements)
54
+
55
+ - **Next.js**: >= 13.0.0
56
+ - **Node.js**: >= 18.0.0
57
+ - **TypeScript**: 권장 (타입 지원)
58
+
59
+ ## ⚙️ Next.js 설정 (Next.js Configuration)
60
+
61
+ ### npm에 배포된 패키지를 사용하는 경우 (권장)
62
+
63
+ npm에 배포된 패키지를 사용할 때는 **별도의 설정이 필요하지 않습니다**. 바로 사용할 수 있습니다.
64
+
65
+ ```bash
66
+ npm install @thinkingcat/auth-token-utils
67
+ ```
68
+
69
+ ```json
70
+ {
71
+ "dependencies": {
72
+ "@thinkingcat/auth-token-utils": "^1.0.0"
73
+ }
74
+ }
75
+ ```
76
+
77
+ **설정 불필요**: npm에 배포된 패키지는 이미 빌드되어 있으므로 `next.config.ts`에 추가 설정이 필요 없습니다.
78
+
79
+ ### 로컬 패키지를 사용하는 경우 (개발 중)
80
+
81
+ 로컬 패키지(`file:../packages/auth-token-utils`)를 사용하는 경우, Next.js가 모듈을 찾지 못하는 오류가 발생할 수 있습니다.
82
+
83
+ **해결 방법 1: transpilePackages 추가 (권장)**
84
+
85
+ `next.config.ts`에 다음 설정을 추가하세요:
86
+
87
+ ```typescript
88
+ /** @type {import('next').NextConfig} */
89
+ const nextConfig = {
90
+ transpilePackages: ["@thinkingcat/auth-token-utils"],
91
+ // ... 기타 설정
92
+ };
93
+
94
+ module.exports = nextConfig;
95
+ ```
96
+
97
+ **왜 필요한가?**
98
+
99
+ - Next.js는 기본적으로 `node_modules`의 패키지를 트랜스파일하지 않습니다
100
+ - 로컬 패키지를 사용할 때는 Next.js가 소스 코드를 직접 처리해야 할 수 있습니다
101
+ - `transpilePackages`는 Next.js에게 해당 패키지를 트랜스파일하라고 지시합니다
102
+
103
+ **해결 방법 2: npm에 배포된 버전 사용 (가장 안정적)**
104
+
105
+ 로컬 패키지 대신 npm에 배포된 버전을 사용하면 설정이 필요 없습니다:
106
+
107
+ ```json
108
+ {
109
+ "dependencies": {
110
+ "@thinkingcat/auth-token-utils": "^1.0.0"
111
+ }
112
+ }
113
+ ```
114
+
115
+ **권장사항**:
116
+
117
+ - **프로덕션 환경**: npm에 배포된 버전 사용 (설정 불필요)
118
+ - **개발 환경**: 로컬 패키지 사용 시 `transpilePackages` 추가
119
+
120
+ ## 🚀 빠른 시작 (Quick Start)
121
+
122
+ ### 1. 기본 import
123
+
124
+ ```typescript
125
+ import {
126
+ verifyToken,
127
+ extractRoleFromPayload,
128
+ createNextAuthJWT,
129
+ encodeNextAuthToken,
130
+ setCustomTokens,
131
+ setNextAuthToken,
132
+ createRedirectHTML,
133
+ createAuthResponse,
134
+ ServiceInfo,
135
+ ResponseLike,
136
+ JWTPayload,
137
+ } from "@thinkingcat/auth-token-utils";
138
+ ```
139
+
140
+ ### 2. 환경 변수 설정
141
+
142
+ `.env.local` 파일에 다음 환경 변수를 설정하세요:
143
+
144
+ ```env
145
+ NEXTAUTH_SECRET=your-secret-key-here
146
+ NODE_ENV=production # 또는 development
147
+ COOKIE_DOMAIN=.yourdomain.com # 선택사항
148
+ ```
149
+
150
+ ## 📚 주요 기능 (Features)
151
+
152
+ 이 패키지는 다음 기능을 제공합니다:
153
+
154
+ 1. **토큰 검증**: JWT access token 검증 및 디코딩
155
+ 2. **역할 추출**: payload에서 서비스별 역할 추출
156
+ 3. **NextAuth JWT 생성**: NextAuth 호환 JWT 객체 생성
157
+ 4. **세션 토큰 인코딩**: NextAuth 세션 토큰 생성
158
+ 5. **쿠키 설정**: 자체 토큰 및 NextAuth 토큰 쿠키 설정
159
+ 6. **리다이렉트 HTML**: 클라이언트 리다이렉트용 HTML 생성
160
+ 7. **완전한 인증 응답**: 모든 인증 단계를 한 번에 처리
161
+
162
+ ## 🔧 API 레퍼런스
163
+
164
+ ### 1. `verifyToken(accessToken: string, secret: string)`
165
+
166
+ JWT access token을 검증하고 디코딩합니다.
167
+
168
+ **파라미터:**
169
+
170
+ - `accessToken`: 검증할 JWT 토큰
171
+ - `secret`: JWT 서명에 사용할 secret key
172
+
173
+ **반환값:**
174
+
175
+ - 성공: `{ payload: JWTPayload }`
176
+ - 실패: `null`
177
+
178
+ **사용 예시:**
179
+
180
+ ```typescript
181
+ const secret = process.env.NEXTAUTH_SECRET!;
182
+ const result = await verifyToken(accessToken, secret);
183
+
184
+ if (result) {
185
+ const { payload } = result;
186
+ console.log("User email:", payload.email);
187
+ console.log("User ID:", payload.sub || payload.id);
188
+ } else {
189
+ console.log("Invalid token");
190
+ }
191
+ ```
192
+
193
+ ### 2. `extractRoleFromPayload(payload: JWTPayload, serviceId?: string, defaultRole?: string)`
194
+
195
+ payload에서 특정 서비스의 역할을 추출합니다.
196
+
197
+ **파라미터:**
198
+
199
+ - `payload`: JWT payload 객체 (JWTPayload 타입)
200
+ - `serviceId`: 서비스 ID (기본값: 'checkon')
201
+ - `defaultRole`: 기본 역할 (기본값: 'ADMIN')
202
+
203
+ **반환값:**
204
+
205
+ - 추출된 역할 문자열
206
+
207
+ **사용 예시:**
208
+
209
+ ```typescript
210
+ const role = extractRoleFromPayload(payload, "checkon", "ADMIN");
211
+ // 'ADMIN', 'TEACHER', 'STUDENT' 등
212
+ ```
213
+
214
+ ### 3. `createNextAuthJWT(payload: JWTPayload, includeAcademies?: boolean)`
215
+
216
+ NextAuth와 호환되는 JWT 객체를 생성합니다.
217
+
218
+ **파라미터:**
219
+
220
+ - `payload`: 원본 JWT payload (JWTPayload 타입)
221
+ - `includeAcademies`: academies 정보 포함 여부 (기본값: false)
222
+
223
+ **반환값:**
224
+
225
+ - NextAuth JWT 객체
226
+
227
+ **사용 예시:**
228
+
229
+ ```typescript
230
+ const jwt = createNextAuthJWT(payload, true);
231
+ // academies 정보가 포함된 JWT 객체
232
+ ```
233
+
234
+ ### 4. `encodeNextAuthToken(jwt: JWT, secret: string, maxAge?: number)`
235
+
236
+ NextAuth JWT 객체를 인코딩된 세션 토큰으로 변환합니다.
237
+
238
+ **파라미터:**
239
+
240
+ - `jwt`: NextAuth JWT 객체
241
+ - `secret`: JWT 서명에 사용할 secret key
242
+ - `maxAge`: 토큰 유효 기간 (초, 기본값: 30일)
243
+
244
+ **반환값:**
245
+
246
+ - 인코딩된 세션 토큰 문자열
247
+
248
+ **사용 예시:**
249
+
250
+ ```typescript
251
+ const secret = process.env.NEXTAUTH_SECRET!;
252
+ const jwt = createNextAuthJWT(payload);
253
+ const sessionToken = await encodeNextAuthToken(jwt, secret);
254
+ ```
255
+
256
+ ### 5. `setCustomTokens(response: ResponseLike, accessToken: string, optionsOrRefreshToken?, options?)`
257
+
258
+ 자체 토큰(access token, refresh token)만 쿠키에 설정합니다.
259
+
260
+ **파라미터:**
261
+
262
+ - `response`: NextResponse 객체 (또는 ResponseLike 인터페이스를 구현한 객체)
263
+ - `accessToken`: access token 문자열
264
+ - `optionsOrRefreshToken`:
265
+ - 문자열인 경우: refresh token
266
+ - 객체인 경우: 옵션 객체 `{ refreshToken?, cookiePrefix?, isProduction? }`
267
+ - `options`: 옵션 객체 (refreshToken이 문자열로 전달된 경우)
268
+
269
+ **옵션:**
270
+
271
+ - `cookiePrefix`: 쿠키 이름 접두사 (기본값: 'checkon')
272
+ - `isProduction`: 프로덕션 환경 여부 (기본값: false)
273
+ - `refreshToken`: refresh token (옵션 객체 내부에 포함 가능)
274
+
275
+ **사용 예시:**
276
+
277
+ **방법 1: refreshToken이 있는 경우**
278
+
279
+ ```typescript
280
+ setCustomTokens(response, accessToken, refreshToken, {
281
+ cookiePrefix: "myservice",
282
+ isProduction: process.env.NODE_ENV === "production",
283
+ });
284
+ ```
285
+
286
+ **방법 2: refreshToken이 없는 경우**
287
+
288
+ ```typescript
289
+ setCustomTokens(response, accessToken, {
290
+ cookiePrefix: "myservice",
291
+ isProduction: process.env.NODE_ENV === "production",
292
+ });
293
+ ```
294
+
295
+ **방법 3: 옵션 객체에 refreshToken 포함**
296
+
297
+ ```typescript
298
+ setCustomTokens(response, accessToken, {
299
+ refreshToken: refreshToken,
300
+ cookiePrefix: "myservice",
301
+ isProduction: true,
302
+ });
303
+ ```
304
+
305
+ **설정되는 쿠키:**
306
+
307
+ - `{cookiePrefix}_access_token`: 15분 유효
308
+ - `{cookiePrefix}_refresh_token`: 30일 유효 (있는 경우)
309
+
310
+ ### 6. `setNextAuthToken(response: ResponseLike, sessionToken: string, options?)`
311
+
312
+ NextAuth 세션 토큰만 쿠키에 설정합니다.
313
+
314
+ **파라미터:**
315
+
316
+ - `response`: NextResponse 객체
317
+ - `sessionToken`: NextAuth 세션 토큰
318
+ - `options`: 옵션 객체
319
+
320
+ **옵션:**
321
+
322
+ - `isProduction`: 프로덕션 환경 여부 (기본값: false)
323
+ - `cookieDomain`: 쿠키 도메인 (선택사항)
324
+
325
+ **사용 예시:**
326
+
327
+ ```typescript
328
+ setNextAuthToken(response, sessionToken, {
329
+ isProduction: process.env.NODE_ENV === "production",
330
+ cookieDomain: process.env.COOKIE_DOMAIN,
331
+ });
332
+ ```
333
+
334
+ **설정되는 쿠키:**
335
+
336
+ - 개발: `next-auth.session-token`
337
+ - 프로덕션: `__Secure-next-auth.session-token`
338
+ - 유효 기간: 30일
339
+
340
+ ### 7. `createRedirectHTML(redirectPath: string, text?: string)`
341
+
342
+ 클라이언트 리다이렉트용 HTML을 생성합니다.
343
+
344
+ **파라미터:**
345
+
346
+ - `redirectPath`: 리다이렉트할 경로
347
+ - `text`: 표시할 텍스트 (기본값: 'checkon')
348
+
349
+ **반환값:**
350
+
351
+ - HTML 문자열
352
+
353
+ **사용 예시:**
354
+
355
+ ```typescript
356
+ const html = createRedirectHTML("/dashboard", "checkon");
357
+ return new NextResponse(html, {
358
+ status: 200,
359
+ headers: { "Content-Type": "text/html" },
360
+ });
361
+ ```
362
+
363
+ ### 8. `createAuthResponse(accessToken: string, secret: string, options?)`
364
+
365
+ 완전한 인증 세션을 생성합니다. 토큰 검증부터 쿠키 설정까지 모든 과정을 처리합니다.
366
+
367
+ **파라미터:**
368
+
369
+ - `accessToken`: access token
370
+ - `secret`: JWT 서명에 사용할 secret key
371
+ - `options`: 옵션 객체
372
+
373
+ **옵션:**
374
+
375
+ - `refreshToken`: refresh token (선택)
376
+ - `redirectPath`: 리다이렉트할 경로 (선택)
377
+ - `text`: 리다이렉트 HTML에 표시할 텍스트 (기본값: 'checkon')
378
+ - `cookiePrefix`: 쿠키 이름 접두사 (기본값: 'checkon')
379
+ - `isProduction`: 프로덕션 환경 여부 (기본값: false)
380
+ - `cookieDomain`: 쿠키 도메인 (선택)
381
+
382
+ **반환값:**
383
+
384
+ - NextResponse 객체
385
+
386
+ **사용 예시:**
387
+
388
+ ```typescript
389
+ const secret = process.env.NEXTAUTH_SECRET!;
390
+ const response = await createAuthResponse(accessToken, secret, {
391
+ refreshToken: refreshToken,
392
+ redirectPath: "/dashboard",
393
+ text: "checkon",
394
+ cookiePrefix: "checkon",
395
+ isProduction: process.env.NODE_ENV === "production",
396
+ cookieDomain: process.env.COOKIE_DOMAIN,
397
+ });
398
+ ```
399
+
400
+ ## 💡 사용 시나리오
401
+
402
+ ### 시나리오 1: 자체 토큰만 사용하는 서비스
403
+
404
+ SSO에서 받은 토큰을 자체 쿠키에만 저장하는 경우:
405
+
406
+ #### 1-1. URL 파라미터로 토큰을 받는 경우 (초기 로그인)
407
+
408
+ ```typescript
409
+ import { NextRequest, NextResponse } from "next/server";
410
+ import {
411
+ verifyToken,
412
+ setCustomTokens,
413
+ createRedirectHTML,
414
+ JWTPayload,
415
+ } from "@thinkingcat/auth-token-utils";
416
+
417
+ export async function GET(req: NextRequest) {
418
+ // URL 파라미터에서 토큰 가져오기 (초기 로그인 시)
419
+ const tokenParam = req.nextUrl.searchParams.get("token");
420
+
421
+ if (!tokenParam) {
422
+ return NextResponse.redirect("/login");
423
+ }
424
+
425
+ // 1. 토큰 검증 (중요: 모든 토큰은 사용 전에 반드시 검증해야 합니다)
426
+ const secret = process.env.NEXTAUTH_SECRET!;
427
+ const tokenResult = await verifyToken(tokenParam, secret);
428
+
429
+ // 2. 검증 실패 시 로그인 페이지로 리다이렉트
430
+ if (!tokenResult) {
431
+ console.error("Token verification failed");
432
+ return NextResponse.redirect("/login");
433
+ }
434
+
435
+ // 3. 검증된 payload 확인 (선택적: 사용자 정보가 필요한 경우)
436
+ const { payload } = tokenResult;
437
+ console.log("Authenticated user:", payload.email);
438
+
439
+ // 4. 검증된 토큰을 쿠키에 저장
440
+ // 주의: verifyToken이 성공했다는 것은 토큰이 유효하고 서명이 올바르다는 의미입니다
441
+ // 따라서 원본 토큰을 안전하게 쿠키에 저장할 수 있습니다
442
+ const html = createRedirectHTML("/dashboard", "myservice");
443
+ const response = new NextResponse(html, {
444
+ status: 200,
445
+ headers: { "Content-Type": "text/html" },
446
+ });
447
+
448
+ setCustomTokens(response, tokenParam, {
449
+ cookiePrefix: "myservice",
450
+ isProduction: process.env.NODE_ENV === "production",
451
+ });
452
+
453
+ return response;
454
+ }
455
+ ```
456
+
457
+ #### 1-2. 쿠키에서 토큰을 읽어서 검증하는 경우 (이미 로그인된 상태)
458
+
459
+ ```typescript
460
+ import { NextRequest, NextResponse } from "next/server";
461
+ import { verifyToken, JWTPayload } from "@thinkingcat/auth-token-utils";
462
+
463
+ export async function GET(req: NextRequest) {
464
+ const secret = process.env.NEXTAUTH_SECRET!;
465
+
466
+ // 1. 쿠키에서 토큰 가져오기 (이미 로그인된 경우)
467
+ const cookieToken = req.cookies.get("myservice_access_token")?.value;
468
+
469
+ if (!cookieToken) {
470
+ // 토큰이 없으면 로그인 페이지로 리다이렉트
471
+ return NextResponse.redirect("/login");
472
+ }
473
+
474
+ // 2. 쿠키의 토큰 검증 (중요: 쿠키의 토큰도 반드시 검증해야 합니다)
475
+ const tokenResult = await verifyToken(cookieToken, secret);
476
+
477
+ if (!tokenResult) {
478
+ // 검증 실패 시 쿠키 삭제하고 로그인 페이지로 리다이렉트
479
+ const response = NextResponse.redirect("/login");
480
+ response.cookies.delete("myservice_access_token");
481
+ response.cookies.delete("myservice_refresh_token");
482
+ return response;
483
+ }
484
+
485
+ // 3. 검증 성공 - 사용자 정보 사용
486
+ const { payload } = tokenResult;
487
+ console.log("Authenticated user:", payload.email);
488
+
489
+ // 4. 정상 응답 반환
490
+ return NextResponse.json({
491
+ success: true,
492
+ user: {
493
+ email: payload.email,
494
+ name: payload.name,
495
+ },
496
+ });
497
+ }
498
+ ```
499
+
500
+ **토큰 검증 로직 설명:**
501
+
502
+ 1. **`verifyToken(token, secret)`**:
503
+
504
+ - JWT 토큰의 서명을 검증합니다
505
+ - 토큰이 만료되지 않았는지 확인합니다
506
+ - 토큰의 형식이 올바른지 확인합니다
507
+ - 검증 성공 시 `{ payload: JWTPayload }` 반환
508
+ - 검증 실패 시 `null` 반환
509
+
510
+ 2. **토큰 소스별 처리**:
511
+
512
+ - **URL 파라미터**: 초기 로그인 시 SSO에서 받은 토큰을 검증 후 쿠키에 저장
513
+ - **쿠키**: 이미 로그인된 상태에서 쿠키의 토큰을 검증하여 사용
514
+ - **중요**: 쿠키에 저장된 토큰도 매번 검증해야 합니다 (토큰이 만료되었을 수 있음)
515
+
516
+ 3. **검증 후 처리**:
517
+ - 검증이 성공하면 토큰이 유효하다는 것이 보장됩니다
518
+ - URL 파라미터로 받은 토큰은 검증 후 쿠키에 저장할 수 있습니다
519
+ - 쿠키에서 읽은 토큰은 검증 후 사용할 수 있습니다
520
+ - 필요시 `payload`에서 사용자 정보를 추출하여 사용할 수 있습니다
521
+
522
+ ### 시나리오 2: NextAuth만 사용하는 서비스
523
+
524
+ NextAuth 세션만 사용하는 경우:
525
+
526
+ ```typescript
527
+ import { NextRequest, NextResponse } from "next/server";
528
+ import {
529
+ verifyToken,
530
+ createNextAuthJWT,
531
+ encodeNextAuthToken,
532
+ setNextAuthToken,
533
+ } from "@thinkingcat/auth-token-utils";
534
+
535
+ export async function GET(req: NextRequest) {
536
+ const tokenParam = req.nextUrl.searchParams.get("token");
537
+
538
+ if (!tokenParam) {
539
+ return NextResponse.redirect("/login");
540
+ }
541
+
542
+ const secret = process.env.NEXTAUTH_SECRET!;
543
+ const tokenResult = await verifyToken(tokenParam, secret);
544
+
545
+ if (!tokenResult) {
546
+ return NextResponse.redirect("/login");
547
+ }
548
+
549
+ const { payload } = tokenResult;
550
+ const jwt = createNextAuthJWT(payload);
551
+ const sessionToken = await encodeNextAuthToken(jwt, secret);
552
+
553
+ const response = NextResponse.redirect("/dashboard");
554
+ setNextAuthToken(response, sessionToken, {
555
+ isProduction: process.env.NODE_ENV === "production",
556
+ cookieDomain: process.env.COOKIE_DOMAIN,
557
+ });
558
+
559
+ return response;
560
+ }
561
+ ```
562
+
563
+ ### 시나리오 3: 자체 토큰 + NextAuth 모두 사용 (check-on 예시)
564
+
565
+ 자체 토큰과 NextAuth 세션을 모두 사용하는 경우:
566
+
567
+ ```typescript
568
+ import { NextRequest, NextResponse } from "next/server";
569
+ import {
570
+ verifyToken,
571
+ extractRoleFromPayload,
572
+ createNextAuthJWT,
573
+ encodeNextAuthToken,
574
+ setCustomTokens,
575
+ setNextAuthToken,
576
+ createRedirectHTML,
577
+ } from "@thinkingcat/auth-token-utils";
578
+
579
+ export async function GET(req: NextRequest) {
580
+ const tokenParam = req.nextUrl.searchParams.get("token");
581
+
582
+ if (!tokenParam) {
583
+ return NextResponse.redirect("/login");
584
+ }
585
+
586
+ const secret = process.env.NEXTAUTH_SECRET!;
587
+ const isProduction = process.env.NODE_ENV === "production";
588
+
589
+ // 1. 토큰 검증
590
+ const tokenResult = await verifyToken(tokenParam, secret);
591
+ if (!tokenResult) {
592
+ return NextResponse.redirect("/login");
593
+ }
594
+
595
+ const { payload } = tokenResult;
596
+
597
+ // 2. 역할 추출
598
+ const role = extractRoleFromPayload(payload, "checkon", "ADMIN");
599
+
600
+ // 3. Refresh token 가져오기 (SSO API 호출 등)
601
+ let refreshToken = "";
602
+ // ... refreshToken 가져오는 로직 ...
603
+
604
+ // 4. NextAuth JWT 생성 및 인코딩
605
+ const jwt = createNextAuthJWT(payload);
606
+ const sessionToken = await encodeNextAuthToken(jwt, secret);
607
+
608
+ // 5. 역할별 리다이렉트 경로 결정
609
+ const redirectPath = role === "ADMIN" ? "/admin" : "/dashboard";
610
+ const html = createRedirectHTML(redirectPath, "checkon");
611
+
612
+ const response = new NextResponse(html, {
613
+ status: 200,
614
+ headers: { "Content-Type": "text/html" },
615
+ });
616
+
617
+ // 6. 자체 토큰 설정
618
+ setCustomTokens(response, tokenParam, refreshToken, {
619
+ cookiePrefix: "checkon",
620
+ isProduction,
621
+ });
622
+
623
+ // 7. NextAuth 토큰 설정
624
+ setNextAuthToken(response, sessionToken, {
625
+ isProduction,
626
+ cookieDomain: process.env.COOKIE_DOMAIN,
627
+ });
628
+
629
+ return response;
630
+ }
631
+ ```
632
+
633
+ ### 시나리오 4: Middleware에서 사용
634
+
635
+ Next.js Middleware에서 토큰 검증 및 리프레시:
636
+
637
+ ```typescript
638
+ import { NextRequest, NextResponse } from "next/server";
639
+ import {
640
+ verifyToken,
641
+ setCustomTokens,
642
+ setNextAuthToken,
643
+ createNextAuthJWT,
644
+ encodeNextAuthToken,
645
+ } from "@thinkingcat/auth-token-utils";
646
+
647
+ export async function middleware(req: NextRequest) {
648
+ const secret = process.env.NEXTAUTH_SECRET!;
649
+ const isProduction = process.env.NODE_ENV === "production";
650
+
651
+ // 1. 자체 토큰 확인
652
+ const accessToken = req.cookies.get("checkon_access_token")?.value;
653
+
654
+ if (accessToken) {
655
+ const tokenResult = await verifyToken(accessToken, secret);
656
+
657
+ if (tokenResult) {
658
+ // 토큰이 유효함
659
+ return NextResponse.next();
660
+ }
661
+
662
+ // 토큰이 만료됨 - 리프레시 시도
663
+ const refreshToken = req.cookies.get("checkon_refresh_token")?.value;
664
+
665
+ if (refreshToken) {
666
+ // SSO API로 토큰 리프레시
667
+ // ... refreshSSOToken 호출 ...
668
+
669
+ if (newAccessToken) {
670
+ const newTokenResult = await verifyToken(newAccessToken, secret);
671
+
672
+ if (newTokenResult) {
673
+ const { payload } = newTokenResult;
674
+ const jwt = createNextAuthJWT(payload);
675
+ const sessionToken = await encodeNextAuthToken(jwt, secret);
676
+
677
+ const response = NextResponse.next();
678
+
679
+ setCustomTokens(response, newAccessToken, {
680
+ cookiePrefix: "checkon",
681
+ isProduction,
682
+ });
683
+
684
+ setNextAuthToken(response, sessionToken, {
685
+ isProduction,
686
+ cookieDomain: process.env.COOKIE_DOMAIN,
687
+ });
688
+
689
+ return response;
690
+ }
691
+ }
692
+ }
693
+ }
694
+
695
+ // 인증 실패 - 로그인 페이지로 리다이렉트
696
+ return NextResponse.redirect(new URL("/login", req.url));
697
+ }
698
+ ```
699
+
700
+ ## 🔒 보안 고려사항
701
+
702
+ ### ⚠️ 중요 사항
703
+
704
+ 1. **Secret Key 관리**
705
+
706
+ - `NEXTAUTH_SECRET`은 반드시 환경 변수로 관리하세요
707
+ - 절대 코드에 하드코딩하지 마세요
708
+ - 프로덕션과 개발 환경에서 다른 secret을 사용하세요
709
+
710
+ 2. **쿠키 보안**
711
+
712
+ - 프로덕션 환경에서는 `isProduction: true`를 설정하세요
713
+ - `secure: true`로 설정되어 HTTPS에서만 전송됩니다
714
+ - `httpOnly: true`로 설정되어 JavaScript에서 접근할 수 없습니다
715
+
716
+ 3. **토큰 검증**
717
+ - 모든 토큰은 사용 전에 반드시 `verifyToken`으로 검증하세요
718
+ - 검증 실패 시 적절한 에러 처리를 하세요
719
+
720
+ ## 📝 타입 정의
721
+
722
+ ### `JWTPayload`
723
+
724
+ JWT 토큰의 payload 타입입니다.
725
+
726
+ ```typescript
727
+ interface JWTPayload {
728
+ // 표준 JWT 필드
729
+ sub?: string; // Subject (사용자 ID)
730
+ id?: string; // 사용자 ID
731
+ email: string; // 이메일
732
+ name: string; // 이름
733
+ role?: string; // 역할
734
+ iat?: number; // Issued At
735
+ exp?: number; // Expiration Time
736
+
737
+ // 서비스 정보
738
+ services?: ServiceInfo[];
739
+
740
+ // 학원 정보
741
+ academies?: Array<{
742
+ id: string;
743
+ name: string;
744
+ role: string;
745
+ }>;
746
+
747
+ // 인증 상태
748
+ phoneVerified?: boolean;
749
+ emailVerified?: boolean;
750
+ smsVerified?: boolean;
751
+
752
+ // 선택적 필드
753
+ phone?: string;
754
+ academyId?: string;
755
+ academyName?: string;
756
+ isPasswordReset?: boolean;
757
+ decryptedEmail?: string;
758
+ decryptedPhone?: string;
759
+ emailHash?: string;
760
+ maskedEmail?: string;
761
+ phoneHash?: string;
762
+ maskedPhone?: string;
763
+ refreshToken?: string;
764
+ accessToken?: string;
765
+ accessTokenExpires?: number;
766
+ serviceId?: string;
767
+
768
+ // 기타 필드
769
+ [key: string]: unknown;
770
+ }
771
+ ```
772
+
773
+ ### `ServiceInfo`
774
+
775
+ ```typescript
776
+ interface ServiceInfo {
777
+ serviceId: string;
778
+ role: string;
779
+ joinedAt: string;
780
+ lastAccessAt?: string;
781
+ expiredAt?: string;
782
+ status: string;
783
+ }
784
+ ```
785
+
786
+ ### SSO API 응답 타입
787
+
788
+ #### `SSOUpsertResponse`
789
+
790
+ ```typescript
791
+ interface SSOUpsertResponse {
792
+ success: boolean;
793
+ message?: string;
794
+ user?: {
795
+ id: string;
796
+ email: string;
797
+ name: string;
798
+ };
799
+ }
800
+ ```
801
+
802
+ #### `SSOErrorResponse`
803
+
804
+ ```typescript
805
+ interface SSOErrorResponse {
806
+ error: string;
807
+ message?: string;
808
+ }
809
+ ```
810
+
811
+ #### `SSORefreshTokenResponse`
812
+
813
+ ```typescript
814
+ interface SSORefreshTokenResponse {
815
+ success: boolean;
816
+ accessToken?: string;
817
+ refreshToken?: string;
818
+ user?: {
819
+ id: string;
820
+ email: string;
821
+ name: string;
822
+ };
823
+ error?: string;
824
+ }
825
+ ```
826
+
827
+ #### `SSOGetRefreshTokenResponse`
828
+
829
+ ```typescript
830
+ interface SSOGetRefreshTokenResponse {
831
+ success: boolean;
832
+ refreshToken?: string;
833
+ error?: string;
834
+ }
835
+ ```
836
+
837
+ #### `SSORegisterResponse`
838
+
839
+ ```typescript
840
+ interface SSORegisterResponse {
841
+ success: boolean;
842
+ message?: string;
843
+ user?: {
844
+ id: string;
845
+ email: string;
846
+ name: string;
847
+ };
848
+ }
849
+ ```
850
+
851
+ ### 사용 예시
852
+
853
+ ```typescript
854
+ import type {
855
+ SSOUpsertResponse,
856
+ SSOErrorResponse,
857
+ SSORefreshTokenResponse,
858
+ SSOGetRefreshTokenResponse,
859
+ SSORegisterResponse,
860
+ } from "@thinkingcat/auth-token-utils";
861
+
862
+ // SSO API 호출 시 타입 사용
863
+ const response = await fetch("/api/sso/refresh", {
864
+ method: "POST",
865
+ body: JSON.stringify({ refreshToken }),
866
+ });
867
+
868
+ const result = (await response.json()) as SSORefreshTokenResponse;
869
+ if (result.success && result.accessToken) {
870
+ // 타입 안전하게 사용
871
+ console.log(result.accessToken);
872
+ }
873
+ ```
874
+
875
+ ### `ResponseLike`
876
+
877
+ ```typescript
878
+ interface ResponseLike {
879
+ cookies: {
880
+ delete(name: string): void;
881
+ set(
882
+ name: string,
883
+ value: string,
884
+ options?: {
885
+ httpOnly?: boolean;
886
+ secure?: boolean;
887
+ sameSite?: "strict" | "lax" | "none";
888
+ maxAge?: number;
889
+ path?: string;
890
+ domain?: string;
891
+ }
892
+ ): void;
893
+ };
894
+ }
895
+ ```
896
+
897
+ ## 🐛 문제 해결 (Troubleshooting)
898
+
899
+ ### 문제 1: "Cannot find module '@thinkingcat/auth-token-utils'"
900
+
901
+ **해결 방법:**
902
+
903
+ ```bash
904
+ # 패키지 재설치
905
+ npm install @thinkingcat/auth-token-utils
906
+
907
+ # 또는 node_modules 삭제 후 재설치
908
+ rm -rf node_modules package-lock.json
909
+ npm install
910
+ ```
911
+
912
+ ### 문제 2: 타입 오류 "NextResponse is not assignable to ResponseLike"
913
+
914
+ **해결 방법:**
915
+ `ResponseLike` 인터페이스는 NextResponse와 호환됩니다. 타입 단언이 필요하지 않습니다. 만약 오류가 발생한다면 패키지 버전을 확인하세요.
916
+
917
+ ### 문제 3: 토큰 검증 실패
918
+
919
+ **확인 사항:**
920
+
921
+ - `NEXTAUTH_SECRET`이 올바르게 설정되었는지 확인
922
+ - 토큰이 만료되지 않았는지 확인
923
+ - 토큰 형식이 올바른지 확인
924
+
925
+ ### 문제 4: 쿠키가 설정되지 않음
926
+
927
+ **확인 사항:**
928
+
929
+ - `isProduction` 옵션이 올바르게 설정되었는지 확인
930
+ - 브라우저 개발자 도구에서 쿠키 설정 확인
931
+ - 도메인 및 경로 설정 확인
932
+
933
+ ## 📦 패키지 정보
934
+
935
+ - **패키지명**: `@thinkingcat/auth-token-utils`
936
+ - **버전**: `1.0.0`
937
+ - **라이선스**: MIT
938
+ - **저장소**: npm registry
939
+
940
+ ## 🤝 기여 (Contributing)
941
+
942
+ 이슈나 개선 사항이 있으면 GitHub 이슈를 등록해주세요.
943
+
944
+ ## 📄 라이선스 (License)
945
+
946
+ MIT License
947
+
948
+ ---
949
+
950
+ **Made with ❤️ by ThinkingCat**