@thinkingcat/auth-utils 1.0.7 → 1.0.10
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 +654 -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.10"
|
|
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,12 @@ 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 (필수)
|
|
778
|
+
- `licenseKey`: 라이센스 키 (필수)
|
|
387
779
|
|
|
388
780
|
**반환값:**
|
|
389
781
|
|
|
@@ -396,55 +788,87 @@ const secret = process.env.NEXTAUTH_SECRET!;
|
|
|
396
788
|
const response = await createAuthResponse(accessToken, secret, {
|
|
397
789
|
refreshToken: refreshToken,
|
|
398
790
|
redirectPath: "/dashboard",
|
|
399
|
-
text: "
|
|
400
|
-
cookiePrefix: "
|
|
791
|
+
text: "myservice",
|
|
792
|
+
cookiePrefix: "myservice",
|
|
401
793
|
isProduction: process.env.NODE_ENV === "production",
|
|
402
794
|
cookieDomain: process.env.COOKIE_DOMAIN,
|
|
795
|
+
serviceId: "myservice",
|
|
796
|
+
licenseKey: process.env.LICENSE_KEY!, // 필수
|
|
403
797
|
});
|
|
404
798
|
```
|
|
405
799
|
|
|
800
|
+
#### `redirectToError(req: NextRequest, errorType: string, message: string, errorPath?: string)`
|
|
801
|
+
|
|
802
|
+
에러 페이지로 리다이렉트합니다.
|
|
803
|
+
|
|
804
|
+
**파라미터:**
|
|
805
|
+
|
|
806
|
+
- `req`: NextRequest 객체
|
|
807
|
+
- `errorType`: 에러 타입
|
|
808
|
+
- `message`: 에러 메시지
|
|
809
|
+
- `errorPath`: 에러 페이지 경로 (기본값: '/error')
|
|
810
|
+
|
|
811
|
+
**반환값:**
|
|
812
|
+
|
|
813
|
+
- NextResponse 리다이렉트 응답
|
|
814
|
+
|
|
815
|
+
#### `redirectToSSOLogin(req: NextRequest, serviceId: string, ssoBaseURL: string)`
|
|
816
|
+
|
|
817
|
+
SSO 로그인 페이지로 리다이렉트합니다.
|
|
818
|
+
|
|
819
|
+
**파라미터:**
|
|
820
|
+
|
|
821
|
+
- `req`: NextRequest 객체
|
|
822
|
+
- `serviceId`: 서비스 ID
|
|
823
|
+
- `ssoBaseURL`: SSO 서버 기본 URL (필수)
|
|
824
|
+
|
|
825
|
+
**반환값:**
|
|
826
|
+
|
|
827
|
+
- NextResponse 리다이렉트 응답
|
|
828
|
+
|
|
829
|
+
#### `redirectToRoleDashboard(req: NextRequest, role: string, rolePaths: Record<string, string>, defaultPath?: string)`
|
|
830
|
+
|
|
831
|
+
역할별 대시보드 경로로 리다이렉트합니다.
|
|
832
|
+
|
|
833
|
+
**파라미터:**
|
|
834
|
+
|
|
835
|
+
- `req`: NextRequest 객체
|
|
836
|
+
- `role`: 사용자 역할
|
|
837
|
+
- `rolePaths`: 역할별 경로 매핑
|
|
838
|
+
- `defaultPath`: 기본 경로 (기본값: '/admin')
|
|
839
|
+
|
|
840
|
+
**반환값:**
|
|
841
|
+
|
|
842
|
+
- NextResponse 리다이렉트 응답
|
|
843
|
+
|
|
406
844
|
## 💡 사용 시나리오
|
|
407
845
|
|
|
408
846
|
### 시나리오 1: 자체 토큰만 사용하는 서비스
|
|
409
847
|
|
|
410
848
|
SSO에서 받은 토큰을 자체 쿠키에만 저장하는 경우:
|
|
411
849
|
|
|
412
|
-
#### 1-1. URL 파라미터로 토큰을 받는 경우 (초기 로그인)
|
|
413
|
-
|
|
414
850
|
```typescript
|
|
415
851
|
import { NextRequest, NextResponse } from "next/server";
|
|
416
852
|
import {
|
|
417
853
|
verifyToken,
|
|
418
854
|
setCustomTokens,
|
|
419
855
|
createRedirectHTML,
|
|
420
|
-
JWTPayload,
|
|
421
856
|
} from "@thinkingcat/auth-utils";
|
|
422
857
|
|
|
423
858
|
export async function GET(req: NextRequest) {
|
|
424
|
-
// URL 파라미터에서 토큰 가져오기 (초기 로그인 시)
|
|
425
859
|
const tokenParam = req.nextUrl.searchParams.get("token");
|
|
426
|
-
|
|
427
860
|
if (!tokenParam) {
|
|
428
861
|
return NextResponse.redirect("/login");
|
|
429
862
|
}
|
|
430
863
|
|
|
431
|
-
// 1. 토큰 검증 (중요: 모든 토큰은 사용 전에 반드시 검증해야 합니다)
|
|
432
864
|
const secret = process.env.NEXTAUTH_SECRET!;
|
|
433
|
-
const
|
|
865
|
+
const licenseKey = process.env.LICENSE_KEY!;
|
|
866
|
+
const tokenResult = await verifyToken(tokenParam, secret, licenseKey);
|
|
434
867
|
|
|
435
|
-
// 2. 검증 실패 시 로그인 페이지로 리다이렉트
|
|
436
868
|
if (!tokenResult) {
|
|
437
|
-
console.error("Token verification failed");
|
|
438
869
|
return NextResponse.redirect("/login");
|
|
439
870
|
}
|
|
440
871
|
|
|
441
|
-
// 3. 검증된 payload 확인 (선택적: 사용자 정보가 필요한 경우)
|
|
442
|
-
const { payload } = tokenResult;
|
|
443
|
-
console.log("Authenticated user:", payload.email);
|
|
444
|
-
|
|
445
|
-
// 4. 검증된 토큰을 쿠키에 저장
|
|
446
|
-
// 주의: verifyToken이 성공했다는 것은 토큰이 유효하고 서명이 올바르다는 의미입니다
|
|
447
|
-
// 따라서 원본 토큰을 안전하게 쿠키에 저장할 수 있습니다
|
|
448
872
|
const html = createRedirectHTML("/dashboard", "myservice");
|
|
449
873
|
const response = new NextResponse(html, {
|
|
450
874
|
status: 200,
|
|
@@ -460,71 +884,6 @@ export async function GET(req: NextRequest) {
|
|
|
460
884
|
}
|
|
461
885
|
```
|
|
462
886
|
|
|
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
887
|
### 시나리오 2: NextAuth만 사용하는 서비스
|
|
529
888
|
|
|
530
889
|
NextAuth 세션만 사용하는 경우:
|
|
@@ -540,20 +899,20 @@ import {
|
|
|
540
899
|
|
|
541
900
|
export async function GET(req: NextRequest) {
|
|
542
901
|
const tokenParam = req.nextUrl.searchParams.get("token");
|
|
543
|
-
|
|
544
902
|
if (!tokenParam) {
|
|
545
903
|
return NextResponse.redirect("/login");
|
|
546
904
|
}
|
|
547
905
|
|
|
548
906
|
const secret = process.env.NEXTAUTH_SECRET!;
|
|
549
|
-
const
|
|
907
|
+
const licenseKey = process.env.LICENSE_KEY!;
|
|
908
|
+
const tokenResult = await verifyToken(tokenParam, secret, licenseKey);
|
|
550
909
|
|
|
551
910
|
if (!tokenResult) {
|
|
552
911
|
return NextResponse.redirect("/login");
|
|
553
912
|
}
|
|
554
913
|
|
|
555
914
|
const { payload } = tokenResult;
|
|
556
|
-
const jwt = createNextAuthJWT(payload);
|
|
915
|
+
const jwt = createNextAuthJWT(payload, "myservice");
|
|
557
916
|
const sessionToken = await encodeNextAuthToken(jwt, secret);
|
|
558
917
|
|
|
559
918
|
const response = NextResponse.redirect("/dashboard");
|
|
@@ -566,7 +925,7 @@ export async function GET(req: NextRequest) {
|
|
|
566
925
|
}
|
|
567
926
|
```
|
|
568
927
|
|
|
569
|
-
### 시나리오 3: 자체 토큰 + NextAuth 모두 사용
|
|
928
|
+
### 시나리오 3: 자체 토큰 + NextAuth 모두 사용
|
|
570
929
|
|
|
571
930
|
자체 토큰과 NextAuth 세션을 모두 사용하는 경우:
|
|
572
931
|
|
|
@@ -584,49 +943,38 @@ import {
|
|
|
584
943
|
|
|
585
944
|
export async function GET(req: NextRequest) {
|
|
586
945
|
const tokenParam = req.nextUrl.searchParams.get("token");
|
|
587
|
-
|
|
588
946
|
if (!tokenParam) {
|
|
589
947
|
return NextResponse.redirect("/login");
|
|
590
948
|
}
|
|
591
949
|
|
|
592
950
|
const secret = process.env.NEXTAUTH_SECRET!;
|
|
951
|
+
const licenseKey = process.env.LICENSE_KEY!;
|
|
593
952
|
const isProduction = process.env.NODE_ENV === "production";
|
|
594
953
|
|
|
595
|
-
|
|
596
|
-
const tokenResult = await verifyToken(tokenParam, secret);
|
|
954
|
+
const tokenResult = await verifyToken(tokenParam, secret, licenseKey);
|
|
597
955
|
if (!tokenResult) {
|
|
598
956
|
return NextResponse.redirect("/login");
|
|
599
957
|
}
|
|
600
958
|
|
|
601
959
|
const { payload } = tokenResult;
|
|
960
|
+
const role = extractRoleFromPayload(payload, "myservice", "ADMIN");
|
|
602
961
|
|
|
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);
|
|
962
|
+
const jwt = createNextAuthJWT(payload, "myservice");
|
|
612
963
|
const sessionToken = await encodeNextAuthToken(jwt, secret);
|
|
613
964
|
|
|
614
|
-
// 5. 역할별 리다이렉트 경로 결정
|
|
615
965
|
const redirectPath = role === "ADMIN" ? "/admin" : "/dashboard";
|
|
616
|
-
const html = createRedirectHTML(redirectPath, "
|
|
966
|
+
const html = createRedirectHTML(redirectPath, "myservice");
|
|
617
967
|
|
|
618
968
|
const response = new NextResponse(html, {
|
|
619
969
|
status: 200,
|
|
620
970
|
headers: { "Content-Type": "text/html" },
|
|
621
971
|
});
|
|
622
972
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
cookiePrefix: "checkon",
|
|
973
|
+
setCustomTokens(response, tokenParam, {
|
|
974
|
+
cookiePrefix: "myservice",
|
|
626
975
|
isProduction,
|
|
627
976
|
});
|
|
628
977
|
|
|
629
|
-
// 7. NextAuth 토큰 설정
|
|
630
978
|
setNextAuthToken(response, sessionToken, {
|
|
631
979
|
isProduction,
|
|
632
980
|
cookieDomain: process.env.COOKIE_DOMAIN,
|
|
@@ -636,71 +984,60 @@ export async function GET(req: NextRequest) {
|
|
|
636
984
|
}
|
|
637
985
|
```
|
|
638
986
|
|
|
639
|
-
### 시나리오 4:
|
|
987
|
+
### 시나리오 4: 미들웨어에서 통합 사용
|
|
640
988
|
|
|
641
|
-
Next.js Middleware에서
|
|
989
|
+
Next.js Middleware에서 통합 미들웨어 핸들러 사용:
|
|
642
990
|
|
|
643
991
|
```typescript
|
|
992
|
+
import { withAuth } from "next-auth/middleware";
|
|
644
993
|
import { NextRequest, NextResponse } from "next/server";
|
|
994
|
+
import { getToken } from "next-auth/jwt";
|
|
645
995
|
import {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
setNextAuthToken,
|
|
649
|
-
createNextAuthJWT,
|
|
650
|
-
encodeNextAuthToken,
|
|
996
|
+
createMiddlewareConfig,
|
|
997
|
+
handleMiddleware,
|
|
651
998
|
} from "@thinkingcat/auth-utils";
|
|
652
999
|
|
|
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
|
-
});
|
|
1000
|
+
const middlewareConfig = createMiddlewareConfig({
|
|
1001
|
+
serviceId: "myservice",
|
|
1002
|
+
publicPaths: ["/login", "/register", "/api/public"],
|
|
1003
|
+
subscriptionRequiredPaths: ["/premium"],
|
|
1004
|
+
roleAccessConfig: [
|
|
1005
|
+
{
|
|
1006
|
+
paths: ["/admin"],
|
|
1007
|
+
role: "ADMIN",
|
|
1008
|
+
message: "관리자만 접근할 수 있습니다.",
|
|
1009
|
+
},
|
|
1010
|
+
],
|
|
1011
|
+
rolePaths: {
|
|
1012
|
+
ADMIN: "/admin",
|
|
1013
|
+
USER: "/dashboard",
|
|
1014
|
+
},
|
|
1015
|
+
});
|
|
694
1016
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
1017
|
+
export default withAuth(
|
|
1018
|
+
async function middleware(req: NextRequest) {
|
|
1019
|
+
const response = await handleMiddleware(req, middlewareConfig, {
|
|
1020
|
+
secret: process.env.NEXTAUTH_SECRET!,
|
|
1021
|
+
isProduction: process.env.NODE_ENV === "production",
|
|
1022
|
+
cookieDomain: process.env.COOKIE_DOMAIN,
|
|
1023
|
+
ssoBaseURL: process.env.SSO_BASE_URL!,
|
|
1024
|
+
getNextAuthToken: async (req: NextRequest) => {
|
|
1025
|
+
return await getToken({ req, secret: process.env.NEXTAUTH_SECRET! });
|
|
1026
|
+
},
|
|
1027
|
+
authServiceKey: process.env.AUTH_SERVICE_SECRET_KEY,
|
|
1028
|
+
});
|
|
1029
|
+
return response || NextResponse.next();
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
callbacks: {
|
|
1033
|
+
authorized: () => true,
|
|
1034
|
+
},
|
|
699
1035
|
}
|
|
1036
|
+
);
|
|
700
1037
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
}
|
|
1038
|
+
export const config = {
|
|
1039
|
+
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
|
1040
|
+
};
|
|
704
1041
|
```
|
|
705
1042
|
|
|
706
1043
|
## 🔒 보안 고려사항
|
|
@@ -720,9 +1057,15 @@ export async function middleware(req: NextRequest) {
|
|
|
720
1057
|
- `httpOnly: true`로 설정되어 JavaScript에서 접근할 수 없습니다
|
|
721
1058
|
|
|
722
1059
|
3. **토큰 검증**
|
|
1060
|
+
|
|
723
1061
|
- 모든 토큰은 사용 전에 반드시 `verifyToken`으로 검증하세요
|
|
724
1062
|
- 검증 실패 시 적절한 에러 처리를 하세요
|
|
725
1063
|
|
|
1064
|
+
4. **환경 변수**
|
|
1065
|
+
- 이 패키지는 환경 변수를 직접 읽지 않습니다
|
|
1066
|
+
- 모든 값은 함수 파라미터로 전달해야 합니다
|
|
1067
|
+
- 하드코딩된 값(서비스 ID, URL 등)이 없도록 주의하세요
|
|
1068
|
+
|
|
726
1069
|
## 📝 타입 정의
|
|
727
1070
|
|
|
728
1071
|
### `JWTPayload`
|
|
@@ -789,31 +1132,49 @@ interface ServiceInfo {
|
|
|
789
1132
|
}
|
|
790
1133
|
```
|
|
791
1134
|
|
|
792
|
-
###
|
|
1135
|
+
### `MiddlewareConfig`
|
|
1136
|
+
|
|
1137
|
+
```typescript
|
|
1138
|
+
interface MiddlewareConfig {
|
|
1139
|
+
serviceId: string;
|
|
1140
|
+
publicPaths: string[];
|
|
1141
|
+
subscriptionRequiredPaths: string[];
|
|
1142
|
+
subscriptionExemptApiPaths: string[];
|
|
1143
|
+
authApiPaths: string[];
|
|
1144
|
+
roleAccessConfig: RoleAccessConfig[];
|
|
1145
|
+
rolePaths: Record<string, string>;
|
|
1146
|
+
systemAdminRole: string;
|
|
1147
|
+
errorPath: string;
|
|
1148
|
+
}
|
|
1149
|
+
```
|
|
793
1150
|
|
|
794
|
-
|
|
1151
|
+
### `MiddlewareOptions`
|
|
795
1152
|
|
|
796
1153
|
```typescript
|
|
797
|
-
interface
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
1154
|
+
interface MiddlewareOptions {
|
|
1155
|
+
secret: string;
|
|
1156
|
+
isProduction: boolean;
|
|
1157
|
+
cookieDomain?: string;
|
|
1158
|
+
getNextAuthToken?: (req: NextRequest) => Promise<JWT | null>;
|
|
1159
|
+
ssoBaseURL: string;
|
|
1160
|
+
authServiceKey?: string;
|
|
1161
|
+
licenseKey: string;
|
|
805
1162
|
}
|
|
806
1163
|
```
|
|
807
1164
|
|
|
808
|
-
|
|
1165
|
+
### `RoleAccessConfig`
|
|
809
1166
|
|
|
810
1167
|
```typescript
|
|
811
|
-
interface
|
|
812
|
-
|
|
1168
|
+
interface RoleAccessConfig {
|
|
1169
|
+
paths: string[];
|
|
1170
|
+
role: string;
|
|
1171
|
+
allowedRoles?: string[];
|
|
813
1172
|
message?: string;
|
|
814
1173
|
}
|
|
815
1174
|
```
|
|
816
1175
|
|
|
1176
|
+
### SSO API 응답 타입
|
|
1177
|
+
|
|
817
1178
|
#### `SSORefreshTokenResponse`
|
|
818
1179
|
|
|
819
1180
|
```typescript
|
|
@@ -840,44 +1201,6 @@ interface SSOGetRefreshTokenResponse {
|
|
|
840
1201
|
}
|
|
841
1202
|
```
|
|
842
1203
|
|
|
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
1204
|
### `ResponseLike`
|
|
882
1205
|
|
|
883
1206
|
```typescript
|
|
@@ -936,10 +1259,55 @@ npm install
|
|
|
936
1259
|
- 브라우저 개발자 도구에서 쿠키 설정 확인
|
|
937
1260
|
- 도메인 및 경로 설정 확인
|
|
938
1261
|
|
|
1262
|
+
### 문제 5: "ssoBaseURL is required" 에러
|
|
1263
|
+
|
|
1264
|
+
**해결 방법:**
|
|
1265
|
+
모든 SSO 관련 함수는 `ssoBaseURL`을 필수 파라미터로 받습니다. 환경 변수에서 값을 가져와서 전달하세요:
|
|
1266
|
+
|
|
1267
|
+
```typescript
|
|
1268
|
+
const ssoBaseURL = process.env.SSO_BASE_URL!;
|
|
1269
|
+
if (!ssoBaseURL) {
|
|
1270
|
+
throw new Error("SSO_BASE_URL environment variable is required");
|
|
1271
|
+
}
|
|
1272
|
+
```
|
|
1273
|
+
|
|
1274
|
+
### 문제 6: "cookiePrefix is required" 에러
|
|
1275
|
+
|
|
1276
|
+
**해결 방법:**
|
|
1277
|
+
`setCustomTokens`와 `clearAuthCookies` 함수는 `cookiePrefix`를 필수 파라미터로 받습니다. 서비스 ID를 사용하거나 명시적으로 전달하세요:
|
|
1278
|
+
|
|
1279
|
+
```typescript
|
|
1280
|
+
const cookiePrefix = "myservice"; // 서비스별로 변경
|
|
1281
|
+
setCustomTokens(response, accessToken, {
|
|
1282
|
+
cookiePrefix, // 필수
|
|
1283
|
+
isProduction: true,
|
|
1284
|
+
});
|
|
1285
|
+
```
|
|
1286
|
+
|
|
1287
|
+
### 문제 7: "License key is required" 또는 "Invalid license key" 에러
|
|
1288
|
+
|
|
1289
|
+
**해결 방법:**
|
|
1290
|
+
라이센스 키는 모든 함수 호출 시 필수입니다. 환경 변수에서 라이센스 키를 가져와서 전달하세요:
|
|
1291
|
+
|
|
1292
|
+
```typescript
|
|
1293
|
+
const licenseKey = process.env.LICENSE_KEY;
|
|
1294
|
+
if (!licenseKey) {
|
|
1295
|
+
throw new Error("LICENSE_KEY environment variable is required");
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// 함수 호출 시 licenseKey 전달
|
|
1299
|
+
const response = await handleMiddleware(req, middlewareConfig, {
|
|
1300
|
+
secret: process.env.NEXTAUTH_SECRET!,
|
|
1301
|
+
licenseKey, // 필수
|
|
1302
|
+
ssoBaseURL: process.env.SSO_BASE_URL!,
|
|
1303
|
+
// ... 기타 옵션
|
|
1304
|
+
});
|
|
1305
|
+
```
|
|
1306
|
+
|
|
939
1307
|
## 📦 패키지 정보
|
|
940
1308
|
|
|
941
1309
|
- **패키지명**: `@thinkingcat/auth-utils`
|
|
942
|
-
- **버전**: `1.0.
|
|
1310
|
+
- **버전**: `1.0.10`
|
|
943
1311
|
- **라이선스**: MIT
|
|
944
1312
|
- **저장소**: npm registry
|
|
945
1313
|
|