@thinkingcat/auth-utils 1.0.7 → 1.0.9
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 +631 -286
- package/dist/index.d.ts +80 -53
- package/dist/index.js +248 -144
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -10,25 +10,16 @@ ThinkingCat SSO 서비스를 위한 인증 유틸리티 패키지입니다. JWT
|
|
|
10
10
|
- [🚀 빠른 시작 (Quick Start)](#-빠른-시작-quick-start)
|
|
11
11
|
- [📚 주요 기능 (Features)](#-주요-기능-features)
|
|
12
12
|
- [🔧 API 레퍼런스](#-api-레퍼런스)
|
|
13
|
-
- [
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
17
|
-
- [
|
|
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)
|
|
13
|
+
- [토큰 검증 및 생성](#토큰-검증-및-생성)
|
|
14
|
+
- [쿠키 관리](#쿠키-관리)
|
|
15
|
+
- [역할 및 접근 제어](#역할-및-접근-제어)
|
|
16
|
+
- [SSO 통합](#sso-통합)
|
|
17
|
+
- [미들웨어](#미들웨어)
|
|
21
18
|
- [💡 사용 시나리오](#-사용-시나리오)
|
|
22
|
-
- [시나리오 1: 자체 토큰만 사용하는 서비스](#시나리오-1-자체-토큰만-사용하는-서비스)
|
|
23
|
-
- [시나리오 2: NextAuth만 사용하는 서비스](#시나리오-2-nextauth만-사용하는-서비스)
|
|
24
|
-
- [시나리오 3: 자체 토큰 + NextAuth 모두 사용 (check-on 예시)](#시나리오-3-자체-토큰--nextauth-모두-사용-check-on-예시)
|
|
25
|
-
- [시나리오 4: Middleware에서 사용](#시나리오-4-middleware에서-사용)
|
|
26
19
|
- [🔒 보안 고려사항](#-보안-고려사항)
|
|
27
20
|
- [📝 타입 정의](#-타입-정의)
|
|
28
21
|
- [🐛 문제 해결 (Troubleshooting)](#-문제-해결-troubleshooting)
|
|
29
22
|
- [📦 패키지 정보](#-패키지-정보)
|
|
30
|
-
- [🤝 기여 (Contributing)](#-기여-contributing)
|
|
31
|
-
- [📄 라이선스 (License)](#-라이선스-license)
|
|
32
23
|
|
|
33
24
|
## 📦 설치 (Installation)
|
|
34
25
|
|
|
@@ -69,7 +60,7 @@ npm install @thinkingcat/auth-utils
|
|
|
69
60
|
```json
|
|
70
61
|
{
|
|
71
62
|
"dependencies": {
|
|
72
|
-
"@thinkingcat/auth-utils": "^1.0.
|
|
63
|
+
"@thinkingcat/auth-utils": "^1.0.8"
|
|
73
64
|
}
|
|
74
65
|
}
|
|
75
66
|
```
|
|
@@ -94,28 +85,9 @@ const nextConfig = {
|
|
|
94
85
|
module.exports = nextConfig;
|
|
95
86
|
```
|
|
96
87
|
|
|
97
|
-
**왜 필요한가?**
|
|
98
|
-
|
|
99
|
-
- Next.js는 기본적으로 `node_modules`의 패키지를 트랜스파일하지 않습니다
|
|
100
|
-
- 로컬 패키지를 사용할 때는 Next.js가 소스 코드를 직접 처리해야 할 수 있습니다
|
|
101
|
-
- `transpilePackages`는 Next.js에게 해당 패키지를 트랜스파일하라고 지시합니다
|
|
102
|
-
|
|
103
88
|
**해결 방법 2: npm에 배포된 버전 사용 (가장 안정적)**
|
|
104
89
|
|
|
105
|
-
로컬 패키지 대신 npm에 배포된 버전을 사용하면 설정이 필요
|
|
106
|
-
|
|
107
|
-
```json
|
|
108
|
-
{
|
|
109
|
-
"dependencies": {
|
|
110
|
-
"@thinkingcat/auth-utils": "^1.0.4"
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
**권장사항**:
|
|
116
|
-
|
|
117
|
-
- **프로덕션 환경**: npm에 배포된 버전 사용 (설정 불필요)
|
|
118
|
-
- **개발 환경**: 로컬 패키지 사용 시 `transpilePackages` 추가
|
|
90
|
+
로컬 패키지 대신 npm에 배포된 버전을 사용하면 설정이 필요 없습니다.
|
|
119
91
|
|
|
120
92
|
## 🚀 빠른 시작 (Quick Start)
|
|
121
93
|
|
|
@@ -123,30 +95,83 @@ module.exports = nextConfig;
|
|
|
123
95
|
|
|
124
96
|
```typescript
|
|
125
97
|
import {
|
|
98
|
+
// 토큰 검증 및 생성
|
|
126
99
|
verifyToken,
|
|
127
100
|
extractRoleFromPayload,
|
|
128
101
|
createNextAuthJWT,
|
|
129
102
|
encodeNextAuthToken,
|
|
103
|
+
|
|
104
|
+
// 쿠키 관리
|
|
130
105
|
setCustomTokens,
|
|
131
106
|
setNextAuthToken,
|
|
107
|
+
clearAuthCookies,
|
|
108
|
+
|
|
109
|
+
// 리다이렉트 및 응답
|
|
132
110
|
createRedirectHTML,
|
|
133
111
|
createAuthResponse,
|
|
112
|
+
redirectToError,
|
|
113
|
+
redirectToSSOLogin,
|
|
114
|
+
redirectToRoleDashboard,
|
|
115
|
+
|
|
116
|
+
// 역할 및 접근 제어
|
|
117
|
+
getEffectiveRole,
|
|
118
|
+
hasRole,
|
|
119
|
+
hasAnyRole,
|
|
120
|
+
checkRoleAccess,
|
|
121
|
+
requiresSubscription,
|
|
122
|
+
|
|
123
|
+
// 경로 체크
|
|
124
|
+
isPublicPath,
|
|
125
|
+
isApiPath,
|
|
126
|
+
isProtectedApiPath,
|
|
127
|
+
|
|
128
|
+
// 토큰 유효성 검사
|
|
129
|
+
isTokenExpired,
|
|
130
|
+
isValidToken,
|
|
131
|
+
|
|
132
|
+
// SSO 통합
|
|
133
|
+
refreshSSOToken,
|
|
134
|
+
getRefreshTokenFromSSO,
|
|
135
|
+
verifyTokenFromSSO,
|
|
136
|
+
validateServiceSubscription,
|
|
137
|
+
|
|
138
|
+
// 미들웨어
|
|
139
|
+
createMiddlewareConfig,
|
|
140
|
+
handleMiddleware,
|
|
141
|
+
verifyAndRefreshTokenWithNextAuth,
|
|
142
|
+
|
|
143
|
+
// 라이센스
|
|
144
|
+
checkLicenseKey,
|
|
145
|
+
|
|
146
|
+
// 타입
|
|
134
147
|
ServiceInfo,
|
|
135
148
|
ResponseLike,
|
|
136
149
|
JWTPayload,
|
|
150
|
+
MiddlewareConfig,
|
|
151
|
+
MiddlewareOptions,
|
|
137
152
|
} from "@thinkingcat/auth-utils";
|
|
138
153
|
```
|
|
139
154
|
|
|
140
|
-
### 2.
|
|
155
|
+
### 2. 라이센스 키 설정 (선택사항)
|
|
141
156
|
|
|
142
|
-
`.env.local` 파일에 다음 환경 변수를 설정하세요:
|
|
157
|
+
라이센스 키를 사용하려면 `.env.local` 파일에 다음 환경 변수를 설정하세요:
|
|
143
158
|
|
|
144
159
|
```env
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
160
|
+
# 단일 라이센스 키 (가장 간단)
|
|
161
|
+
LICENSE_KEY=TC-checkon-XXXX-XXXX
|
|
162
|
+
|
|
163
|
+
# 또는 여러 서비스별 키 목록
|
|
164
|
+
LICENSE_KEYS={"checkon":"TC-checkon-XXXX-XXXX","renton":"TC-renton-YYYY-YYYY"}
|
|
148
165
|
```
|
|
149
166
|
|
|
167
|
+
**중요**:
|
|
168
|
+
|
|
169
|
+
- 라이센스 키는 선택사항입니다. 제공하지 않으면 라이센스 검증을 건너뜁니다.
|
|
170
|
+
- `LICENSE_KEY` 환경 변수를 설정하면 함수 호출 시 자동으로 사용됩니다.
|
|
171
|
+
- 함수 파라미터로 `licenseKey`를 전달하면 환경 변수보다 우선합니다.
|
|
172
|
+
- 다른 환경 변수(NEXTAUTH_SECRET, SSO_BASE_URL 등)는 이 패키지를 사용하는 애플리케이션에서 관리합니다.
|
|
173
|
+
- `.env.example` 파일을 참조하여 라이센스 키 설정 방법을 확인하세요.
|
|
174
|
+
|
|
150
175
|
## 📚 주요 기능 (Features)
|
|
151
176
|
|
|
152
177
|
이 패키지는 다음 기능을 제공합니다:
|
|
@@ -164,10 +189,14 @@ COOKIE_DOMAIN=.yourdomain.com # 선택사항
|
|
|
164
189
|
11. **토큰 유효성 검사**: 토큰 만료 및 유효성 확인
|
|
165
190
|
12. **경로 체크**: 공개 경로, API 경로, 보호된 경로 확인
|
|
166
191
|
13. **통합 인증 체크**: NextAuth와 자체 토큰을 모두 확인하는 통합 함수
|
|
192
|
+
14. **미들웨어 통합**: 완전한 미들웨어 핸들러 함수 제공
|
|
193
|
+
15. **설정 관리**: 미들웨어 설정 생성 및 관리 함수
|
|
167
194
|
|
|
168
195
|
## 🔧 API 레퍼런스
|
|
169
196
|
|
|
170
|
-
###
|
|
197
|
+
### 토큰 검증 및 생성
|
|
198
|
+
|
|
199
|
+
#### `verifyToken(accessToken: string, secret: string, licenseKey: string)`
|
|
171
200
|
|
|
172
201
|
JWT access token을 검증하고 디코딩합니다.
|
|
173
202
|
|
|
@@ -175,6 +204,7 @@ JWT access token을 검증하고 디코딩합니다.
|
|
|
175
204
|
|
|
176
205
|
- `accessToken`: 검증할 JWT 토큰
|
|
177
206
|
- `secret`: JWT 서명에 사용할 secret key
|
|
207
|
+
- `licenseKey`: 라이센스 키 (필수)
|
|
178
208
|
|
|
179
209
|
**반환값:**
|
|
180
210
|
|
|
@@ -185,7 +215,8 @@ JWT access token을 검증하고 디코딩합니다.
|
|
|
185
215
|
|
|
186
216
|
```typescript
|
|
187
217
|
const secret = process.env.NEXTAUTH_SECRET!;
|
|
188
|
-
const
|
|
218
|
+
const licenseKey = process.env.LICENSE_KEY!;
|
|
219
|
+
const result = await verifyToken(accessToken, secret, licenseKey);
|
|
189
220
|
|
|
190
221
|
if (result) {
|
|
191
222
|
const { payload } = result;
|
|
@@ -196,14 +227,14 @@ if (result) {
|
|
|
196
227
|
}
|
|
197
228
|
```
|
|
198
229
|
|
|
199
|
-
|
|
230
|
+
#### `extractRoleFromPayload(payload: JWTPayload, serviceId: string, defaultRole?: string)`
|
|
200
231
|
|
|
201
232
|
payload에서 특정 서비스의 역할을 추출합니다.
|
|
202
233
|
|
|
203
234
|
**파라미터:**
|
|
204
235
|
|
|
205
236
|
- `payload`: JWT payload 객체 (JWTPayload 타입)
|
|
206
|
-
- `serviceId`: 서비스 ID (
|
|
237
|
+
- `serviceId`: 서비스 ID (필수)
|
|
207
238
|
- `defaultRole`: 기본 역할 (기본값: 'ADMIN')
|
|
208
239
|
|
|
209
240
|
**반환값:**
|
|
@@ -213,17 +244,18 @@ payload에서 특정 서비스의 역할을 추출합니다.
|
|
|
213
244
|
**사용 예시:**
|
|
214
245
|
|
|
215
246
|
```typescript
|
|
216
|
-
const role = extractRoleFromPayload(payload, "
|
|
247
|
+
const role = extractRoleFromPayload(payload, "myservice", "ADMIN");
|
|
217
248
|
// 'ADMIN', 'TEACHER', 'STUDENT' 등
|
|
218
249
|
```
|
|
219
250
|
|
|
220
|
-
|
|
251
|
+
#### `createNextAuthJWT(payload: JWTPayload, serviceId: string, includeAcademies?: boolean)`
|
|
221
252
|
|
|
222
253
|
NextAuth와 호환되는 JWT 객체를 생성합니다.
|
|
223
254
|
|
|
224
255
|
**파라미터:**
|
|
225
256
|
|
|
226
257
|
- `payload`: 원본 JWT payload (JWTPayload 타입)
|
|
258
|
+
- `serviceId`: 서비스 ID (필수)
|
|
227
259
|
- `includeAcademies`: academies 정보 포함 여부 (기본값: false)
|
|
228
260
|
|
|
229
261
|
**반환값:**
|
|
@@ -233,11 +265,11 @@ NextAuth와 호환되는 JWT 객체를 생성합니다.
|
|
|
233
265
|
**사용 예시:**
|
|
234
266
|
|
|
235
267
|
```typescript
|
|
236
|
-
const jwt = createNextAuthJWT(payload, true);
|
|
268
|
+
const jwt = createNextAuthJWT(payload, "myservice", true);
|
|
237
269
|
// academies 정보가 포함된 JWT 객체
|
|
238
270
|
```
|
|
239
271
|
|
|
240
|
-
|
|
272
|
+
#### `encodeNextAuthToken(jwt: JWT, secret: string, maxAge?: number)`
|
|
241
273
|
|
|
242
274
|
NextAuth JWT 객체를 인코딩된 세션 토큰으로 변환합니다.
|
|
243
275
|
|
|
@@ -255,11 +287,13 @@ NextAuth JWT 객체를 인코딩된 세션 토큰으로 변환합니다.
|
|
|
255
287
|
|
|
256
288
|
```typescript
|
|
257
289
|
const secret = process.env.NEXTAUTH_SECRET!;
|
|
258
|
-
const jwt = createNextAuthJWT(payload);
|
|
290
|
+
const jwt = createNextAuthJWT(payload, "myservice");
|
|
259
291
|
const sessionToken = await encodeNextAuthToken(jwt, secret);
|
|
260
292
|
```
|
|
261
293
|
|
|
262
|
-
###
|
|
294
|
+
### 쿠키 관리
|
|
295
|
+
|
|
296
|
+
#### `setCustomTokens(response: ResponseLike, accessToken: string, optionsOrRefreshToken?, options?)`
|
|
263
297
|
|
|
264
298
|
자체 토큰(access token, refresh token)만 쿠키에 설정합니다.
|
|
265
299
|
|
|
@@ -274,46 +308,32 @@ const sessionToken = await encodeNextAuthToken(jwt, secret);
|
|
|
274
308
|
|
|
275
309
|
**옵션:**
|
|
276
310
|
|
|
277
|
-
- `cookiePrefix`: 쿠키 이름 접두사 (
|
|
311
|
+
- `cookiePrefix`: 쿠키 이름 접두사 (필수)
|
|
278
312
|
- `isProduction`: 프로덕션 환경 여부 (기본값: false)
|
|
279
313
|
- `refreshToken`: refresh token (옵션 객체 내부에 포함 가능)
|
|
280
314
|
|
|
281
315
|
**사용 예시:**
|
|
282
316
|
|
|
283
|
-
**방법 1: refreshToken이 있는 경우**
|
|
284
|
-
|
|
285
317
|
```typescript
|
|
318
|
+
// 방법 1: refreshToken이 있는 경우
|
|
286
319
|
setCustomTokens(response, accessToken, refreshToken, {
|
|
287
320
|
cookiePrefix: "myservice",
|
|
288
321
|
isProduction: process.env.NODE_ENV === "production",
|
|
289
322
|
});
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
**방법 2: refreshToken이 없는 경우**
|
|
293
323
|
|
|
294
|
-
|
|
324
|
+
// 방법 2: refreshToken이 없는 경우
|
|
295
325
|
setCustomTokens(response, accessToken, {
|
|
296
326
|
cookiePrefix: "myservice",
|
|
297
327
|
isProduction: process.env.NODE_ENV === "production",
|
|
298
328
|
});
|
|
299
329
|
```
|
|
300
330
|
|
|
301
|
-
**방법 3: 옵션 객체에 refreshToken 포함**
|
|
302
|
-
|
|
303
|
-
```typescript
|
|
304
|
-
setCustomTokens(response, accessToken, {
|
|
305
|
-
refreshToken: refreshToken,
|
|
306
|
-
cookiePrefix: "myservice",
|
|
307
|
-
isProduction: true,
|
|
308
|
-
});
|
|
309
|
-
```
|
|
310
|
-
|
|
311
331
|
**설정되는 쿠키:**
|
|
312
332
|
|
|
313
333
|
- `{cookiePrefix}_access_token`: 15분 유효
|
|
314
334
|
- `{cookiePrefix}_refresh_token`: 30일 유효 (있는 경우)
|
|
315
335
|
|
|
316
|
-
|
|
336
|
+
#### `setNextAuthToken(response: ResponseLike, sessionToken: string, options?)`
|
|
317
337
|
|
|
318
338
|
NextAuth 세션 토큰만 쿠키에 설정합니다.
|
|
319
339
|
|
|
@@ -343,14 +363,384 @@ setNextAuthToken(response, sessionToken, {
|
|
|
343
363
|
- 프로덕션: `__Secure-next-auth.session-token`
|
|
344
364
|
- 유효 기간: 30일
|
|
345
365
|
|
|
346
|
-
|
|
366
|
+
#### `clearAuthCookies(response: NextResponse, cookiePrefix: string)`
|
|
367
|
+
|
|
368
|
+
인증 쿠키를 삭제합니다.
|
|
369
|
+
|
|
370
|
+
**파라미터:**
|
|
371
|
+
|
|
372
|
+
- `response`: NextResponse 객체
|
|
373
|
+
- `cookiePrefix`: 쿠키 이름 접두사 (필수)
|
|
374
|
+
|
|
375
|
+
**사용 예시:**
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
const response = NextResponse.redirect("/login");
|
|
379
|
+
clearAuthCookies(response, "myservice");
|
|
380
|
+
return response;
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### 역할 및 접근 제어
|
|
384
|
+
|
|
385
|
+
#### `getEffectiveRole(payload: JWTPayload, serviceId: string, defaultRole?: string)`
|
|
386
|
+
|
|
387
|
+
payload에서 유효한 역할을 가져옵니다.
|
|
388
|
+
|
|
389
|
+
**파라미터:**
|
|
390
|
+
|
|
391
|
+
- `payload`: JWT payload 객체
|
|
392
|
+
- `serviceId`: 서비스 ID (필수)
|
|
393
|
+
- `defaultRole`: 기본 역할 (기본값: 'ADMIN')
|
|
394
|
+
|
|
395
|
+
**반환값:**
|
|
396
|
+
|
|
397
|
+
- 역할 문자열
|
|
398
|
+
|
|
399
|
+
#### `hasRole(payload: JWTPayload, serviceId: string, role: string)`
|
|
400
|
+
|
|
401
|
+
payload에 특정 역할이 있는지 확인합니다.
|
|
402
|
+
|
|
403
|
+
**파라미터:**
|
|
404
|
+
|
|
405
|
+
- `payload`: JWT payload 객체
|
|
406
|
+
- `serviceId`: 서비스 ID (필수)
|
|
407
|
+
- `role`: 확인할 역할
|
|
408
|
+
|
|
409
|
+
**반환값:**
|
|
410
|
+
|
|
411
|
+
- `boolean`
|
|
412
|
+
|
|
413
|
+
#### `hasAnyRole(payload: JWTPayload, serviceId: string, roles: string[])`
|
|
414
|
+
|
|
415
|
+
payload에 여러 역할 중 하나라도 있는지 확인합니다.
|
|
416
|
+
|
|
417
|
+
**파라미터:**
|
|
418
|
+
|
|
419
|
+
- `payload`: JWT payload 객체
|
|
420
|
+
- `serviceId`: 서비스 ID (필수)
|
|
421
|
+
- `roles`: 확인할 역할 배열
|
|
422
|
+
|
|
423
|
+
**반환값:**
|
|
424
|
+
|
|
425
|
+
- `boolean`
|
|
426
|
+
|
|
427
|
+
#### `checkRoleAccess(pathname: string, role: string, config: RoleAccessConfig[])`
|
|
428
|
+
|
|
429
|
+
경로에 대한 역할 접근 권한을 확인합니다.
|
|
430
|
+
|
|
431
|
+
**파라미터:**
|
|
432
|
+
|
|
433
|
+
- `pathname`: 경로
|
|
434
|
+
- `role`: 사용자 역할
|
|
435
|
+
- `config`: 역할 접근 설정 배열
|
|
436
|
+
|
|
437
|
+
**반환값:**
|
|
438
|
+
|
|
439
|
+
- `{ allowed: boolean; message?: string }`
|
|
440
|
+
|
|
441
|
+
#### `requiresSubscription(pathname: string, role: string, subscriptionRequiredPaths: string[], systemAdminRole?: string)`
|
|
442
|
+
|
|
443
|
+
경로가 구독이 필요한지 확인합니다.
|
|
444
|
+
|
|
445
|
+
**파라미터:**
|
|
446
|
+
|
|
447
|
+
- `pathname`: 경로
|
|
448
|
+
- `role`: 사용자 역할
|
|
449
|
+
- `subscriptionRequiredPaths`: 구독이 필요한 경로 배열
|
|
450
|
+
- `systemAdminRole`: 시스템 관리자 역할 (선택사항)
|
|
451
|
+
|
|
452
|
+
**반환값:**
|
|
453
|
+
|
|
454
|
+
- `boolean`
|
|
455
|
+
|
|
456
|
+
### SSO 통합
|
|
457
|
+
|
|
458
|
+
#### `refreshSSOToken(refreshToken: string, options: { ssoBaseURL: string; authServiceKey?: string })`
|
|
459
|
+
|
|
460
|
+
SSO 서버에서 refresh token을 사용하여 새로운 access token을 발급받습니다.
|
|
461
|
+
|
|
462
|
+
**파라미터:**
|
|
463
|
+
|
|
464
|
+
- `refreshToken`: refresh token
|
|
465
|
+
- `options.ssoBaseURL`: SSO 서버 기본 URL (필수)
|
|
466
|
+
- `options.authServiceKey`: 인증 서비스 키 (선택사항)
|
|
467
|
+
|
|
468
|
+
**반환값:**
|
|
469
|
+
|
|
470
|
+
- `SSORefreshTokenResponse`
|
|
471
|
+
|
|
472
|
+
**사용 예시:**
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
const result = await refreshSSOToken(refreshToken, {
|
|
476
|
+
ssoBaseURL: process.env.SSO_BASE_URL!,
|
|
477
|
+
authServiceKey: process.env.AUTH_SERVICE_SECRET_KEY,
|
|
478
|
+
});
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
#### `getRefreshTokenFromSSO(userId: string, accessToken: string, options: { ssoBaseURL: string; authServiceKey?: string })`
|
|
482
|
+
|
|
483
|
+
SSO 서버에서 사용자의 refresh token을 가져옵니다.
|
|
484
|
+
|
|
485
|
+
**파라미터:**
|
|
486
|
+
|
|
487
|
+
- `userId`: 사용자 ID
|
|
488
|
+
- `accessToken`: access token
|
|
489
|
+
- `options.ssoBaseURL`: SSO 서버 기본 URL (필수)
|
|
490
|
+
- `options.authServiceKey`: 인증 서비스 키 (선택사항)
|
|
491
|
+
|
|
492
|
+
**반환값:**
|
|
493
|
+
|
|
494
|
+
- `string | null`
|
|
495
|
+
|
|
496
|
+
#### `verifyTokenFromSSO(accessToken: string, options: { ssoBaseURL: string; authServiceKey?: string })`
|
|
497
|
+
|
|
498
|
+
SSO 서버에서 토큰을 검증합니다.
|
|
499
|
+
|
|
500
|
+
**파라미터:**
|
|
501
|
+
|
|
502
|
+
- `accessToken`: access token
|
|
503
|
+
- `options.ssoBaseURL`: SSO 서버 기본 URL (필수)
|
|
504
|
+
- `options.authServiceKey`: 인증 서비스 키 (선택사항)
|
|
505
|
+
|
|
506
|
+
**반환값:**
|
|
507
|
+
|
|
508
|
+
- `Promise<{ isValid: boolean; payload?: JWTPayload }>`
|
|
509
|
+
|
|
510
|
+
#### `validateServiceSubscription(services: ServiceInfo[], serviceId: string, ssoBaseURL: string)`
|
|
511
|
+
|
|
512
|
+
서비스 구독 상태를 확인합니다.
|
|
513
|
+
|
|
514
|
+
**파라미터:**
|
|
515
|
+
|
|
516
|
+
- `services`: 서비스 정보 배열
|
|
517
|
+
- `serviceId`: 서비스 ID
|
|
518
|
+
- `ssoBaseURL`: SSO 서버 기본 URL (필수)
|
|
519
|
+
|
|
520
|
+
**반환값:**
|
|
521
|
+
|
|
522
|
+
- `{ isValid: boolean; redirectUrl?: string }`
|
|
523
|
+
|
|
524
|
+
### 미들웨어
|
|
525
|
+
|
|
526
|
+
#### `createMiddlewareConfig(config: Partial<MiddlewareConfig> & { serviceId: string }, defaults?)`
|
|
527
|
+
|
|
528
|
+
미들웨어 설정을 생성합니다.
|
|
529
|
+
|
|
530
|
+
**파라미터:**
|
|
531
|
+
|
|
532
|
+
- `config`: 미들웨어 설정 객체 (serviceId 필수)
|
|
533
|
+
- `defaults`: 기본 설정값 (선택사항)
|
|
534
|
+
|
|
535
|
+
**반환값:**
|
|
536
|
+
|
|
537
|
+
- 완전한 미들웨어 설정 객체
|
|
538
|
+
|
|
539
|
+
**사용 예시:**
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
import { createMiddlewareConfig } from "@thinkingcat/auth-utils";
|
|
543
|
+
|
|
544
|
+
const middlewareConfig = createMiddlewareConfig({
|
|
545
|
+
serviceId: "myservice",
|
|
546
|
+
publicPaths: ["/login", "/register"],
|
|
547
|
+
roleAccessConfig: [
|
|
548
|
+
{
|
|
549
|
+
paths: ["/admin"],
|
|
550
|
+
role: "ADMIN",
|
|
551
|
+
message: "관리자만 접근할 수 있습니다.",
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
});
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
#### `handleMiddleware(req: NextRequest, config: MiddlewareConfig, options: MiddlewareOptions)`
|
|
558
|
+
|
|
559
|
+
통합 미들웨어 핸들러 함수입니다. 모든 인증, 권한, 구독 체크를 포함합니다.
|
|
560
|
+
|
|
561
|
+
**파라미터:**
|
|
562
|
+
|
|
563
|
+
- `req`: NextRequest 객체
|
|
564
|
+
- `config`: 미들웨어 설정 (createMiddlewareConfig로 생성)
|
|
565
|
+
- `options`: 미들웨어 실행 옵션
|
|
566
|
+
|
|
567
|
+
**옵션:**
|
|
568
|
+
|
|
569
|
+
- `secret`: NextAuth Secret (필수)
|
|
570
|
+
- `isProduction`: 프로덕션 환경 여부 (필수)
|
|
571
|
+
- `cookieDomain`: 쿠키 도메인 (선택사항)
|
|
572
|
+
- `getNextAuthToken`: NextAuth 토큰을 가져오는 함수 (선택사항)
|
|
573
|
+
- `ssoBaseURL`: SSO 서버 기본 URL (필수)
|
|
574
|
+
- `authServiceKey`: 인증 서비스 키 (선택사항)
|
|
575
|
+
|
|
576
|
+
**반환값:**
|
|
577
|
+
|
|
578
|
+
- NextResponse 또는 null (다음 미들웨어로 진행)
|
|
579
|
+
|
|
580
|
+
**사용 예시:**
|
|
581
|
+
|
|
582
|
+
```typescript
|
|
583
|
+
import { withAuth } from "next-auth/middleware";
|
|
584
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
585
|
+
import { getToken } from "next-auth/jwt";
|
|
586
|
+
import {
|
|
587
|
+
createMiddlewareConfig,
|
|
588
|
+
handleMiddleware,
|
|
589
|
+
} from "@thinkingcat/auth-utils";
|
|
590
|
+
|
|
591
|
+
const middlewareConfig = createMiddlewareConfig({
|
|
592
|
+
serviceId: "myservice",
|
|
593
|
+
publicPaths: ["/login", "/register"],
|
|
594
|
+
roleAccessConfig: [
|
|
595
|
+
{
|
|
596
|
+
paths: ["/admin"],
|
|
597
|
+
role: "ADMIN",
|
|
598
|
+
message: "관리자만 접근할 수 있습니다.",
|
|
599
|
+
},
|
|
600
|
+
],
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
export default withAuth(
|
|
604
|
+
async function middleware(req: NextRequest) {
|
|
605
|
+
const response = await handleMiddleware(req, middlewareConfig, {
|
|
606
|
+
secret: process.env.NEXTAUTH_SECRET!,
|
|
607
|
+
isProduction: process.env.NODE_ENV === "production",
|
|
608
|
+
cookieDomain: process.env.COOKIE_DOMAIN,
|
|
609
|
+
ssoBaseURL: process.env.SSO_BASE_URL!,
|
|
610
|
+
getNextAuthToken: async (req: NextRequest) => {
|
|
611
|
+
return await getToken({ req, secret: process.env.NEXTAUTH_SECRET! });
|
|
612
|
+
},
|
|
613
|
+
authServiceKey: process.env.AUTH_SERVICE_SECRET_KEY,
|
|
614
|
+
});
|
|
615
|
+
return response || NextResponse.next();
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
callbacks: {
|
|
619
|
+
authorized: () => true,
|
|
620
|
+
},
|
|
621
|
+
}
|
|
622
|
+
);
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
#### `verifyAndRefreshTokenWithNextAuth(req: NextRequest, secret: string, cookiePrefix: string, serviceId: string, options: { ssoBaseURL: string; authServiceKey?: string; getNextAuthToken?: (req: NextRequest) => Promise<JWT | null> })`
|
|
626
|
+
|
|
627
|
+
NextAuth 토큰을 먼저 확인하고, 없으면 자체 토큰을 확인하고 필요시 리프레시합니다.
|
|
628
|
+
|
|
629
|
+
**파라미터:**
|
|
630
|
+
|
|
631
|
+
- `req`: NextRequest 객체
|
|
632
|
+
- `secret`: JWT 서명에 사용할 secret key
|
|
633
|
+
- `cookiePrefix`: 쿠키 이름 접두사
|
|
634
|
+
- `serviceId`: 서비스 ID (필수)
|
|
635
|
+
- `options`: 옵션 객체
|
|
636
|
+
|
|
637
|
+
**반환값:**
|
|
638
|
+
|
|
639
|
+
- `Promise<{ isValid: boolean; payload?: JWTPayload; error?: string }>`
|
|
640
|
+
|
|
641
|
+
### 경로 체크
|
|
642
|
+
|
|
643
|
+
#### `isPublicPath(pathname: string, publicPaths: string[])`
|
|
644
|
+
|
|
645
|
+
경로가 공개 경로인지 확인합니다.
|
|
646
|
+
|
|
647
|
+
**파라미터:**
|
|
648
|
+
|
|
649
|
+
- `pathname`: 경로
|
|
650
|
+
- `publicPaths`: 공개 경로 배열
|
|
651
|
+
|
|
652
|
+
**반환값:**
|
|
653
|
+
|
|
654
|
+
- `boolean`
|
|
655
|
+
|
|
656
|
+
#### `isApiPath(pathname: string)`
|
|
657
|
+
|
|
658
|
+
경로가 API 경로인지 확인합니다.
|
|
659
|
+
|
|
660
|
+
**파라미터:**
|
|
661
|
+
|
|
662
|
+
- `pathname`: 경로
|
|
663
|
+
|
|
664
|
+
**반환값:**
|
|
665
|
+
|
|
666
|
+
- `boolean`
|
|
667
|
+
|
|
668
|
+
#### `isProtectedApiPath(pathname: string, authApiPaths: string[])`
|
|
669
|
+
|
|
670
|
+
경로가 보호된 API 경로인지 확인합니다.
|
|
671
|
+
|
|
672
|
+
**파라미터:**
|
|
673
|
+
|
|
674
|
+
- `pathname`: 경로
|
|
675
|
+
- `authApiPaths`: 인증이 필요한 API 경로 배열
|
|
676
|
+
|
|
677
|
+
**반환값:**
|
|
678
|
+
|
|
679
|
+
- `boolean`
|
|
680
|
+
|
|
681
|
+
### 토큰 유효성 검사
|
|
682
|
+
|
|
683
|
+
#### `isTokenExpired(token: JWT | null)`
|
|
684
|
+
|
|
685
|
+
토큰이 만료되었는지 확인합니다.
|
|
686
|
+
|
|
687
|
+
**파라미터:**
|
|
688
|
+
|
|
689
|
+
- `token`: JWT 객체 또는 null
|
|
690
|
+
|
|
691
|
+
**반환값:**
|
|
692
|
+
|
|
693
|
+
- `boolean`
|
|
694
|
+
|
|
695
|
+
#### `isValidToken(token: JWT | null)`
|
|
696
|
+
|
|
697
|
+
토큰이 유효한지 확인합니다.
|
|
698
|
+
|
|
699
|
+
**파라미터:**
|
|
700
|
+
|
|
701
|
+
- `token`: JWT 객체 또는 null
|
|
702
|
+
|
|
703
|
+
**반환값:**
|
|
704
|
+
|
|
705
|
+
- `boolean`
|
|
706
|
+
|
|
707
|
+
### 라이센스 검증
|
|
708
|
+
|
|
709
|
+
#### `checkLicenseKey(licenseKey: string)`
|
|
710
|
+
|
|
711
|
+
라이센스 키를 검증합니다.
|
|
712
|
+
|
|
713
|
+
**파라미터:**
|
|
714
|
+
|
|
715
|
+
- `licenseKey`: 라이센스 키 (필수)
|
|
716
|
+
|
|
717
|
+
**반환값:**
|
|
718
|
+
|
|
719
|
+
- 없음 (유효하지 않으면 에러 발생)
|
|
720
|
+
|
|
721
|
+
**사용 예시:**
|
|
722
|
+
|
|
723
|
+
```typescript
|
|
724
|
+
import { checkLicenseKey } from "@thinkingcat/auth-utils";
|
|
725
|
+
|
|
726
|
+
try {
|
|
727
|
+
checkLicenseKey(process.env.LICENSE_KEY!);
|
|
728
|
+
console.log("라이센스 키가 유효합니다");
|
|
729
|
+
} catch (error) {
|
|
730
|
+
console.error("라이센스 키 검증 실패:", error);
|
|
731
|
+
}
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
### 리다이렉트 및 응답
|
|
735
|
+
|
|
736
|
+
#### `createRedirectHTML(redirectPath: string, text: string)`
|
|
347
737
|
|
|
348
738
|
클라이언트 리다이렉트용 HTML을 생성합니다.
|
|
349
739
|
|
|
350
740
|
**파라미터:**
|
|
351
741
|
|
|
352
742
|
- `redirectPath`: 리다이렉트할 경로
|
|
353
|
-
- `text`: 표시할 텍스트 (
|
|
743
|
+
- `text`: 표시할 텍스트 (필수)
|
|
354
744
|
|
|
355
745
|
**반환값:**
|
|
356
746
|
|
|
@@ -359,14 +749,14 @@ setNextAuthToken(response, sessionToken, {
|
|
|
359
749
|
**사용 예시:**
|
|
360
750
|
|
|
361
751
|
```typescript
|
|
362
|
-
const html = createRedirectHTML("/dashboard", "
|
|
752
|
+
const html = createRedirectHTML("/dashboard", "myservice");
|
|
363
753
|
return new NextResponse(html, {
|
|
364
754
|
status: 200,
|
|
365
755
|
headers: { "Content-Type": "text/html" },
|
|
366
756
|
});
|
|
367
757
|
```
|
|
368
758
|
|
|
369
|
-
|
|
759
|
+
#### `createAuthResponse(accessToken: string, secret: string, options)`
|
|
370
760
|
|
|
371
761
|
완전한 인증 세션을 생성합니다. 토큰 검증부터 쿠키 설정까지 모든 과정을 처리합니다.
|
|
372
762
|
|
|
@@ -380,10 +770,11 @@ return new NextResponse(html, {
|
|
|
380
770
|
|
|
381
771
|
- `refreshToken`: refresh token (선택)
|
|
382
772
|
- `redirectPath`: 리다이렉트할 경로 (선택)
|
|
383
|
-
- `text`: 리다이렉트 HTML에 표시할 텍스트 (
|
|
384
|
-
- `cookiePrefix`: 쿠키 이름 접두사 (
|
|
773
|
+
- `text`: 리다이렉트 HTML에 표시할 텍스트 (선택사항, serviceId가 기본값)
|
|
774
|
+
- `cookiePrefix`: 쿠키 이름 접두사 (선택사항, serviceId가 기본값)
|
|
385
775
|
- `isProduction`: 프로덕션 환경 여부 (기본값: false)
|
|
386
776
|
- `cookieDomain`: 쿠키 도메인 (선택)
|
|
777
|
+
- `serviceId`: 서비스 ID (필수)
|
|
387
778
|
|
|
388
779
|
**반환값:**
|
|
389
780
|
|
|
@@ -396,55 +787,86 @@ const secret = process.env.NEXTAUTH_SECRET!;
|
|
|
396
787
|
const response = await createAuthResponse(accessToken, secret, {
|
|
397
788
|
refreshToken: refreshToken,
|
|
398
789
|
redirectPath: "/dashboard",
|
|
399
|
-
text: "
|
|
400
|
-
cookiePrefix: "
|
|
790
|
+
text: "myservice",
|
|
791
|
+
cookiePrefix: "myservice",
|
|
401
792
|
isProduction: process.env.NODE_ENV === "production",
|
|
402
793
|
cookieDomain: process.env.COOKIE_DOMAIN,
|
|
794
|
+
serviceId: "myservice", // 필수
|
|
403
795
|
});
|
|
404
796
|
```
|
|
405
797
|
|
|
798
|
+
#### `redirectToError(req: NextRequest, errorType: string, message: string, errorPath?: string)`
|
|
799
|
+
|
|
800
|
+
에러 페이지로 리다이렉트합니다.
|
|
801
|
+
|
|
802
|
+
**파라미터:**
|
|
803
|
+
|
|
804
|
+
- `req`: NextRequest 객체
|
|
805
|
+
- `errorType`: 에러 타입
|
|
806
|
+
- `message`: 에러 메시지
|
|
807
|
+
- `errorPath`: 에러 페이지 경로 (기본값: '/error')
|
|
808
|
+
|
|
809
|
+
**반환값:**
|
|
810
|
+
|
|
811
|
+
- NextResponse 리다이렉트 응답
|
|
812
|
+
|
|
813
|
+
#### `redirectToSSOLogin(req: NextRequest, serviceId: string, ssoBaseURL: string)`
|
|
814
|
+
|
|
815
|
+
SSO 로그인 페이지로 리다이렉트합니다.
|
|
816
|
+
|
|
817
|
+
**파라미터:**
|
|
818
|
+
|
|
819
|
+
- `req`: NextRequest 객체
|
|
820
|
+
- `serviceId`: 서비스 ID
|
|
821
|
+
- `ssoBaseURL`: SSO 서버 기본 URL (필수)
|
|
822
|
+
|
|
823
|
+
**반환값:**
|
|
824
|
+
|
|
825
|
+
- NextResponse 리다이렉트 응답
|
|
826
|
+
|
|
827
|
+
#### `redirectToRoleDashboard(req: NextRequest, role: string, rolePaths: Record<string, string>, defaultPath?: string)`
|
|
828
|
+
|
|
829
|
+
역할별 대시보드 경로로 리다이렉트합니다.
|
|
830
|
+
|
|
831
|
+
**파라미터:**
|
|
832
|
+
|
|
833
|
+
- `req`: NextRequest 객체
|
|
834
|
+
- `role`: 사용자 역할
|
|
835
|
+
- `rolePaths`: 역할별 경로 매핑
|
|
836
|
+
- `defaultPath`: 기본 경로 (기본값: '/admin')
|
|
837
|
+
|
|
838
|
+
**반환값:**
|
|
839
|
+
|
|
840
|
+
- NextResponse 리다이렉트 응답
|
|
841
|
+
|
|
406
842
|
## 💡 사용 시나리오
|
|
407
843
|
|
|
408
844
|
### 시나리오 1: 자체 토큰만 사용하는 서비스
|
|
409
845
|
|
|
410
846
|
SSO에서 받은 토큰을 자체 쿠키에만 저장하는 경우:
|
|
411
847
|
|
|
412
|
-
#### 1-1. URL 파라미터로 토큰을 받는 경우 (초기 로그인)
|
|
413
|
-
|
|
414
848
|
```typescript
|
|
415
849
|
import { NextRequest, NextResponse } from "next/server";
|
|
416
850
|
import {
|
|
417
851
|
verifyToken,
|
|
418
852
|
setCustomTokens,
|
|
419
853
|
createRedirectHTML,
|
|
420
|
-
JWTPayload,
|
|
421
854
|
} from "@thinkingcat/auth-utils";
|
|
422
855
|
|
|
423
856
|
export async function GET(req: NextRequest) {
|
|
424
|
-
// URL 파라미터에서 토큰 가져오기 (초기 로그인 시)
|
|
425
857
|
const tokenParam = req.nextUrl.searchParams.get("token");
|
|
426
|
-
|
|
427
858
|
if (!tokenParam) {
|
|
428
859
|
return NextResponse.redirect("/login");
|
|
429
860
|
}
|
|
430
861
|
|
|
431
|
-
// 1. 토큰 검증 (중요: 모든 토큰은 사용 전에 반드시 검증해야 합니다)
|
|
432
862
|
const secret = process.env.NEXTAUTH_SECRET!;
|
|
433
|
-
const
|
|
863
|
+
const licenseKey = process.env.LICENSE_KEY!;
|
|
864
|
+
const tokenResult = await verifyToken(tokenParam, secret, licenseKey);
|
|
434
865
|
|
|
435
|
-
// 2. 검증 실패 시 로그인 페이지로 리다이렉트
|
|
436
866
|
if (!tokenResult) {
|
|
437
|
-
console.error("Token verification failed");
|
|
438
867
|
return NextResponse.redirect("/login");
|
|
439
868
|
}
|
|
440
869
|
|
|
441
|
-
// 3. 검증된 payload 확인 (선택적: 사용자 정보가 필요한 경우)
|
|
442
|
-
const { payload } = tokenResult;
|
|
443
|
-
console.log("Authenticated user:", payload.email);
|
|
444
|
-
|
|
445
|
-
// 4. 검증된 토큰을 쿠키에 저장
|
|
446
|
-
// 주의: verifyToken이 성공했다는 것은 토큰이 유효하고 서명이 올바르다는 의미입니다
|
|
447
|
-
// 따라서 원본 토큰을 안전하게 쿠키에 저장할 수 있습니다
|
|
448
870
|
const html = createRedirectHTML("/dashboard", "myservice");
|
|
449
871
|
const response = new NextResponse(html, {
|
|
450
872
|
status: 200,
|
|
@@ -460,71 +882,6 @@ export async function GET(req: NextRequest) {
|
|
|
460
882
|
}
|
|
461
883
|
```
|
|
462
884
|
|
|
463
|
-
#### 1-2. 쿠키에서 토큰을 읽어서 검증하는 경우 (이미 로그인된 상태)
|
|
464
|
-
|
|
465
|
-
```typescript
|
|
466
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
467
|
-
import { verifyToken, JWTPayload } from "@thinkingcat/auth-utils";
|
|
468
|
-
|
|
469
|
-
export async function GET(req: NextRequest) {
|
|
470
|
-
const secret = process.env.NEXTAUTH_SECRET!;
|
|
471
|
-
|
|
472
|
-
// 1. 쿠키에서 토큰 가져오기 (이미 로그인된 경우)
|
|
473
|
-
const cookieToken = req.cookies.get("myservice_access_token")?.value;
|
|
474
|
-
|
|
475
|
-
if (!cookieToken) {
|
|
476
|
-
// 토큰이 없으면 로그인 페이지로 리다이렉트
|
|
477
|
-
return NextResponse.redirect("/login");
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// 2. 쿠키의 토큰 검증 (중요: 쿠키의 토큰도 반드시 검증해야 합니다)
|
|
481
|
-
const tokenResult = await verifyToken(cookieToken, secret);
|
|
482
|
-
|
|
483
|
-
if (!tokenResult) {
|
|
484
|
-
// 검증 실패 시 쿠키 삭제하고 로그인 페이지로 리다이렉트
|
|
485
|
-
const response = NextResponse.redirect("/login");
|
|
486
|
-
response.cookies.delete("myservice_access_token");
|
|
487
|
-
response.cookies.delete("myservice_refresh_token");
|
|
488
|
-
return response;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// 3. 검증 성공 - 사용자 정보 사용
|
|
492
|
-
const { payload } = tokenResult;
|
|
493
|
-
console.log("Authenticated user:", payload.email);
|
|
494
|
-
|
|
495
|
-
// 4. 정상 응답 반환
|
|
496
|
-
return NextResponse.json({
|
|
497
|
-
success: true,
|
|
498
|
-
user: {
|
|
499
|
-
email: payload.email,
|
|
500
|
-
name: payload.name,
|
|
501
|
-
},
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
**토큰 검증 로직 설명:**
|
|
507
|
-
|
|
508
|
-
1. **`verifyToken(token, secret)`**:
|
|
509
|
-
|
|
510
|
-
- JWT 토큰의 서명을 검증합니다
|
|
511
|
-
- 토큰이 만료되지 않았는지 확인합니다
|
|
512
|
-
- 토큰의 형식이 올바른지 확인합니다
|
|
513
|
-
- 검증 성공 시 `{ payload: JWTPayload }` 반환
|
|
514
|
-
- 검증 실패 시 `null` 반환
|
|
515
|
-
|
|
516
|
-
2. **토큰 소스별 처리**:
|
|
517
|
-
|
|
518
|
-
- **URL 파라미터**: 초기 로그인 시 SSO에서 받은 토큰을 검증 후 쿠키에 저장
|
|
519
|
-
- **쿠키**: 이미 로그인된 상태에서 쿠키의 토큰을 검증하여 사용
|
|
520
|
-
- **중요**: 쿠키에 저장된 토큰도 매번 검증해야 합니다 (토큰이 만료되었을 수 있음)
|
|
521
|
-
|
|
522
|
-
3. **검증 후 처리**:
|
|
523
|
-
- 검증이 성공하면 토큰이 유효하다는 것이 보장됩니다
|
|
524
|
-
- URL 파라미터로 받은 토큰은 검증 후 쿠키에 저장할 수 있습니다
|
|
525
|
-
- 쿠키에서 읽은 토큰은 검증 후 사용할 수 있습니다
|
|
526
|
-
- 필요시 `payload`에서 사용자 정보를 추출하여 사용할 수 있습니다
|
|
527
|
-
|
|
528
885
|
### 시나리오 2: NextAuth만 사용하는 서비스
|
|
529
886
|
|
|
530
887
|
NextAuth 세션만 사용하는 경우:
|
|
@@ -540,20 +897,20 @@ import {
|
|
|
540
897
|
|
|
541
898
|
export async function GET(req: NextRequest) {
|
|
542
899
|
const tokenParam = req.nextUrl.searchParams.get("token");
|
|
543
|
-
|
|
544
900
|
if (!tokenParam) {
|
|
545
901
|
return NextResponse.redirect("/login");
|
|
546
902
|
}
|
|
547
903
|
|
|
548
904
|
const secret = process.env.NEXTAUTH_SECRET!;
|
|
549
|
-
const
|
|
905
|
+
const licenseKey = process.env.LICENSE_KEY!;
|
|
906
|
+
const tokenResult = await verifyToken(tokenParam, secret, licenseKey);
|
|
550
907
|
|
|
551
908
|
if (!tokenResult) {
|
|
552
909
|
return NextResponse.redirect("/login");
|
|
553
910
|
}
|
|
554
911
|
|
|
555
912
|
const { payload } = tokenResult;
|
|
556
|
-
const jwt = createNextAuthJWT(payload);
|
|
913
|
+
const jwt = createNextAuthJWT(payload, "myservice");
|
|
557
914
|
const sessionToken = await encodeNextAuthToken(jwt, secret);
|
|
558
915
|
|
|
559
916
|
const response = NextResponse.redirect("/dashboard");
|
|
@@ -566,7 +923,7 @@ export async function GET(req: NextRequest) {
|
|
|
566
923
|
}
|
|
567
924
|
```
|
|
568
925
|
|
|
569
|
-
### 시나리오 3: 자체 토큰 + NextAuth 모두 사용
|
|
926
|
+
### 시나리오 3: 자체 토큰 + NextAuth 모두 사용
|
|
570
927
|
|
|
571
928
|
자체 토큰과 NextAuth 세션을 모두 사용하는 경우:
|
|
572
929
|
|
|
@@ -584,49 +941,38 @@ import {
|
|
|
584
941
|
|
|
585
942
|
export async function GET(req: NextRequest) {
|
|
586
943
|
const tokenParam = req.nextUrl.searchParams.get("token");
|
|
587
|
-
|
|
588
944
|
if (!tokenParam) {
|
|
589
945
|
return NextResponse.redirect("/login");
|
|
590
946
|
}
|
|
591
947
|
|
|
592
948
|
const secret = process.env.NEXTAUTH_SECRET!;
|
|
949
|
+
const licenseKey = process.env.LICENSE_KEY!;
|
|
593
950
|
const isProduction = process.env.NODE_ENV === "production";
|
|
594
951
|
|
|
595
|
-
|
|
596
|
-
const tokenResult = await verifyToken(tokenParam, secret);
|
|
952
|
+
const tokenResult = await verifyToken(tokenParam, secret, licenseKey);
|
|
597
953
|
if (!tokenResult) {
|
|
598
954
|
return NextResponse.redirect("/login");
|
|
599
955
|
}
|
|
600
956
|
|
|
601
957
|
const { payload } = tokenResult;
|
|
958
|
+
const role = extractRoleFromPayload(payload, "myservice", "ADMIN");
|
|
602
959
|
|
|
603
|
-
|
|
604
|
-
const role = extractRoleFromPayload(payload, "checkon", "ADMIN");
|
|
605
|
-
|
|
606
|
-
// 3. Refresh token 가져오기 (SSO API 호출 등)
|
|
607
|
-
let refreshToken = "";
|
|
608
|
-
// ... refreshToken 가져오는 로직 ...
|
|
609
|
-
|
|
610
|
-
// 4. NextAuth JWT 생성 및 인코딩
|
|
611
|
-
const jwt = createNextAuthJWT(payload);
|
|
960
|
+
const jwt = createNextAuthJWT(payload, "myservice");
|
|
612
961
|
const sessionToken = await encodeNextAuthToken(jwt, secret);
|
|
613
962
|
|
|
614
|
-
// 5. 역할별 리다이렉트 경로 결정
|
|
615
963
|
const redirectPath = role === "ADMIN" ? "/admin" : "/dashboard";
|
|
616
|
-
const html = createRedirectHTML(redirectPath, "
|
|
964
|
+
const html = createRedirectHTML(redirectPath, "myservice");
|
|
617
965
|
|
|
618
966
|
const response = new NextResponse(html, {
|
|
619
967
|
status: 200,
|
|
620
968
|
headers: { "Content-Type": "text/html" },
|
|
621
969
|
});
|
|
622
970
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
cookiePrefix: "checkon",
|
|
971
|
+
setCustomTokens(response, tokenParam, {
|
|
972
|
+
cookiePrefix: "myservice",
|
|
626
973
|
isProduction,
|
|
627
974
|
});
|
|
628
975
|
|
|
629
|
-
// 7. NextAuth 토큰 설정
|
|
630
976
|
setNextAuthToken(response, sessionToken, {
|
|
631
977
|
isProduction,
|
|
632
978
|
cookieDomain: process.env.COOKIE_DOMAIN,
|
|
@@ -636,71 +982,60 @@ export async function GET(req: NextRequest) {
|
|
|
636
982
|
}
|
|
637
983
|
```
|
|
638
984
|
|
|
639
|
-
### 시나리오 4:
|
|
985
|
+
### 시나리오 4: 미들웨어에서 통합 사용
|
|
640
986
|
|
|
641
|
-
Next.js Middleware에서
|
|
987
|
+
Next.js Middleware에서 통합 미들웨어 핸들러 사용:
|
|
642
988
|
|
|
643
989
|
```typescript
|
|
990
|
+
import { withAuth } from "next-auth/middleware";
|
|
644
991
|
import { NextRequest, NextResponse } from "next/server";
|
|
992
|
+
import { getToken } from "next-auth/jwt";
|
|
645
993
|
import {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
setNextAuthToken,
|
|
649
|
-
createNextAuthJWT,
|
|
650
|
-
encodeNextAuthToken,
|
|
994
|
+
createMiddlewareConfig,
|
|
995
|
+
handleMiddleware,
|
|
651
996
|
} from "@thinkingcat/auth-utils";
|
|
652
997
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const refreshToken = req.cookies.get("checkon_refresh_token")?.value;
|
|
670
|
-
|
|
671
|
-
if (refreshToken) {
|
|
672
|
-
// SSO API로 토큰 리프레시
|
|
673
|
-
// ... refreshSSOToken 호출 ...
|
|
674
|
-
|
|
675
|
-
if (newAccessToken) {
|
|
676
|
-
const newTokenResult = await verifyToken(newAccessToken, secret);
|
|
677
|
-
|
|
678
|
-
if (newTokenResult) {
|
|
679
|
-
const { payload } = newTokenResult;
|
|
680
|
-
const jwt = createNextAuthJWT(payload);
|
|
681
|
-
const sessionToken = await encodeNextAuthToken(jwt, secret);
|
|
682
|
-
|
|
683
|
-
const response = NextResponse.next();
|
|
684
|
-
|
|
685
|
-
setCustomTokens(response, newAccessToken, {
|
|
686
|
-
cookiePrefix: "checkon",
|
|
687
|
-
isProduction,
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
setNextAuthToken(response, sessionToken, {
|
|
691
|
-
isProduction,
|
|
692
|
-
cookieDomain: process.env.COOKIE_DOMAIN,
|
|
693
|
-
});
|
|
998
|
+
const middlewareConfig = createMiddlewareConfig({
|
|
999
|
+
serviceId: "myservice",
|
|
1000
|
+
publicPaths: ["/login", "/register", "/api/public"],
|
|
1001
|
+
subscriptionRequiredPaths: ["/premium"],
|
|
1002
|
+
roleAccessConfig: [
|
|
1003
|
+
{
|
|
1004
|
+
paths: ["/admin"],
|
|
1005
|
+
role: "ADMIN",
|
|
1006
|
+
message: "관리자만 접근할 수 있습니다.",
|
|
1007
|
+
},
|
|
1008
|
+
],
|
|
1009
|
+
rolePaths: {
|
|
1010
|
+
ADMIN: "/admin",
|
|
1011
|
+
USER: "/dashboard",
|
|
1012
|
+
},
|
|
1013
|
+
});
|
|
694
1014
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
1015
|
+
export default withAuth(
|
|
1016
|
+
async function middleware(req: NextRequest) {
|
|
1017
|
+
const response = await handleMiddleware(req, middlewareConfig, {
|
|
1018
|
+
secret: process.env.NEXTAUTH_SECRET!,
|
|
1019
|
+
isProduction: process.env.NODE_ENV === "production",
|
|
1020
|
+
cookieDomain: process.env.COOKIE_DOMAIN,
|
|
1021
|
+
ssoBaseURL: process.env.SSO_BASE_URL!,
|
|
1022
|
+
getNextAuthToken: async (req: NextRequest) => {
|
|
1023
|
+
return await getToken({ req, secret: process.env.NEXTAUTH_SECRET! });
|
|
1024
|
+
},
|
|
1025
|
+
authServiceKey: process.env.AUTH_SERVICE_SECRET_KEY,
|
|
1026
|
+
});
|
|
1027
|
+
return response || NextResponse.next();
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
callbacks: {
|
|
1031
|
+
authorized: () => true,
|
|
1032
|
+
},
|
|
699
1033
|
}
|
|
1034
|
+
);
|
|
700
1035
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
}
|
|
1036
|
+
export const config = {
|
|
1037
|
+
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
|
1038
|
+
};
|
|
704
1039
|
```
|
|
705
1040
|
|
|
706
1041
|
## 🔒 보안 고려사항
|
|
@@ -720,9 +1055,15 @@ export async function middleware(req: NextRequest) {
|
|
|
720
1055
|
- `httpOnly: true`로 설정되어 JavaScript에서 접근할 수 없습니다
|
|
721
1056
|
|
|
722
1057
|
3. **토큰 검증**
|
|
1058
|
+
|
|
723
1059
|
- 모든 토큰은 사용 전에 반드시 `verifyToken`으로 검증하세요
|
|
724
1060
|
- 검증 실패 시 적절한 에러 처리를 하세요
|
|
725
1061
|
|
|
1062
|
+
4. **환경 변수**
|
|
1063
|
+
- 이 패키지는 환경 변수를 직접 읽지 않습니다
|
|
1064
|
+
- 모든 값은 함수 파라미터로 전달해야 합니다
|
|
1065
|
+
- 하드코딩된 값(서비스 ID, URL 등)이 없도록 주의하세요
|
|
1066
|
+
|
|
726
1067
|
## 📝 타입 정의
|
|
727
1068
|
|
|
728
1069
|
### `JWTPayload`
|
|
@@ -789,31 +1130,48 @@ interface ServiceInfo {
|
|
|
789
1130
|
}
|
|
790
1131
|
```
|
|
791
1132
|
|
|
792
|
-
###
|
|
1133
|
+
### `MiddlewareConfig`
|
|
1134
|
+
|
|
1135
|
+
```typescript
|
|
1136
|
+
interface MiddlewareConfig {
|
|
1137
|
+
serviceId: string;
|
|
1138
|
+
publicPaths: string[];
|
|
1139
|
+
subscriptionRequiredPaths: string[];
|
|
1140
|
+
subscriptionExemptApiPaths: string[];
|
|
1141
|
+
authApiPaths: string[];
|
|
1142
|
+
roleAccessConfig: RoleAccessConfig[];
|
|
1143
|
+
rolePaths: Record<string, string>;
|
|
1144
|
+
systemAdminRole: string;
|
|
1145
|
+
errorPath: string;
|
|
1146
|
+
}
|
|
1147
|
+
```
|
|
793
1148
|
|
|
794
|
-
|
|
1149
|
+
### `MiddlewareOptions`
|
|
795
1150
|
|
|
796
1151
|
```typescript
|
|
797
|
-
interface
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
};
|
|
1152
|
+
interface MiddlewareOptions {
|
|
1153
|
+
secret: string;
|
|
1154
|
+
isProduction: boolean;
|
|
1155
|
+
cookieDomain?: string;
|
|
1156
|
+
getNextAuthToken?: (req: NextRequest) => Promise<JWT | null>;
|
|
1157
|
+
ssoBaseURL: string;
|
|
1158
|
+
authServiceKey?: string;
|
|
805
1159
|
}
|
|
806
1160
|
```
|
|
807
1161
|
|
|
808
|
-
|
|
1162
|
+
### `RoleAccessConfig`
|
|
809
1163
|
|
|
810
1164
|
```typescript
|
|
811
|
-
interface
|
|
812
|
-
|
|
1165
|
+
interface RoleAccessConfig {
|
|
1166
|
+
paths: string[];
|
|
1167
|
+
role: string;
|
|
1168
|
+
allowedRoles?: string[];
|
|
813
1169
|
message?: string;
|
|
814
1170
|
}
|
|
815
1171
|
```
|
|
816
1172
|
|
|
1173
|
+
### SSO API 응답 타입
|
|
1174
|
+
|
|
817
1175
|
#### `SSORefreshTokenResponse`
|
|
818
1176
|
|
|
819
1177
|
```typescript
|
|
@@ -840,44 +1198,6 @@ interface SSOGetRefreshTokenResponse {
|
|
|
840
1198
|
}
|
|
841
1199
|
```
|
|
842
1200
|
|
|
843
|
-
#### `SSORegisterResponse`
|
|
844
|
-
|
|
845
|
-
```typescript
|
|
846
|
-
interface SSORegisterResponse {
|
|
847
|
-
success: boolean;
|
|
848
|
-
message?: string;
|
|
849
|
-
user?: {
|
|
850
|
-
id: string;
|
|
851
|
-
email: string;
|
|
852
|
-
name: string;
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
|
-
```
|
|
856
|
-
|
|
857
|
-
### 사용 예시
|
|
858
|
-
|
|
859
|
-
```typescript
|
|
860
|
-
import type {
|
|
861
|
-
SSOUpsertResponse,
|
|
862
|
-
SSOErrorResponse,
|
|
863
|
-
SSORefreshTokenResponse,
|
|
864
|
-
SSOGetRefreshTokenResponse,
|
|
865
|
-
SSORegisterResponse,
|
|
866
|
-
} from "@thinkingcat/auth-utils";
|
|
867
|
-
|
|
868
|
-
// SSO API 호출 시 타입 사용
|
|
869
|
-
const response = await fetch("/api/sso/refresh", {
|
|
870
|
-
method: "POST",
|
|
871
|
-
body: JSON.stringify({ refreshToken }),
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
const result = (await response.json()) as SSORefreshTokenResponse;
|
|
875
|
-
if (result.success && result.accessToken) {
|
|
876
|
-
// 타입 안전하게 사용
|
|
877
|
-
console.log(result.accessToken);
|
|
878
|
-
}
|
|
879
|
-
```
|
|
880
|
-
|
|
881
1201
|
### `ResponseLike`
|
|
882
1202
|
|
|
883
1203
|
```typescript
|
|
@@ -936,10 +1256,35 @@ npm install
|
|
|
936
1256
|
- 브라우저 개발자 도구에서 쿠키 설정 확인
|
|
937
1257
|
- 도메인 및 경로 설정 확인
|
|
938
1258
|
|
|
1259
|
+
### 문제 5: "ssoBaseURL is required" 에러
|
|
1260
|
+
|
|
1261
|
+
**해결 방법:**
|
|
1262
|
+
모든 SSO 관련 함수는 `ssoBaseURL`을 필수 파라미터로 받습니다. 환경 변수에서 값을 가져와서 전달하세요:
|
|
1263
|
+
|
|
1264
|
+
```typescript
|
|
1265
|
+
const ssoBaseURL = process.env.SSO_BASE_URL!;
|
|
1266
|
+
if (!ssoBaseURL) {
|
|
1267
|
+
throw new Error("SSO_BASE_URL environment variable is required");
|
|
1268
|
+
}
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1271
|
+
### 문제 6: "cookiePrefix is required" 에러
|
|
1272
|
+
|
|
1273
|
+
**해결 방법:**
|
|
1274
|
+
`setCustomTokens`와 `clearAuthCookies` 함수는 `cookiePrefix`를 필수 파라미터로 받습니다. 서비스 ID를 사용하거나 명시적으로 전달하세요:
|
|
1275
|
+
|
|
1276
|
+
```typescript
|
|
1277
|
+
const cookiePrefix = "myservice"; // 서비스별로 변경
|
|
1278
|
+
setCustomTokens(response, accessToken, {
|
|
1279
|
+
cookiePrefix, // 필수
|
|
1280
|
+
isProduction: true,
|
|
1281
|
+
});
|
|
1282
|
+
```
|
|
1283
|
+
|
|
939
1284
|
## 📦 패키지 정보
|
|
940
1285
|
|
|
941
1286
|
- **패키지명**: `@thinkingcat/auth-utils`
|
|
942
|
-
- **버전**: `1.0.
|
|
1287
|
+
- **버전**: `1.0.8`
|
|
943
1288
|
- **라이선스**: MIT
|
|
944
1289
|
- **저장소**: npm registry
|
|
945
1290
|
|