@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/dist/index.js
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.verifyToken = verifyToken;
|
|
4
37
|
exports.extractRoleFromPayload = extractRoleFromPayload;
|
|
@@ -16,6 +49,7 @@ exports.redirectToError = redirectToError;
|
|
|
16
49
|
exports.clearAuthCookies = clearAuthCookies;
|
|
17
50
|
exports.getEffectiveRole = getEffectiveRole;
|
|
18
51
|
exports.requiresSubscription = requiresSubscription;
|
|
52
|
+
exports.checkLicenseKey = checkLicenseKey;
|
|
19
53
|
exports.checkRoleAccess = checkRoleAccess;
|
|
20
54
|
exports.redirectToSSOLogin = redirectToSSOLogin;
|
|
21
55
|
exports.redirectToRoleDashboard = redirectToRoleDashboard;
|
|
@@ -33,6 +67,7 @@ exports.handleMiddleware = handleMiddleware;
|
|
|
33
67
|
const jwt_1 = require("next-auth/jwt");
|
|
34
68
|
const jose_1 = require("jose");
|
|
35
69
|
const server_1 = require("next/server");
|
|
70
|
+
const crypto_1 = require("crypto");
|
|
36
71
|
/**
|
|
37
72
|
* 토큰 검증 및 디코딩
|
|
38
73
|
* @param accessToken JWT access token
|
|
@@ -55,11 +90,11 @@ async function verifyToken(accessToken, secret) {
|
|
|
55
90
|
/**
|
|
56
91
|
* payload에서 역할 추출 (서비스별)
|
|
57
92
|
* @param payload JWT payload
|
|
58
|
-
* @param serviceId 서비스 ID (
|
|
93
|
+
* @param serviceId 서비스 ID (필수)
|
|
59
94
|
* @param defaultRole 기본 역할 (기본값: 'ADMIN')
|
|
60
95
|
* @returns 추출된 역할
|
|
61
96
|
*/
|
|
62
|
-
function extractRoleFromPayload(payload, serviceId
|
|
97
|
+
function extractRoleFromPayload(payload, serviceId, defaultRole = 'ADMIN') {
|
|
63
98
|
const services = payload.services || [];
|
|
64
99
|
const service = services.find((s) => s.serviceId === serviceId);
|
|
65
100
|
return service?.role || payload.role || defaultRole;
|
|
@@ -67,18 +102,19 @@ function extractRoleFromPayload(payload, serviceId = 'checkon', defaultRole = 'A
|
|
|
67
102
|
/**
|
|
68
103
|
* payload에서 NextAuth JWT 객체 생성
|
|
69
104
|
* @param payload JWT payload
|
|
105
|
+
* @param serviceId 서비스 ID (필수)
|
|
70
106
|
* @param includeAcademies academies 정보 포함 여부
|
|
71
107
|
* @returns NextAuth JWT 객체
|
|
72
108
|
*/
|
|
73
|
-
function createNextAuthJWT(payload, includeAcademies = false) {
|
|
109
|
+
function createNextAuthJWT(payload, serviceId, includeAcademies = false) {
|
|
74
110
|
const services = payload.services || [];
|
|
75
|
-
const
|
|
76
|
-
const effectiveRole =
|
|
111
|
+
const service = services.find((s) => s.serviceId === serviceId);
|
|
112
|
+
const effectiveRole = service?.role || payload.role || 'ADMIN';
|
|
77
113
|
const jwt = {
|
|
78
114
|
id: (payload.id || payload.sub),
|
|
79
115
|
email: payload.email,
|
|
80
116
|
name: payload.name,
|
|
81
|
-
role: effectiveRole, // Role enum 타입
|
|
117
|
+
role: effectiveRole, // Role enum 타입 (string으로 캐스팅)
|
|
82
118
|
services: payload.services,
|
|
83
119
|
academies: includeAcademies
|
|
84
120
|
? payload.academies || []
|
|
@@ -142,7 +178,10 @@ function setCustomTokens(response, accessToken, optionsOrRefreshToken, options)
|
|
|
142
178
|
if (typeof optionsOrRefreshToken === 'string') {
|
|
143
179
|
// 기존 방식: refreshToken이 문자열로 전달된 경우
|
|
144
180
|
refreshTokenValue = optionsOrRefreshToken;
|
|
145
|
-
const { cookiePrefix: prefix
|
|
181
|
+
const { cookiePrefix: prefix, isProduction: prod = false, } = options || {};
|
|
182
|
+
if (!prefix) {
|
|
183
|
+
throw new Error('cookiePrefix is required');
|
|
184
|
+
}
|
|
146
185
|
cookiePrefix = prefix;
|
|
147
186
|
isProduction = prod;
|
|
148
187
|
}
|
|
@@ -150,7 +189,10 @@ function setCustomTokens(response, accessToken, optionsOrRefreshToken, options)
|
|
|
150
189
|
// 새로운 방식: options 객체로 전달된 경우
|
|
151
190
|
const opts = optionsOrRefreshToken || {};
|
|
152
191
|
refreshTokenValue = opts.refreshToken;
|
|
153
|
-
|
|
192
|
+
if (!opts.cookiePrefix) {
|
|
193
|
+
throw new Error('cookiePrefix is required');
|
|
194
|
+
}
|
|
195
|
+
cookiePrefix = opts.cookiePrefix;
|
|
154
196
|
isProduction = opts.isProduction || false;
|
|
155
197
|
}
|
|
156
198
|
// access_token 설정
|
|
@@ -204,10 +246,10 @@ function setNextAuthToken(response, sessionToken, options = {}) {
|
|
|
204
246
|
/**
|
|
205
247
|
* 리다이렉트용 HTML 생성
|
|
206
248
|
* @param redirectPath 리다이렉트할 경로
|
|
207
|
-
* @param text 표시할 텍스트 (
|
|
249
|
+
* @param text 표시할 텍스트 (필수)
|
|
208
250
|
* @returns HTML 문자열
|
|
209
251
|
*/
|
|
210
|
-
function createRedirectHTML(redirectPath, text
|
|
252
|
+
function createRedirectHTML(redirectPath, text) {
|
|
211
253
|
return `
|
|
212
254
|
<!DOCTYPE html>
|
|
213
255
|
<html>
|
|
@@ -282,14 +324,15 @@ function createRedirectHTML(redirectPath, text = 'checkon') {
|
|
|
282
324
|
* @param options 추가 옵션
|
|
283
325
|
* @param options.refreshToken refresh token (선택)
|
|
284
326
|
* @param options.redirectPath 리다이렉트할 경로 (기본값: 페이지 리로드)
|
|
285
|
-
* @param options.text 리다이렉트 HTML에 표시할 텍스트 (
|
|
286
|
-
* @param options.cookiePrefix 쿠키 이름 접두사 (
|
|
327
|
+
* @param options.text 리다이렉트 HTML에 표시할 텍스트 (선택사항)
|
|
328
|
+
* @param options.cookiePrefix 쿠키 이름 접두사 (필수)
|
|
287
329
|
* @param options.isProduction 프로덕션 환경 여부 (기본값: false)
|
|
288
330
|
* @param options.cookieDomain 쿠키 도메인 (선택)
|
|
289
331
|
* @returns NextResponse 객체
|
|
290
332
|
*/
|
|
291
|
-
async function createAuthResponse(accessToken, secret, options
|
|
292
|
-
|
|
333
|
+
async function createAuthResponse(accessToken, secret, options) {
|
|
334
|
+
checkLicenseKey(options.licenseKey);
|
|
335
|
+
const { refreshToken, redirectPath, text, cookiePrefix, isProduction = false, cookieDomain, serviceId, } = options;
|
|
293
336
|
// 1. 토큰 검증
|
|
294
337
|
const tokenResult = await verifyToken(accessToken, secret);
|
|
295
338
|
if (!tokenResult) {
|
|
@@ -297,14 +340,15 @@ async function createAuthResponse(accessToken, secret, options = {}) {
|
|
|
297
340
|
}
|
|
298
341
|
const { payload } = tokenResult;
|
|
299
342
|
// 2. 역할 추출
|
|
300
|
-
const role = extractRoleFromPayload(payload);
|
|
343
|
+
const role = extractRoleFromPayload(payload, serviceId);
|
|
301
344
|
// 3. NextAuth JWT 생성 및 인코딩
|
|
302
|
-
const jwt = createNextAuthJWT(payload);
|
|
345
|
+
const jwt = createNextAuthJWT(payload, serviceId);
|
|
303
346
|
const sessionToken = await encodeNextAuthToken(jwt, secret);
|
|
304
347
|
// 4. HTML 생성
|
|
348
|
+
const displayText = text || serviceId;
|
|
305
349
|
const html = redirectPath
|
|
306
|
-
? createRedirectHTML(redirectPath,
|
|
307
|
-
: createRedirectHTML('',
|
|
350
|
+
? createRedirectHTML(redirectPath, displayText)
|
|
351
|
+
: createRedirectHTML('', displayText).replace("window.location.href = ''", "window.location.reload()");
|
|
308
352
|
// 5. Response 생성
|
|
309
353
|
const response = new server_1.NextResponse(html, {
|
|
310
354
|
status: 200,
|
|
@@ -339,21 +383,22 @@ async function createAuthResponse(accessToken, secret, options = {}) {
|
|
|
339
383
|
* 서비스 구독 유효성 확인 함수
|
|
340
384
|
* @param services 서비스 정보 배열
|
|
341
385
|
* @param serviceId 확인할 서비스 ID
|
|
386
|
+
* @param ssoBaseURL SSO 서버 기본 URL (필수)
|
|
342
387
|
* @returns 구독 유효성 결과
|
|
343
388
|
*/
|
|
344
|
-
function validateServiceSubscription(services, serviceId) {
|
|
389
|
+
function validateServiceSubscription(services, serviceId, ssoBaseURL) {
|
|
345
390
|
const filteredServices = services.filter(service => service.serviceId === serviceId);
|
|
346
391
|
if (filteredServices.length === 0) {
|
|
347
392
|
return {
|
|
348
393
|
isValid: false,
|
|
349
|
-
redirectUrl: `${
|
|
394
|
+
redirectUrl: `${ssoBaseURL}/services/${serviceId}?type=subscription_required`
|
|
350
395
|
};
|
|
351
396
|
}
|
|
352
397
|
const service = filteredServices[0];
|
|
353
398
|
if (service.status !== "ACTIVE") {
|
|
354
399
|
return {
|
|
355
400
|
isValid: false,
|
|
356
|
-
redirectUrl: `${
|
|
401
|
+
redirectUrl: `${ssoBaseURL}/services/${service.serviceId}?type=subscription_required`,
|
|
357
402
|
service
|
|
358
403
|
};
|
|
359
404
|
}
|
|
@@ -366,13 +411,12 @@ function validateServiceSubscription(services, serviceId) {
|
|
|
366
411
|
* SSO 서버에서 refresh token을 사용하여 새로운 access token을 발급받는 함수
|
|
367
412
|
* @param refreshToken refresh token
|
|
368
413
|
* @param options 옵션
|
|
369
|
-
* @param options.ssoBaseURL SSO 서버 기본 URL (
|
|
414
|
+
* @param options.ssoBaseURL SSO 서버 기본 URL (필수)
|
|
370
415
|
* @param options.authServiceKey 인증 서비스 키 (기본값: 환경 변수)
|
|
371
416
|
* @returns SSO refresh token 응답
|
|
372
417
|
*/
|
|
373
418
|
async function refreshSSOToken(refreshToken, options) {
|
|
374
|
-
const
|
|
375
|
-
const authServiceKey = options?.authServiceKey || process.env.AUTH_SERVICE_SECRET_KEY;
|
|
419
|
+
const { ssoBaseURL, authServiceKey } = options;
|
|
376
420
|
if (!authServiceKey) {
|
|
377
421
|
throw new Error('AUTH_SERVICE_SECRET_KEY not configured');
|
|
378
422
|
}
|
|
@@ -391,13 +435,12 @@ async function refreshSSOToken(refreshToken, options) {
|
|
|
391
435
|
* @param userId 사용자 ID
|
|
392
436
|
* @param accessToken access token
|
|
393
437
|
* @param options 옵션
|
|
394
|
-
* @param options.ssoBaseURL SSO 서버 기본 URL (
|
|
438
|
+
* @param options.ssoBaseURL SSO 서버 기본 URL (필수)
|
|
395
439
|
* @param options.authServiceKey 인증 서비스 키 (기본값: 환경 변수)
|
|
396
440
|
* @returns refresh token 또는 null
|
|
397
441
|
*/
|
|
398
442
|
async function getRefreshTokenFromSSO(userId, accessToken, options) {
|
|
399
|
-
const
|
|
400
|
-
const authServiceKey = options?.authServiceKey || process.env.AUTH_SERVICE_SECRET_KEY;
|
|
443
|
+
const { ssoBaseURL, authServiceKey } = options;
|
|
401
444
|
if (!authServiceKey) {
|
|
402
445
|
return null;
|
|
403
446
|
}
|
|
@@ -423,56 +466,59 @@ async function getRefreshTokenFromSSO(userId, accessToken, options) {
|
|
|
423
466
|
}
|
|
424
467
|
return null;
|
|
425
468
|
}
|
|
426
|
-
/**
|
|
427
|
-
* 토큰 검증 및 리프레시 처리 공통 함수
|
|
428
|
-
*
|
|
429
|
-
* @param req - NextRequest 객체
|
|
430
|
-
* @param secret - JWT 서명에 사용할 secret key
|
|
431
|
-
* @param options - 옵션
|
|
432
|
-
* @param options.cookiePrefix - 쿠키 이름 접두사 (기본값: 'checkon')
|
|
433
|
-
* @param options.isProduction - 프로덕션 환경 여부 (기본값: false)
|
|
434
|
-
* @param options.cookieDomain - 쿠키 도메인 (선택)
|
|
435
|
-
* @param options.text - 리다이렉트 HTML에 표시할 텍스트 (기본값: 'checkon')
|
|
436
|
-
* @param options.ssoBaseURL - SSO 서버 기본 URL (기본값: 환경 변수 또는 기본값)
|
|
437
|
-
* @param options.authServiceKey - 인증 서비스 키 (기본값: 환경 변수)
|
|
438
|
-
* @returns 검증 결과 및 필요시 리프레시된 응답
|
|
439
|
-
*/
|
|
440
469
|
async function verifyAndRefreshToken(req, secret, options) {
|
|
441
|
-
const { cookiePrefix
|
|
470
|
+
const { cookiePrefix, serviceId, isProduction, cookieDomain, text, ssoBaseURL, authServiceKey, } = options;
|
|
442
471
|
// 1. access_token 쿠키 확인
|
|
443
472
|
const accessTokenName = `${cookiePrefix}_access_token`;
|
|
444
473
|
const accessToken = req.cookies.get(accessTokenName)?.value;
|
|
445
474
|
if (accessToken) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
475
|
+
try {
|
|
476
|
+
const secretBytes = new TextEncoder().encode(secret);
|
|
477
|
+
const { payload } = await (0, jose_1.jwtVerify)(accessToken, secretBytes);
|
|
478
|
+
if (payload && typeof payload === 'object' && payload.email) {
|
|
479
|
+
return { isValid: true, payload: payload };
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
// 토큰 검증 실패
|
|
449
484
|
}
|
|
450
|
-
// 토큰이 만료되었거나 유효하지 않음 (리프레시 시도)
|
|
451
485
|
}
|
|
452
|
-
//
|
|
486
|
+
// 리프레시 토큰으로 갱신 시도
|
|
453
487
|
const refreshTokenName = `${cookiePrefix}_refresh_token`;
|
|
454
488
|
const refreshToken = req.cookies.get(refreshTokenName)?.value;
|
|
455
489
|
if (refreshToken) {
|
|
456
490
|
try {
|
|
491
|
+
if (!ssoBaseURL || !authServiceKey) {
|
|
492
|
+
return { isValid: false, error: 'SSO_CONFIG_MISSING' };
|
|
493
|
+
}
|
|
457
494
|
const refreshResult = await refreshSSOToken(refreshToken, {
|
|
458
|
-
ssoBaseURL
|
|
459
|
-
authServiceKey
|
|
495
|
+
ssoBaseURL,
|
|
496
|
+
authServiceKey,
|
|
460
497
|
});
|
|
461
498
|
if (refreshResult.success && refreshResult.accessToken) {
|
|
462
|
-
// 토큰 갱신 성공 - createAuthResponse 사용
|
|
463
499
|
const newRefreshToken = refreshResult.refreshToken || refreshToken;
|
|
464
500
|
try {
|
|
501
|
+
let payload;
|
|
502
|
+
try {
|
|
503
|
+
const secretBytes = new TextEncoder().encode(secret);
|
|
504
|
+
const { payload: tokenPayload } = await (0, jose_1.jwtVerify)(refreshResult.accessToken, secretBytes);
|
|
505
|
+
if (tokenPayload && typeof tokenPayload === 'object' && tokenPayload.email) {
|
|
506
|
+
payload = tokenPayload;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
// 토큰 검증 실패
|
|
511
|
+
}
|
|
465
512
|
const response = await createAuthResponse(refreshResult.accessToken, secret, {
|
|
466
513
|
refreshToken: newRefreshToken,
|
|
467
|
-
redirectPath: '',
|
|
468
|
-
text,
|
|
514
|
+
redirectPath: '',
|
|
515
|
+
text: text || serviceId,
|
|
469
516
|
cookiePrefix,
|
|
470
517
|
isProduction,
|
|
471
518
|
cookieDomain,
|
|
519
|
+
serviceId,
|
|
520
|
+
licenseKey: options.licenseKey,
|
|
472
521
|
});
|
|
473
|
-
// payload 추출을 위해 토큰 검증
|
|
474
|
-
const tokenResult = await verifyToken(refreshResult.accessToken, secret);
|
|
475
|
-
const payload = tokenResult?.payload;
|
|
476
522
|
return { isValid: true, response, payload };
|
|
477
523
|
}
|
|
478
524
|
catch (error) {
|
|
@@ -508,10 +554,10 @@ function redirectToError(req, code, message, errorPath = '/error') {
|
|
|
508
554
|
/**
|
|
509
555
|
* 인증 쿠키를 삭제하는 헬퍼 함수
|
|
510
556
|
* @param response NextResponse 객체
|
|
511
|
-
* @param cookiePrefix 쿠키 이름 접두사 (
|
|
557
|
+
* @param cookiePrefix 쿠키 이름 접두사 (필수)
|
|
512
558
|
* @returns NextResponse 객체
|
|
513
559
|
*/
|
|
514
|
-
function clearAuthCookies(response, cookiePrefix
|
|
560
|
+
function clearAuthCookies(response, cookiePrefix) {
|
|
515
561
|
response.cookies.delete(`${cookiePrefix}_access_token`);
|
|
516
562
|
response.cookies.delete(`${cookiePrefix}_refresh_token`);
|
|
517
563
|
return response;
|
|
@@ -519,10 +565,10 @@ function clearAuthCookies(response, cookiePrefix = 'checkon') {
|
|
|
519
565
|
/**
|
|
520
566
|
* JWT에서 서비스별 역할을 추출하는 헬퍼 함수
|
|
521
567
|
* @param token NextAuth JWT 객체 또는 null
|
|
522
|
-
* @param serviceId 서비스 ID (
|
|
568
|
+
* @param serviceId 서비스 ID (필수)
|
|
523
569
|
* @returns 추출된 역할 또는 undefined
|
|
524
570
|
*/
|
|
525
|
-
function getEffectiveRole(token, serviceId
|
|
571
|
+
function getEffectiveRole(token, serviceId) {
|
|
526
572
|
if (!token)
|
|
527
573
|
return undefined;
|
|
528
574
|
// token이 이미 JWT 객체인 경우 role을 직접 사용
|
|
@@ -550,6 +596,19 @@ function requiresSubscription(pathname, role, subscriptionRequiredPaths, systemA
|
|
|
550
596
|
// 구독이 필요한 경로인지 확인
|
|
551
597
|
return subscriptionRequiredPaths.some(path => pathname.startsWith(path));
|
|
552
598
|
}
|
|
599
|
+
// 유효한 라이센스 키 해시 목록
|
|
600
|
+
const VALID_LICENSE_KEY_HASHES = new Set([
|
|
601
|
+
'73bce4f3b64804c255cdab450d759a8b53038f9edb59ae42d9988b08dfd007e2',
|
|
602
|
+
]);
|
|
603
|
+
function checkLicenseKey(licenseKey) {
|
|
604
|
+
if (!licenseKey || licenseKey.length < 10) {
|
|
605
|
+
throw new Error('License key is required');
|
|
606
|
+
}
|
|
607
|
+
const keyHash = (0, crypto_1.createHash)('sha256').update(licenseKey).digest('hex');
|
|
608
|
+
if (!VALID_LICENSE_KEY_HASHES.has(keyHash)) {
|
|
609
|
+
throw new Error('Invalid license key');
|
|
610
|
+
}
|
|
611
|
+
}
|
|
553
612
|
function checkRoleAccess(pathname, role, roleConfig) {
|
|
554
613
|
// 각 역할 설정을 확인
|
|
555
614
|
for (const [configRole, config] of Object.entries(roleConfig)) {
|
|
@@ -570,12 +629,11 @@ function checkRoleAccess(pathname, role, roleConfig) {
|
|
|
570
629
|
* SSO 로그인 페이지로 리다이렉트하는 헬퍼 함수
|
|
571
630
|
* @param req NextRequest 객체
|
|
572
631
|
* @param serviceId 서비스 ID
|
|
573
|
-
* @param ssoBaseURL SSO 서버 기본 URL (
|
|
632
|
+
* @param ssoBaseURL SSO 서버 기본 URL (필수)
|
|
574
633
|
* @returns NextResponse 리다이렉트 응답
|
|
575
634
|
*/
|
|
576
635
|
function redirectToSSOLogin(req, serviceId, ssoBaseURL) {
|
|
577
|
-
|
|
578
|
-
return server_1.NextResponse.redirect(new URL(`${baseURL}/auth/login?serviceId=${serviceId}`, req.url));
|
|
636
|
+
return server_1.NextResponse.redirect(new URL(`${ssoBaseURL}/auth/login?serviceId=${serviceId}`, req.url));
|
|
579
637
|
}
|
|
580
638
|
/**
|
|
581
639
|
* 역할별 대시보드 경로로 리다이렉트하는 헬퍼 함수
|
|
@@ -620,10 +678,10 @@ function isValidToken(token) {
|
|
|
620
678
|
* 특정 역할을 가지고 있는지 확인하는 함수
|
|
621
679
|
* @param token NextAuth JWT 객체
|
|
622
680
|
* @param role 확인할 역할
|
|
623
|
-
* @param serviceId 서비스 ID (
|
|
681
|
+
* @param serviceId 서비스 ID (필수)
|
|
624
682
|
* @returns 역할 보유 여부
|
|
625
683
|
*/
|
|
626
|
-
function hasRole(token, role, serviceId
|
|
684
|
+
function hasRole(token, role, serviceId) {
|
|
627
685
|
if (!token)
|
|
628
686
|
return false;
|
|
629
687
|
const effectiveRole = getEffectiveRole(token, serviceId);
|
|
@@ -633,10 +691,10 @@ function hasRole(token, role, serviceId = 'checkon') {
|
|
|
633
691
|
* 여러 역할 중 하나라도 가지고 있는지 확인하는 함수
|
|
634
692
|
* @param token NextAuth JWT 객체
|
|
635
693
|
* @param roles 확인할 역할 배열
|
|
636
|
-
* @param serviceId 서비스 ID (
|
|
694
|
+
* @param serviceId 서비스 ID (필수)
|
|
637
695
|
* @returns 역할 보유 여부
|
|
638
696
|
*/
|
|
639
|
-
function hasAnyRole(token, roles, serviceId
|
|
697
|
+
function hasAnyRole(token, roles, serviceId) {
|
|
640
698
|
if (!token)
|
|
641
699
|
return false;
|
|
642
700
|
const effectiveRole = getEffectiveRole(token, serviceId);
|
|
@@ -678,8 +736,7 @@ function isProtectedApiPath(pathname, exemptPaths = []) {
|
|
|
678
736
|
* @returns 인증 결과
|
|
679
737
|
*/
|
|
680
738
|
async function checkAuthentication(req, secret, options) {
|
|
681
|
-
const { cookiePrefix
|
|
682
|
-
// 1. NextAuth 토큰 확인
|
|
739
|
+
const { cookiePrefix, serviceId, getNextAuthToken, } = options;
|
|
683
740
|
let nextAuthToken = null;
|
|
684
741
|
if (getNextAuthToken) {
|
|
685
742
|
nextAuthToken = await getNextAuthToken(req);
|
|
@@ -690,7 +747,6 @@ async function checkAuthentication(req, secret, options) {
|
|
|
690
747
|
token: nextAuthToken,
|
|
691
748
|
};
|
|
692
749
|
}
|
|
693
|
-
// 2. 자체 토큰 확인
|
|
694
750
|
const authCheck = await verifyAndRefreshToken(req, secret, options);
|
|
695
751
|
if (authCheck.response) {
|
|
696
752
|
return {
|
|
@@ -720,77 +776,74 @@ async function checkAuthentication(req, secret, options) {
|
|
|
720
776
|
* @returns 인증 결과
|
|
721
777
|
*/
|
|
722
778
|
async function verifyAndRefreshTokenWithNextAuth(req, nextAuthToken, secret, options) {
|
|
723
|
-
// 1. NextAuth 토큰이 있고 유효하면 통과
|
|
724
779
|
if (nextAuthToken && isValidToken(nextAuthToken)) {
|
|
725
780
|
return { isValid: true };
|
|
726
781
|
}
|
|
727
|
-
// 2. 자체 토큰 확인 및 리프레시
|
|
728
782
|
const authCheck = await verifyAndRefreshToken(req, secret, options);
|
|
729
783
|
return authCheck;
|
|
730
784
|
}
|
|
731
785
|
/**
|
|
732
786
|
* 기본 미들웨어 설정을 생성하는 함수
|
|
733
|
-
* @param config 커스텀 설정 (
|
|
787
|
+
* @param config 커스텀 설정 (필수: serviceId 포함)
|
|
788
|
+
* @param defaults 기본 설정값 (선택사항, 제공하지 않으면 최소 기본값 사용)
|
|
734
789
|
* @returns 미들웨어 설정 객체
|
|
735
790
|
*/
|
|
736
|
-
function createMiddlewareConfig(config) {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
},
|
|
779
|
-
roleAccessConfig: {},
|
|
780
|
-
systemAdminRole: 'SYSTEM_ADMIN',
|
|
781
|
-
errorPath: '/error',
|
|
782
|
-
serviceId: config?.serviceId,
|
|
791
|
+
function createMiddlewareConfig(config, defaults) {
|
|
792
|
+
// 기본값 설정
|
|
793
|
+
const defaultPublicPaths = defaults?.publicPaths || [
|
|
794
|
+
'/robots.txt',
|
|
795
|
+
'/sitemap.xml',
|
|
796
|
+
'/ads.txt',
|
|
797
|
+
'/images',
|
|
798
|
+
'/login',
|
|
799
|
+
'/register',
|
|
800
|
+
'/forgot-password',
|
|
801
|
+
'/reset-password',
|
|
802
|
+
'/about',
|
|
803
|
+
'/pricing',
|
|
804
|
+
'/support',
|
|
805
|
+
'/terms',
|
|
806
|
+
'/privacy',
|
|
807
|
+
'/refund',
|
|
808
|
+
'/error',
|
|
809
|
+
];
|
|
810
|
+
const defaultSubscriptionRequiredPaths = defaults?.subscriptionRequiredPaths || [
|
|
811
|
+
'/admin',
|
|
812
|
+
'/teacher',
|
|
813
|
+
'/student',
|
|
814
|
+
'/personal',
|
|
815
|
+
];
|
|
816
|
+
const defaultSubscriptionExemptApiPaths = defaults?.subscriptionExemptApiPaths || [
|
|
817
|
+
'/api/auth',
|
|
818
|
+
'/api/sso',
|
|
819
|
+
'/api/admin/subscription',
|
|
820
|
+
'/api/admin/payment-methods',
|
|
821
|
+
];
|
|
822
|
+
const defaultAuthApiPaths = defaults?.authApiPaths || [
|
|
823
|
+
'/api/auth/send-verification',
|
|
824
|
+
'/api/auth/verify-code',
|
|
825
|
+
'/api/auth/verify-user',
|
|
826
|
+
'/api/auth/select-academy',
|
|
827
|
+
];
|
|
828
|
+
const defaultRolePaths = defaults?.rolePaths || {
|
|
829
|
+
SYSTEM_ADMIN: '/system',
|
|
830
|
+
ADMIN: '/admin',
|
|
831
|
+
TEACHER: '/teacher',
|
|
832
|
+
STUDENT: '/student',
|
|
783
833
|
};
|
|
834
|
+
const defaultSystemAdminRole = defaults?.systemAdminRole || 'SYSTEM_ADMIN';
|
|
835
|
+
const defaultErrorPath = defaults?.errorPath || '/error';
|
|
784
836
|
// 커스텀 설정으로 병합
|
|
785
837
|
return {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
838
|
+
publicPaths: config.publicPaths || defaultPublicPaths,
|
|
839
|
+
subscriptionRequiredPaths: config.subscriptionRequiredPaths || defaultSubscriptionRequiredPaths,
|
|
840
|
+
subscriptionExemptApiPaths: config.subscriptionExemptApiPaths || defaultSubscriptionExemptApiPaths,
|
|
841
|
+
authApiPaths: config.authApiPaths || defaultAuthApiPaths,
|
|
842
|
+
rolePaths: config.rolePaths || defaultRolePaths,
|
|
843
|
+
roleAccessConfig: config.roleAccessConfig || {},
|
|
844
|
+
systemAdminRole: config.systemAdminRole || defaultSystemAdminRole,
|
|
845
|
+
errorPath: config.errorPath || defaultErrorPath,
|
|
846
|
+
serviceId: config.serviceId,
|
|
794
847
|
};
|
|
795
848
|
}
|
|
796
849
|
/**
|
|
@@ -798,18 +851,33 @@ function createMiddlewareConfig(config) {
|
|
|
798
851
|
* 모든 인증, 권한, 구독 체크를 포함한 완전한 미들웨어 로직
|
|
799
852
|
* @param req NextRequest 객체
|
|
800
853
|
* @param config 미들웨어 설정
|
|
801
|
-
* @param
|
|
854
|
+
* @param options 미들웨어 실행 옵션 (secret 필수)
|
|
802
855
|
* @returns NextResponse 또는 null (다음 미들웨어로 진행)
|
|
803
856
|
*/
|
|
804
|
-
async function handleMiddleware(req, config,
|
|
857
|
+
async function handleMiddleware(req, config, options) {
|
|
805
858
|
try {
|
|
806
859
|
const pathname = req.nextUrl.pathname;
|
|
807
|
-
const secret =
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
860
|
+
const { secret, isProduction, cookieDomain, getNextAuthToken, licenseKey } = options;
|
|
861
|
+
checkLicenseKey(licenseKey);
|
|
862
|
+
if (!config.serviceId) {
|
|
863
|
+
throw new Error('serviceId is required in middleware config');
|
|
864
|
+
}
|
|
865
|
+
const serviceId = config.serviceId;
|
|
811
866
|
const cookiePrefix = serviceId;
|
|
812
|
-
|
|
867
|
+
let token = null;
|
|
868
|
+
if (getNextAuthToken) {
|
|
869
|
+
token = await getNextAuthToken(req);
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
// 기본적으로 secret을 사용하여 토큰 가져오기
|
|
873
|
+
try {
|
|
874
|
+
const { getToken } = await Promise.resolve().then(() => __importStar(require('next-auth/jwt')));
|
|
875
|
+
token = await getToken({ req, secret });
|
|
876
|
+
}
|
|
877
|
+
catch {
|
|
878
|
+
// NextAuth가 없으면 null 유지
|
|
879
|
+
}
|
|
880
|
+
}
|
|
813
881
|
const effectiveRole = getEffectiveRole(token, serviceId);
|
|
814
882
|
// 1. API 요청 처리
|
|
815
883
|
if (pathname.startsWith('/api/')) {
|
|
@@ -821,9 +889,13 @@ async function handleMiddleware(req, config, getNextAuthToken) {
|
|
|
821
889
|
}
|
|
822
890
|
const authCheck = await verifyAndRefreshTokenWithNextAuth(req, token, secret, {
|
|
823
891
|
cookiePrefix,
|
|
892
|
+
serviceId,
|
|
824
893
|
isProduction,
|
|
825
894
|
cookieDomain,
|
|
826
895
|
text: serviceId,
|
|
896
|
+
ssoBaseURL: options.ssoBaseURL,
|
|
897
|
+
authServiceKey: options.authServiceKey,
|
|
898
|
+
licenseKey: options.licenseKey,
|
|
827
899
|
});
|
|
828
900
|
if (authCheck.response) {
|
|
829
901
|
return authCheck.response;
|
|
@@ -850,7 +922,11 @@ async function handleMiddleware(req, config, getNextAuthToken) {
|
|
|
850
922
|
const tokenRole = extractRoleFromPayload(payload, serviceId, defaultRole);
|
|
851
923
|
// 3. Refresh token 가져오기 (서버 간 통신)
|
|
852
924
|
const userId = payload.sub || payload.userId || '';
|
|
853
|
-
const
|
|
925
|
+
const ssoBaseURL = options.ssoBaseURL;
|
|
926
|
+
const authServiceKey = options.authServiceKey;
|
|
927
|
+
const refreshToken = authServiceKey
|
|
928
|
+
? await getRefreshTokenFromSSO(userId, tokenParam, { ssoBaseURL, authServiceKey }) || ''
|
|
929
|
+
: '';
|
|
854
930
|
// 4. 자체 토큰 생성 및 쿠키 설정
|
|
855
931
|
const redirectPath = config.rolePaths[tokenRole] || config.rolePaths[defaultRole] || '/admin';
|
|
856
932
|
const response = await createAuthResponse(tokenParam, secret, {
|
|
@@ -860,12 +936,15 @@ async function handleMiddleware(req, config, getNextAuthToken) {
|
|
|
860
936
|
cookiePrefix,
|
|
861
937
|
isProduction,
|
|
862
938
|
cookieDomain,
|
|
939
|
+
serviceId,
|
|
940
|
+
licenseKey: options.licenseKey,
|
|
863
941
|
});
|
|
864
942
|
return response;
|
|
865
943
|
}
|
|
866
944
|
catch {
|
|
867
945
|
// 토큰 검증 실패 시 SSO 로그인으로 리다이렉트
|
|
868
|
-
|
|
946
|
+
const ssoBaseURL = options.ssoBaseURL;
|
|
947
|
+
return redirectToSSOLogin(req, serviceId, ssoBaseURL);
|
|
869
948
|
}
|
|
870
949
|
}
|
|
871
950
|
// 토큰이 없고 이미 인증된 경우 역할별 대시보드로 리다이렉트
|
|
@@ -873,7 +952,8 @@ async function handleMiddleware(req, config, getNextAuthToken) {
|
|
|
873
952
|
return redirectToRoleDashboard(req, effectiveRole, config.rolePaths);
|
|
874
953
|
}
|
|
875
954
|
// 인증되지 않은 경우 SSO 로그인 페이지로 리다이렉트
|
|
876
|
-
|
|
955
|
+
const ssoBaseURL = options.ssoBaseURL;
|
|
956
|
+
return redirectToSSOLogin(req, serviceId, ssoBaseURL);
|
|
877
957
|
}
|
|
878
958
|
// 3. 공개 경로 처리
|
|
879
959
|
if (config.publicPaths.some((path) => pathname === path || pathname.startsWith(path))) {
|
|
@@ -888,18 +968,35 @@ async function handleMiddleware(req, config, getNextAuthToken) {
|
|
|
888
968
|
// 4. 인증 체크
|
|
889
969
|
const authCheck = await verifyAndRefreshTokenWithNextAuth(req, token, secret, {
|
|
890
970
|
cookiePrefix,
|
|
971
|
+
serviceId,
|
|
891
972
|
isProduction,
|
|
892
973
|
cookieDomain,
|
|
893
974
|
text: serviceId,
|
|
975
|
+
ssoBaseURL: options.ssoBaseURL,
|
|
976
|
+
authServiceKey: options.authServiceKey,
|
|
977
|
+
licenseKey: options.licenseKey,
|
|
894
978
|
});
|
|
895
979
|
if (authCheck.response) {
|
|
896
980
|
return authCheck.response;
|
|
897
981
|
}
|
|
898
982
|
if (!authCheck.isValid) {
|
|
899
|
-
|
|
983
|
+
const ssoBaseURL = options.ssoBaseURL;
|
|
984
|
+
return redirectToSSOLogin(req, serviceId, ssoBaseURL);
|
|
900
985
|
}
|
|
901
986
|
// 5. 토큰 확인 및 변환
|
|
902
|
-
let finalToken = token
|
|
987
|
+
let finalToken = token;
|
|
988
|
+
if (!finalToken && getNextAuthToken) {
|
|
989
|
+
finalToken = await getNextAuthToken(req);
|
|
990
|
+
}
|
|
991
|
+
else if (!finalToken) {
|
|
992
|
+
try {
|
|
993
|
+
const { getToken } = await Promise.resolve().then(() => __importStar(require('next-auth/jwt')));
|
|
994
|
+
finalToken = await getToken({ req, secret });
|
|
995
|
+
}
|
|
996
|
+
catch {
|
|
997
|
+
// NextAuth가 없으면 null 유지
|
|
998
|
+
}
|
|
999
|
+
}
|
|
903
1000
|
// verifyAndRefreshToken이 성공했는데 NextAuth 토큰이 없으면, 자체 토큰을 사용
|
|
904
1001
|
if (!finalToken && authCheck.isValid) {
|
|
905
1002
|
const accessToken = req.cookies.get(`${cookiePrefix}_access_token`)?.value;
|
|
@@ -907,20 +1004,23 @@ async function handleMiddleware(req, config, getNextAuthToken) {
|
|
|
907
1004
|
const tokenResult = await verifyToken(accessToken, secret);
|
|
908
1005
|
if (tokenResult) {
|
|
909
1006
|
const { payload } = tokenResult;
|
|
910
|
-
finalToken = createNextAuthJWT(payload, true); // academies 포함
|
|
1007
|
+
finalToken = createNextAuthJWT(payload, serviceId, true); // academies 포함
|
|
911
1008
|
}
|
|
912
1009
|
}
|
|
913
1010
|
}
|
|
914
1011
|
if (!finalToken) {
|
|
915
|
-
|
|
1012
|
+
const ssoBaseURL = options.ssoBaseURL;
|
|
1013
|
+
return redirectToSSOLogin(req, serviceId, ssoBaseURL);
|
|
916
1014
|
}
|
|
917
1015
|
// 6. 토큰 에러 체크
|
|
918
1016
|
if (finalToken.error === "RefreshAccessTokenError") {
|
|
919
|
-
|
|
1017
|
+
const ssoBaseURL = options.ssoBaseURL;
|
|
1018
|
+
return redirectToSSOLogin(req, serviceId, ssoBaseURL);
|
|
920
1019
|
}
|
|
921
1020
|
// 7. 토큰 유효성 체크
|
|
922
1021
|
if (!finalToken.role || !finalToken.email) {
|
|
923
|
-
|
|
1022
|
+
const ssoBaseURL = options.ssoBaseURL;
|
|
1023
|
+
return redirectToSSOLogin(req, serviceId, ssoBaseURL);
|
|
924
1024
|
}
|
|
925
1025
|
// 8. 역할 기반 접근 제어
|
|
926
1026
|
const finalEffectiveRole = finalToken.role || getEffectiveRole(finalToken, serviceId) || effectiveRole || '';
|
|
@@ -933,7 +1033,11 @@ async function handleMiddleware(req, config, getNextAuthToken) {
|
|
|
933
1033
|
// 9. 구독 상태 확인 (시스템 관리자 제외)
|
|
934
1034
|
if (finalEffectiveRole && requiresSubscription(pathname, finalEffectiveRole, config.subscriptionRequiredPaths, config.systemAdminRole)) {
|
|
935
1035
|
const services = finalToken.services || [];
|
|
936
|
-
const
|
|
1036
|
+
const ssoBaseURL = options.ssoBaseURL;
|
|
1037
|
+
if (!ssoBaseURL) {
|
|
1038
|
+
throw new Error('ssoBaseURL is required in middleware options');
|
|
1039
|
+
}
|
|
1040
|
+
const subscriptionCheck = validateServiceSubscription(services, serviceId, ssoBaseURL);
|
|
937
1041
|
if (!subscriptionCheck.isValid) {
|
|
938
1042
|
return server_1.NextResponse.redirect(subscriptionCheck.redirectUrl);
|
|
939
1043
|
}
|