@thinkingcat/auth-utils 2.0.0 → 2.0.2
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 +18 -24
- package/dist/core/auth-response.js +12 -12
- package/dist/core/cookies.d.ts +1 -1
- package/dist/core/cookies.js +2 -2
- package/dist/core/jwt.d.ts +1 -1
- package/dist/core/jwt.js +6 -6
- package/dist/core/roles.d.ts +1 -1
- package/dist/core/roles.js +3 -2
- package/dist/core/verify.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/middleware/config.d.ts +1 -1
- package/dist/middleware/handler.d.ts +1 -1
- package/dist/middleware/handler.js +39 -39
- package/dist/nextauth/callbacks.js +7 -7
- package/dist/sso/api.d.ts +1 -1
- package/dist/utils/license.js +2 -2
- package/dist/utils/redirect.d.ts +7 -10
- package/dist/utils/redirect.js +18 -74
- package/dist/utils/server.d.ts +0 -7
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ npm install @thinkingcat/auth-utils
|
|
|
91
91
|
```json
|
|
92
92
|
{
|
|
93
93
|
"dependencies": {
|
|
94
|
-
"@thinkingcat/auth-utils": "^
|
|
94
|
+
"@thinkingcat/auth-utils": "^2.0.2"
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
```
|
|
@@ -237,7 +237,7 @@ const response = await handleMiddleware(req, middlewareConfig, {
|
|
|
237
237
|
|
|
238
238
|
### 토큰 검증 및 생성
|
|
239
239
|
|
|
240
|
-
#### `verifyToken(accessToken: string, secret: string
|
|
240
|
+
#### `verifyToken(accessToken: string, secret: string)`
|
|
241
241
|
|
|
242
242
|
JWT access token을 검증하고 디코딩합니다.
|
|
243
243
|
|
|
@@ -245,7 +245,6 @@ JWT access token을 검증하고 디코딩합니다.
|
|
|
245
245
|
|
|
246
246
|
- `accessToken`: 검증할 JWT 토큰
|
|
247
247
|
- `secret`: JWT 서명에 사용할 secret key
|
|
248
|
-
- `licenseKey`: 라이센스 키 (필수)
|
|
249
248
|
|
|
250
249
|
**반환값:**
|
|
251
250
|
|
|
@@ -256,8 +255,7 @@ JWT access token을 검증하고 디코딩합니다.
|
|
|
256
255
|
|
|
257
256
|
```typescript
|
|
258
257
|
const secret = process.env.NEXTAUTH_SECRET!;
|
|
259
|
-
const
|
|
260
|
-
const result = await verifyToken(accessToken, secret, licenseKey);
|
|
258
|
+
const result = await verifyToken(accessToken, secret);
|
|
261
259
|
|
|
262
260
|
if (result) {
|
|
263
261
|
const { payload } = result;
|
|
@@ -1054,8 +1052,8 @@ export async function GET(req: NextRequest) {
|
|
|
1054
1052
|
}
|
|
1055
1053
|
|
|
1056
1054
|
const secret = process.env.NEXTAUTH_SECRET!;
|
|
1057
|
-
|
|
1058
|
-
const tokenResult = await verifyToken(tokenParam, secret
|
|
1055
|
+
|
|
1056
|
+
const tokenResult = await verifyToken(tokenParam, secret);
|
|
1059
1057
|
|
|
1060
1058
|
if (!tokenResult) {
|
|
1061
1059
|
return NextResponse.redirect("/login");
|
|
@@ -1096,8 +1094,8 @@ export async function GET(req: NextRequest) {
|
|
|
1096
1094
|
}
|
|
1097
1095
|
|
|
1098
1096
|
const secret = process.env.NEXTAUTH_SECRET!;
|
|
1099
|
-
|
|
1100
|
-
const tokenResult = await verifyToken(tokenParam, secret
|
|
1097
|
+
|
|
1098
|
+
const tokenResult = await verifyToken(tokenParam, secret);
|
|
1101
1099
|
|
|
1102
1100
|
if (!tokenResult) {
|
|
1103
1101
|
return NextResponse.redirect("/login");
|
|
@@ -1140,10 +1138,10 @@ export async function GET(req: NextRequest) {
|
|
|
1140
1138
|
}
|
|
1141
1139
|
|
|
1142
1140
|
const secret = process.env.NEXTAUTH_SECRET!;
|
|
1143
|
-
|
|
1141
|
+
|
|
1144
1142
|
const isProduction = process.env.NODE_ENV === "production";
|
|
1145
1143
|
|
|
1146
|
-
const tokenResult = await verifyToken(tokenParam, secret
|
|
1144
|
+
const tokenResult = await verifyToken(tokenParam, secret);
|
|
1147
1145
|
if (!tokenResult) {
|
|
1148
1146
|
return NextResponse.redirect("/login");
|
|
1149
1147
|
}
|
|
@@ -1490,31 +1488,27 @@ const response = await handleMiddleware(req, middlewareConfig, {
|
|
|
1490
1488
|
## 📦 패키지 정보
|
|
1491
1489
|
|
|
1492
1490
|
- **패키지명**: `@thinkingcat/auth-utils`
|
|
1493
|
-
- **버전**: `
|
|
1491
|
+
- **버전**: `2.0.2`
|
|
1494
1492
|
- **라이선스**: MIT
|
|
1495
1493
|
- **저장소**: npm registry
|
|
1496
1494
|
|
|
1497
1495
|
## 📝 변경 이력 (Changelog)
|
|
1498
1496
|
|
|
1499
|
-
###
|
|
1497
|
+
### v2.0.2 (2025-12-24)
|
|
1500
1498
|
|
|
1501
1499
|
**새로운 기능:**
|
|
1502
1500
|
|
|
1503
|
-
-
|
|
1504
|
-
-
|
|
1505
|
-
-
|
|
1501
|
+
- SSO 로그인 URL 경로 최적화 (`/login` -> `/auth/login`)
|
|
1502
|
+
- 라이센스 키(LICENSE_KEY) 필수화 및 가용성 확대
|
|
1503
|
+
- 타입 정의 및 미들웨어 핸들러의 안정성 강화
|
|
1506
1504
|
|
|
1507
1505
|
**개선 사항:**
|
|
1508
1506
|
|
|
1509
|
-
-
|
|
1510
|
-
-
|
|
1511
|
-
-
|
|
1512
|
-
- 불필요한 주석 제거
|
|
1513
|
-
|
|
1514
|
-
**성능 최적화:**
|
|
1507
|
+
- `verifyToken` 함수에서 `licenseKey` 파라미터 제거 (내부 로직 최적화)
|
|
1508
|
+
- `README.md`를 최신 소스 코드 API에 맞게 전체 업데이트
|
|
1509
|
+
- 미들웨어 설정(`MiddlewareConfig`) 및 옵션(`MiddlewareOptions`) 상세화
|
|
1515
1510
|
|
|
1516
|
-
|
|
1517
|
-
- 조건부 로깅으로 런타임 오버헤드 감소
|
|
1511
|
+
### v1.0.17 (2024-11-15)
|
|
1518
1512
|
|
|
1519
1513
|
## 🤝 기여 (Contributing)
|
|
1520
1514
|
|
|
@@ -1,41 +1,41 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createAuthResponse = createAuthResponse;
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
4
|
+
const license_js_1 = require("../utils/license.js");
|
|
5
|
+
const jwt_js_1 = require("./jwt.js");
|
|
6
|
+
const server_js_1 = require("../utils/server.js");
|
|
7
|
+
const cookies_js_1 = require("./cookies.js");
|
|
8
|
+
const logger_js_1 = require("../utils/logger.js");
|
|
9
9
|
/**
|
|
10
10
|
* access token과 refresh token을 사용하여 완전한 인증 세션 생성
|
|
11
11
|
*/
|
|
12
12
|
async function createAuthResponse(accessToken, secret, options) {
|
|
13
|
-
await (0,
|
|
13
|
+
await (0, license_js_1.checkLicenseKey)(options.licenseKey);
|
|
14
14
|
const { req, refreshToken, redirectPath, text, cookiePrefix, isProduction = false, cookieDomain, serviceId, } = options;
|
|
15
|
-
const tokenResult = await (0,
|
|
15
|
+
const tokenResult = await (0, jwt_js_1.verifyToken)(accessToken, secret);
|
|
16
16
|
if (!tokenResult) {
|
|
17
17
|
throw new Error('Invalid token');
|
|
18
18
|
}
|
|
19
19
|
const { payload } = tokenResult;
|
|
20
|
-
const jwt = (0,
|
|
20
|
+
const jwt = (0, jwt_js_1.createNextAuthJWT)(payload, serviceId);
|
|
21
21
|
if (refreshToken) {
|
|
22
22
|
jwt.refreshToken = refreshToken;
|
|
23
23
|
}
|
|
24
24
|
jwt.accessTokenExpires = Date.now() + (15 * 60 * 1000);
|
|
25
|
-
(0,
|
|
26
|
-
const { NextResponse: NextResponseClass } = await (0,
|
|
25
|
+
(0, logger_js_1.debugLog)('createAuthResponse', 'JWT prepared', { jwtId: jwt?.id });
|
|
26
|
+
const { NextResponse: NextResponseClass } = await (0, server_js_1.getNextServer)();
|
|
27
27
|
const response = redirectPath
|
|
28
28
|
? NextResponseClass.redirect(new URL(redirectPath, req.url), { status: 302 })
|
|
29
29
|
: NextResponseClass.json({ success: true, message: text || 'Authentication successful' }, { status: 200 });
|
|
30
30
|
if (refreshToken) {
|
|
31
|
-
(0,
|
|
31
|
+
(0, cookies_js_1.setCustomTokens)(response, accessToken, refreshToken, {
|
|
32
32
|
cookiePrefix,
|
|
33
33
|
isProduction,
|
|
34
34
|
cookieDomain,
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
37
|
else {
|
|
38
|
-
(0,
|
|
38
|
+
(0, cookies_js_1.setCustomTokens)(response, accessToken, {
|
|
39
39
|
cookiePrefix,
|
|
40
40
|
isProduction,
|
|
41
41
|
cookieDomain,
|
package/dist/core/cookies.d.ts
CHANGED
package/dist/core/cookies.js
CHANGED
|
@@ -5,7 +5,7 @@ exports.clearAuthCookies = clearAuthCookies;
|
|
|
5
5
|
exports.clearAllAuthCookies = clearAllAuthCookies;
|
|
6
6
|
exports.setCustomTokens = setCustomTokens;
|
|
7
7
|
exports.setNextAuthToken = setNextAuthToken;
|
|
8
|
-
const
|
|
8
|
+
const logger_js_1 = require("../utils/logger.js");
|
|
9
9
|
/**
|
|
10
10
|
* NextAuth 세션 토큰 쿠키를 삭제하는 헬퍼 함수
|
|
11
11
|
*/
|
|
@@ -70,7 +70,7 @@ function setCustomTokens(response, accessToken, optionsOrRefreshToken, options)
|
|
|
70
70
|
const accessTokenName = `${cookiePrefix}_access_token`;
|
|
71
71
|
response.cookies.delete(accessTokenName);
|
|
72
72
|
response.cookies.set(accessTokenName, accessToken, cookieOptions);
|
|
73
|
-
(0,
|
|
73
|
+
(0, logger_js_1.debugLog)('setCustomTokens', `Set ${accessTokenName} cookie`, { domain: cookieOptions.domain });
|
|
74
74
|
if (refreshTokenValue) {
|
|
75
75
|
const refreshTokenName = `${cookiePrefix}_refresh_token`;
|
|
76
76
|
const refreshCookieOptions = {
|
package/dist/core/jwt.d.ts
CHANGED
package/dist/core/jwt.js
CHANGED
|
@@ -40,8 +40,8 @@ exports.encodeNextAuthToken = encodeNextAuthToken;
|
|
|
40
40
|
exports.isTokenExpired = isTokenExpired;
|
|
41
41
|
exports.isValidToken = isValidToken;
|
|
42
42
|
const jose_1 = require("jose");
|
|
43
|
-
const
|
|
44
|
-
const
|
|
43
|
+
const logger_js_1 = require("../utils/logger.js");
|
|
44
|
+
const crypto_js_1 = require("../utils/crypto.js");
|
|
45
45
|
/**
|
|
46
46
|
* 토큰 검증 및 디코딩
|
|
47
47
|
*/
|
|
@@ -121,7 +121,7 @@ function createNextAuthJWT(payload, serviceId) {
|
|
|
121
121
|
async function encodeNextAuthToken(jwt, secret, maxAge = 30 * 24 * 60 * 60) {
|
|
122
122
|
try {
|
|
123
123
|
const { encode } = await Promise.resolve().then(() => __importStar(require('next-auth/jwt')));
|
|
124
|
-
(0,
|
|
124
|
+
(0, logger_js_1.debugLog)('encodeNextAuthToken', 'Using next-auth/jwt encode');
|
|
125
125
|
const encoded = await encode({
|
|
126
126
|
token: jwt,
|
|
127
127
|
secret: secret,
|
|
@@ -130,8 +130,8 @@ async function encodeNextAuthToken(jwt, secret, maxAge = 30 * 24 * 60 * 60) {
|
|
|
130
130
|
return encoded;
|
|
131
131
|
}
|
|
132
132
|
catch (error) {
|
|
133
|
-
(0,
|
|
134
|
-
const secretHash = await (0,
|
|
133
|
+
(0, logger_js_1.debugLog)('encodeNextAuthToken', 'NextAuth encode failed, using jose EncryptJWT fallback', error);
|
|
134
|
+
const secretHash = await (0, crypto_js_1.createHashSHA256)(secret);
|
|
135
135
|
const keyBytes = new Uint8Array(32);
|
|
136
136
|
for (let i = 0; i < 32; i++) {
|
|
137
137
|
keyBytes[i] = parseInt(secretHash.slice(i * 2, i * 2 + 2), 16);
|
|
@@ -150,7 +150,7 @@ async function encodeNextAuthToken(jwt, secret, maxAge = 30 * 24 * 60 * 60) {
|
|
|
150
150
|
return token;
|
|
151
151
|
}
|
|
152
152
|
catch (encryptError) {
|
|
153
|
-
(0,
|
|
153
|
+
(0, logger_js_1.debugError)('encodeNextAuthToken', 'EncryptJWT also failed:', encryptError);
|
|
154
154
|
throw new Error(`Failed to encode NextAuth token: ${error instanceof Error ? error.message : String(error)}`);
|
|
155
155
|
}
|
|
156
156
|
}
|
package/dist/core/roles.d.ts
CHANGED
package/dist/core/roles.js
CHANGED
|
@@ -30,8 +30,9 @@ function requiresSubscription(pathname, role, subscriptionRequiredPaths, systemA
|
|
|
30
30
|
* 역할 기반 접근 제어 확인
|
|
31
31
|
*/
|
|
32
32
|
function checkRoleAccess(pathname, role, roleConfig) {
|
|
33
|
-
for (const [configRole,
|
|
34
|
-
const
|
|
33
|
+
for (const [configRole, configEntry] of Object.entries(roleConfig)) {
|
|
34
|
+
const config = configEntry;
|
|
35
|
+
const isPathMatch = config.paths.some((path) => pathname.startsWith(path));
|
|
35
36
|
if (isPathMatch) {
|
|
36
37
|
if (role === configRole || config.allowedRoles?.includes(role)) {
|
|
37
38
|
return { allowed: true };
|
package/dist/core/verify.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
__exportStar(require("./types"), exports);
|
|
17
|
+
__exportStar(require("./types/index"), exports);
|
|
18
18
|
__exportStar(require("./utils/logger"), exports);
|
|
19
19
|
__exportStar(require("./utils/crypto"), exports);
|
|
20
20
|
__exportStar(require("./utils/server"), exports);
|
|
@@ -34,25 +34,25 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.handleMiddleware = handleMiddleware;
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
37
|
+
const server_js_1 = require("../utils/server.js");
|
|
38
|
+
const license_js_1 = require("../utils/license.js");
|
|
39
|
+
const logger_js_1 = require("../utils/logger.js");
|
|
40
|
+
const roles_js_1 = require("../core/roles.js");
|
|
41
|
+
const verify_js_1 = require("../core/verify.js");
|
|
42
|
+
const jwt_js_1 = require("../core/jwt.js");
|
|
43
|
+
const api_js_1 = require("../sso/api.js");
|
|
44
|
+
const auth_response_js_1 = require("../core/auth-response.js");
|
|
45
|
+
const redirect_js_1 = require("../utils/redirect.js");
|
|
46
|
+
const cookies_js_1 = require("../core/cookies.js");
|
|
47
47
|
/**
|
|
48
48
|
* 통합 미들웨어 핸들러 함수
|
|
49
49
|
*/
|
|
50
50
|
async function handleMiddleware(req, config, options) {
|
|
51
|
-
const { NextResponse: NextResponseClass } = await (0,
|
|
51
|
+
const { NextResponse: NextResponseClass } = await (0, server_js_1.getNextServer)();
|
|
52
52
|
try {
|
|
53
53
|
const pathname = req.nextUrl.pathname;
|
|
54
54
|
const { secret, isProduction, cookieDomain, getNextAuthToken, licenseKey } = options;
|
|
55
|
-
await (0,
|
|
55
|
+
await (0, license_js_1.checkLicenseKey)(licenseKey);
|
|
56
56
|
if (!config.serviceId) {
|
|
57
57
|
throw new Error('serviceId is required in middleware config');
|
|
58
58
|
}
|
|
@@ -68,10 +68,10 @@ async function handleMiddleware(req, config, options) {
|
|
|
68
68
|
token = await getToken({ req, secret });
|
|
69
69
|
}
|
|
70
70
|
catch (error) {
|
|
71
|
-
(0,
|
|
71
|
+
(0, logger_js_1.debugLog)('handleMiddleware', 'getToken failed', error);
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
-
const effectiveRole = (0,
|
|
74
|
+
const effectiveRole = (0, roles_js_1.getEffectiveRole)(token, serviceId);
|
|
75
75
|
// 1. API 요청 처리
|
|
76
76
|
if (pathname.startsWith('/api/')) {
|
|
77
77
|
if (config.authApiPaths.includes(pathname)) {
|
|
@@ -80,7 +80,7 @@ async function handleMiddleware(req, config, options) {
|
|
|
80
80
|
if (config.subscriptionExemptApiPaths.some((path) => pathname.startsWith(path))) {
|
|
81
81
|
return NextResponseClass.next();
|
|
82
82
|
}
|
|
83
|
-
const authCheck = await (0,
|
|
83
|
+
const authCheck = await (0, verify_js_1.verifyAndRefreshTokenWithNextAuth)(req, token, secret, {
|
|
84
84
|
cookiePrefix,
|
|
85
85
|
serviceId,
|
|
86
86
|
isProduction,
|
|
@@ -93,8 +93,8 @@ async function handleMiddleware(req, config, options) {
|
|
|
93
93
|
if (authCheck.response)
|
|
94
94
|
return authCheck.response;
|
|
95
95
|
if (!authCheck.isValid) {
|
|
96
|
-
const response = await (0,
|
|
97
|
-
(0,
|
|
96
|
+
const response = await (0, redirect_js_1.redirectToError)(req, 'UNAUTHORIZED', '인증이 필요합니다.', config.errorPath);
|
|
97
|
+
(0, cookies_js_1.clearAuthCookies)(response, cookiePrefix);
|
|
98
98
|
return response;
|
|
99
99
|
}
|
|
100
100
|
return NextResponseClass.next();
|
|
@@ -104,27 +104,27 @@ async function handleMiddleware(req, config, options) {
|
|
|
104
104
|
const tokenParam = req.nextUrl.searchParams.get('token');
|
|
105
105
|
if (tokenParam) {
|
|
106
106
|
try {
|
|
107
|
-
const tokenResult = await (0,
|
|
107
|
+
const tokenResult = await (0, jwt_js_1.verifyToken)(tokenParam, secret);
|
|
108
108
|
if (!tokenResult)
|
|
109
109
|
throw new Error('Invalid token');
|
|
110
110
|
const { payload } = tokenResult;
|
|
111
111
|
const defaultRole = Object.keys(config.rolePaths)[0] || 'ADMIN';
|
|
112
|
-
const tokenRole = (0,
|
|
112
|
+
const tokenRole = (0, jwt_js_1.extractRoleFromPayload)(payload, serviceId, defaultRole);
|
|
113
113
|
const userId = payload.id || payload.sub || payload.userId || '';
|
|
114
114
|
const ssoBaseURL = options.ssoBaseURL;
|
|
115
115
|
const authServiceKey = options.authServiceKey;
|
|
116
116
|
let refreshToken = '';
|
|
117
117
|
if (authServiceKey && userId) {
|
|
118
118
|
try {
|
|
119
|
-
const refreshTokenResult = await (0,
|
|
119
|
+
const refreshTokenResult = await (0, api_js_1.getRefreshTokenFromSSO)(userId, tokenParam, { ssoBaseURL, authServiceKey });
|
|
120
120
|
refreshToken = refreshTokenResult || '';
|
|
121
121
|
}
|
|
122
122
|
catch (error) {
|
|
123
|
-
(0,
|
|
123
|
+
(0, logger_js_1.debugError)('handleMiddleware', 'Failed to get refresh token', error);
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
const redirectPath = config.rolePaths[tokenRole] || config.rolePaths[defaultRole] || '/admin';
|
|
127
|
-
return await (0,
|
|
127
|
+
return await (0, auth_response_js_1.createAuthResponse)(tokenParam, secret, {
|
|
128
128
|
req,
|
|
129
129
|
refreshToken: refreshToken || undefined,
|
|
130
130
|
redirectPath,
|
|
@@ -137,16 +137,16 @@ async function handleMiddleware(req, config, options) {
|
|
|
137
137
|
});
|
|
138
138
|
}
|
|
139
139
|
catch (error) {
|
|
140
|
-
(0,
|
|
140
|
+
(0, logger_js_1.debugError)('handleMiddleware', 'Error processing token', error);
|
|
141
141
|
const ssoBaseURL = options.ssoBaseURL;
|
|
142
|
-
return await (0,
|
|
142
|
+
return await (0, redirect_js_1.redirectToSSOLogin)(req, serviceId, ssoBaseURL);
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
if (token && effectiveRole) {
|
|
146
|
-
return await (0,
|
|
146
|
+
return await (0, redirect_js_1.redirectToRoleDashboard)(req, effectiveRole, config.rolePaths);
|
|
147
147
|
}
|
|
148
148
|
const ssoBaseURL = options.ssoBaseURL;
|
|
149
|
-
return await (0,
|
|
149
|
+
return await (0, redirect_js_1.redirectToSSOLogin)(req, serviceId, ssoBaseURL);
|
|
150
150
|
}
|
|
151
151
|
// 3. 공개 경로 처리
|
|
152
152
|
if (config.publicPaths.some((path) => pathname === path || pathname.startsWith(path))) {
|
|
@@ -154,12 +154,12 @@ async function handleMiddleware(req, config, options) {
|
|
|
154
154
|
return NextResponseClass.next();
|
|
155
155
|
}
|
|
156
156
|
if (token && effectiveRole) {
|
|
157
|
-
return await (0,
|
|
157
|
+
return await (0, redirect_js_1.redirectToRoleDashboard)(req, effectiveRole, config.rolePaths);
|
|
158
158
|
}
|
|
159
159
|
return NextResponseClass.next();
|
|
160
160
|
}
|
|
161
161
|
// 4. 인증 체크
|
|
162
|
-
const authCheck = await (0,
|
|
162
|
+
const authCheck = await (0, verify_js_1.verifyAndRefreshTokenWithNextAuth)(req, token, secret, {
|
|
163
163
|
cookiePrefix,
|
|
164
164
|
serviceId,
|
|
165
165
|
isProduction,
|
|
@@ -173,7 +173,7 @@ async function handleMiddleware(req, config, options) {
|
|
|
173
173
|
return authCheck.response;
|
|
174
174
|
if (!authCheck.isValid) {
|
|
175
175
|
const ssoBaseURL = options.ssoBaseURL;
|
|
176
|
-
return await (0,
|
|
176
|
+
return await (0, redirect_js_1.redirectToSSOLogin)(req, serviceId, ssoBaseURL);
|
|
177
177
|
}
|
|
178
178
|
let finalToken = authCheck.token || token;
|
|
179
179
|
if (!finalToken && authCheck.isValid) {
|
|
@@ -192,29 +192,29 @@ async function handleMiddleware(req, config, options) {
|
|
|
192
192
|
}
|
|
193
193
|
if (!finalToken) {
|
|
194
194
|
const ssoBaseURL = options.ssoBaseURL;
|
|
195
|
-
return await (0,
|
|
195
|
+
return await (0, redirect_js_1.redirectToSSOLogin)(req, serviceId, ssoBaseURL);
|
|
196
196
|
}
|
|
197
197
|
if (finalToken.error === "RefreshAccessTokenError") {
|
|
198
198
|
const ssoBaseURL = options.ssoBaseURL;
|
|
199
|
-
return await (0,
|
|
199
|
+
return await (0, redirect_js_1.redirectToSSOLogin)(req, serviceId, ssoBaseURL);
|
|
200
200
|
}
|
|
201
201
|
if (!finalToken.role || !finalToken.email) {
|
|
202
202
|
const ssoBaseURL = options.ssoBaseURL;
|
|
203
|
-
return await (0,
|
|
203
|
+
return await (0, redirect_js_1.redirectToSSOLogin)(req, serviceId, ssoBaseURL);
|
|
204
204
|
}
|
|
205
|
-
const finalEffectiveRole = finalToken.role || (0,
|
|
205
|
+
const finalEffectiveRole = finalToken.role || (0, roles_js_1.getEffectiveRole)(finalToken, serviceId) || effectiveRole || '';
|
|
206
206
|
if (config.roleAccessConfig && Object.keys(config.roleAccessConfig).length > 0 && finalEffectiveRole) {
|
|
207
|
-
const roleCheck = (0,
|
|
207
|
+
const roleCheck = (0, roles_js_1.checkRoleAccess)(pathname, finalEffectiveRole, config.roleAccessConfig);
|
|
208
208
|
if (!roleCheck.allowed) {
|
|
209
|
-
return await (0,
|
|
209
|
+
return await (0, redirect_js_1.redirectToError)(req, 'ACCESS_DENIED', roleCheck.message || '접근 권한이 없습니다.', config.errorPath);
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
|
-
if (finalEffectiveRole && (0,
|
|
212
|
+
if (finalEffectiveRole && (0, roles_js_1.requiresSubscription)(pathname, finalEffectiveRole, config.subscriptionRequiredPaths, config.systemAdminRole)) {
|
|
213
213
|
const services = finalToken.services || [];
|
|
214
214
|
const ssoBaseURL = options.ssoBaseURL;
|
|
215
215
|
if (!ssoBaseURL)
|
|
216
216
|
throw new Error('ssoBaseURL is required');
|
|
217
|
-
const subscriptionCheck = (0,
|
|
217
|
+
const subscriptionCheck = (0, api_js_1.validateServiceSubscription)(services, serviceId, ssoBaseURL);
|
|
218
218
|
if (!subscriptionCheck.isValid) {
|
|
219
219
|
return NextResponseClass.redirect(subscriptionCheck.redirectUrl);
|
|
220
220
|
}
|
|
@@ -222,7 +222,7 @@ async function handleMiddleware(req, config, options) {
|
|
|
222
222
|
return null;
|
|
223
223
|
}
|
|
224
224
|
catch (error) {
|
|
225
|
-
(0,
|
|
226
|
-
return await (0,
|
|
225
|
+
(0, logger_js_1.debugError)('handleMiddleware', 'Middleware error', error);
|
|
226
|
+
return await (0, redirect_js_1.redirectToError)(req, 'INTERNAL_ERROR', '서버 오류가 발생했습니다.', config.errorPath);
|
|
227
227
|
}
|
|
228
228
|
}
|
|
@@ -36,8 +36,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.createInitialJWTToken = createInitialJWTToken;
|
|
37
37
|
exports.getJWTFromCustomTokenCookie = getJWTFromCustomTokenCookie;
|
|
38
38
|
exports.handleJWTCallback = handleJWTCallback;
|
|
39
|
-
const
|
|
40
|
-
const
|
|
39
|
+
const jwt_js_1 = require("../core/jwt.js");
|
|
40
|
+
const license_js_1 = require("../utils/license.js");
|
|
41
41
|
/**
|
|
42
42
|
* JWT 콜백에서 초기 로그인 시 토큰 생성 헬퍼
|
|
43
43
|
*/
|
|
@@ -69,12 +69,12 @@ async function getJWTFromCustomTokenCookie(cookieName, secret, serviceId, licens
|
|
|
69
69
|
const accessToken = cookieStore.get(cookieName)?.value;
|
|
70
70
|
if (!accessToken)
|
|
71
71
|
return null;
|
|
72
|
-
await (0,
|
|
73
|
-
const tokenResult = await (0,
|
|
72
|
+
await (0, license_js_1.checkLicenseKey)(licenseKey);
|
|
73
|
+
const tokenResult = await (0, jwt_js_1.verifyToken)(accessToken, secret);
|
|
74
74
|
if (!tokenResult)
|
|
75
75
|
return null;
|
|
76
76
|
const { payload } = tokenResult;
|
|
77
|
-
const jwt = (0,
|
|
77
|
+
const jwt = (0, jwt_js_1.createNextAuthJWT)(payload, serviceId);
|
|
78
78
|
jwt.accessTokenExpires = Date.now() + (15 * 60 * 1000);
|
|
79
79
|
const refreshTokenCookieName = cookieName.replace('_access_token', '_refresh_token');
|
|
80
80
|
const refreshToken = cookieStore.get(refreshTokenCookieName)?.value;
|
|
@@ -123,9 +123,9 @@ async function handleJWTCallback(token, user, account, options) {
|
|
|
123
123
|
if (response.ok) {
|
|
124
124
|
const result = await response.json();
|
|
125
125
|
if (result.success && result.accessToken) {
|
|
126
|
-
const tokenResult = await (0,
|
|
126
|
+
const tokenResult = await (0, jwt_js_1.verifyToken)(result.accessToken, secret);
|
|
127
127
|
if (tokenResult) {
|
|
128
|
-
const newJWT = (0,
|
|
128
|
+
const newJWT = (0, jwt_js_1.createNextAuthJWT)(tokenResult.payload, serviceId || '');
|
|
129
129
|
return {
|
|
130
130
|
...newJWT,
|
|
131
131
|
refreshToken,
|
package/dist/sso/api.d.ts
CHANGED
package/dist/utils/license.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.checkLicenseKey = checkLicenseKey;
|
|
4
|
-
const
|
|
4
|
+
const crypto_js_1 = require("./crypto.js");
|
|
5
5
|
// 유효한 라이센스 키 해시 목록
|
|
6
6
|
const VALID_LICENSE_KEY_HASHES = new Set([
|
|
7
7
|
'73bce4f3b64804c255cdab450d759a8b53038f9edb59ae42d9988b08dfd007e2',
|
|
@@ -13,7 +13,7 @@ async function checkLicenseKey(licenseKey) {
|
|
|
13
13
|
if (!licenseKey || licenseKey.length < 10) {
|
|
14
14
|
throw new Error('License key is required');
|
|
15
15
|
}
|
|
16
|
-
const keyHash = await (0,
|
|
16
|
+
const keyHash = await (0, crypto_js_1.createHashSHA256)(licenseKey);
|
|
17
17
|
if (!VALID_LICENSE_KEY_HASHES.has(keyHash)) {
|
|
18
18
|
throw new Error('Invalid license key');
|
|
19
19
|
}
|
package/dist/utils/redirect.d.ts
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import type { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* @param redirectPath 리다이렉트할 경로
|
|
5
|
-
* @param text 표시할 텍스트 (필수)
|
|
6
|
-
* @returns HTML 문자열
|
|
3
|
+
* 리다이렉트를 위한 HTML 생성
|
|
7
4
|
*/
|
|
8
|
-
export declare function createRedirectHTML(
|
|
5
|
+
export declare function createRedirectHTML(url: string): string;
|
|
9
6
|
/**
|
|
10
|
-
* 에러 페이지로
|
|
7
|
+
* 에러 페이지로 리다이렉트
|
|
11
8
|
*/
|
|
12
9
|
export declare function redirectToError(req: NextRequest, code: string, message: string, errorPath?: string): Promise<NextResponse>;
|
|
13
10
|
/**
|
|
14
|
-
* SSO 로그인 페이지로
|
|
11
|
+
* SSO 로그인 페이지로 리다이렉트
|
|
15
12
|
*/
|
|
16
|
-
export declare function redirectToSSOLogin(req: NextRequest, serviceId: string, ssoBaseURL
|
|
13
|
+
export declare function redirectToSSOLogin(req: NextRequest, serviceId: string, ssoBaseURL?: string): Promise<NextResponse>;
|
|
17
14
|
/**
|
|
18
|
-
* 역할별
|
|
15
|
+
* 역할별 대시보드로 리다이렉트
|
|
19
16
|
*/
|
|
20
|
-
export declare function redirectToRoleDashboard(req: NextRequest, role: string, rolePaths: Record<string, string
|
|
17
|
+
export declare function redirectToRoleDashboard(req: NextRequest, role: string, rolePaths: Record<string, string>): Promise<NextResponse>;
|
package/dist/utils/redirect.js
CHANGED
|
@@ -4,103 +4,47 @@ exports.createRedirectHTML = createRedirectHTML;
|
|
|
4
4
|
exports.redirectToError = redirectToError;
|
|
5
5
|
exports.redirectToSSOLogin = redirectToSSOLogin;
|
|
6
6
|
exports.redirectToRoleDashboard = redirectToRoleDashboard;
|
|
7
|
-
const
|
|
7
|
+
const server_js_1 = require("./server.js");
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
* @param redirectPath 리다이렉트할 경로
|
|
11
|
-
* @param text 표시할 텍스트 (필수)
|
|
12
|
-
* @returns HTML 문자열
|
|
9
|
+
* 리다이렉트를 위한 HTML 생성
|
|
13
10
|
*/
|
|
14
|
-
function createRedirectHTML(
|
|
11
|
+
function createRedirectHTML(url) {
|
|
15
12
|
return `
|
|
16
|
-
<!DOCTYPE html>
|
|
17
13
|
<html>
|
|
18
14
|
<head>
|
|
19
|
-
<meta
|
|
20
|
-
<title>Redirecting...</title>
|
|
21
|
-
<style>
|
|
22
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
23
|
-
html, body {
|
|
24
|
-
width: 100%;
|
|
25
|
-
height: 100%;
|
|
26
|
-
overflow: hidden;
|
|
27
|
-
}
|
|
28
|
-
body {
|
|
29
|
-
display: flex;
|
|
30
|
-
align-items: flex-start;
|
|
31
|
-
justify-content: flex-start;
|
|
32
|
-
padding-left: 10%;
|
|
33
|
-
padding-top: 20%;
|
|
34
|
-
background: #fff;
|
|
35
|
-
}
|
|
36
|
-
.typing-text {
|
|
37
|
-
font-family: 'Courier New', monospace;
|
|
38
|
-
font-size: 2.5rem;
|
|
39
|
-
font-weight: bold;
|
|
40
|
-
color: #667eea;
|
|
41
|
-
letter-spacing: 0.1em;
|
|
42
|
-
}
|
|
43
|
-
.cursor {
|
|
44
|
-
display: inline-block;
|
|
45
|
-
width: 3px;
|
|
46
|
-
height: 2.5rem;
|
|
47
|
-
background-color: #667eea;
|
|
48
|
-
margin-left: 2px;
|
|
49
|
-
animation: blink 0.7s infinite;
|
|
50
|
-
}
|
|
51
|
-
@keyframes blink {
|
|
52
|
-
0%, 50% { opacity: 1; }
|
|
53
|
-
51%, 100% { opacity: 0; }
|
|
54
|
-
}
|
|
55
|
-
</style>
|
|
15
|
+
<meta http-equiv="refresh" content="0;url=${url}">
|
|
56
16
|
</head>
|
|
57
17
|
<body>
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
</div>
|
|
61
|
-
<script>
|
|
62
|
-
const text = '${text}';
|
|
63
|
-
let index = 0;
|
|
64
|
-
const speed = 100;
|
|
65
|
-
|
|
66
|
-
function type() {
|
|
67
|
-
if (index < text.length) {
|
|
68
|
-
document.getElementById('text').textContent += text.charAt(index);
|
|
69
|
-
index++;
|
|
70
|
-
setTimeout(type, speed);
|
|
71
|
-
} else {
|
|
72
|
-
setTimeout(() => window.location.href = '${redirectPath}', 200);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
type();
|
|
77
|
-
</script>
|
|
18
|
+
<p>Redirecting to <a href="${url}">${url}</a>...</p>
|
|
19
|
+
<script>window.location.href = "${url}";</script>
|
|
78
20
|
</body>
|
|
79
21
|
</html>
|
|
80
22
|
`;
|
|
81
23
|
}
|
|
82
24
|
/**
|
|
83
|
-
* 에러 페이지로
|
|
25
|
+
* 에러 페이지로 리다이렉트
|
|
84
26
|
*/
|
|
85
27
|
async function redirectToError(req, code, message, errorPath = '/error') {
|
|
28
|
+
const { NextResponse: NextResponseClass } = await (0, server_js_1.getNextServer)();
|
|
86
29
|
const url = new URL(errorPath, req.url);
|
|
87
30
|
url.searchParams.set('code', code);
|
|
88
31
|
url.searchParams.set('message', message);
|
|
89
|
-
const { NextResponse: NextResponseClass } = await (0, server_1.getNextServer)();
|
|
90
32
|
return NextResponseClass.redirect(url);
|
|
91
33
|
}
|
|
92
34
|
/**
|
|
93
|
-
* SSO 로그인 페이지로
|
|
35
|
+
* SSO 로그인 페이지로 리다이렉트
|
|
94
36
|
*/
|
|
95
37
|
async function redirectToSSOLogin(req, serviceId, ssoBaseURL) {
|
|
96
|
-
const { NextResponse: NextResponseClass } = await (0,
|
|
97
|
-
|
|
38
|
+
const { NextResponse: NextResponseClass } = await (0, server_js_1.getNextServer)();
|
|
39
|
+
const baseUrl = ssoBaseURL || 'https://sso.thinkingcat.com';
|
|
40
|
+
const callbackUrl = encodeURIComponent(req.url);
|
|
41
|
+
return NextResponseClass.redirect(`${baseUrl}/auth/login?serviceId=${serviceId}&callbackUrl=${callbackUrl}`);
|
|
98
42
|
}
|
|
99
43
|
/**
|
|
100
|
-
* 역할별
|
|
44
|
+
* 역할별 대시보드로 리다이렉트
|
|
101
45
|
*/
|
|
102
|
-
async function redirectToRoleDashboard(req, role, rolePaths
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
return NextResponseClass.redirect(new URL(
|
|
46
|
+
async function redirectToRoleDashboard(req, role, rolePaths) {
|
|
47
|
+
const { NextResponse: NextResponseClass } = await (0, server_js_1.getNextServer)();
|
|
48
|
+
const path = rolePaths[role] || rolePaths['ADMIN'] || '/admin';
|
|
49
|
+
return NextResponseClass.redirect(new URL(path, req.url));
|
|
106
50
|
}
|
package/dist/utils/server.d.ts
CHANGED
|
@@ -3,13 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export declare function getNextServer(): Promise<{
|
|
5
5
|
default: typeof import("next/server");
|
|
6
|
-
NextFetchEvent: typeof import("next/server").NextFetchEvent;
|
|
7
6
|
NextRequest: typeof import("next/server").NextRequest;
|
|
8
7
|
NextResponse: typeof import("next/server").NextResponse;
|
|
9
|
-
userAgentFromString: typeof import("next/server").userAgentFromString;
|
|
10
|
-
userAgent: typeof import("next/server").userAgent;
|
|
11
|
-
URLPattern: typeof import("next/server").URLPattern;
|
|
12
|
-
ImageResponse: typeof import("next/server").ImageResponse;
|
|
13
|
-
after: typeof import("next/server").after;
|
|
14
|
-
connection: typeof import("next/server").connection;
|
|
15
8
|
}>;
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thinkingcat/auth-utils",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "Authentication utilities for ThinkingCat SSO services with conditional logging",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": {
|
|
9
9
|
"types": "./dist/index.d.ts",
|
|
10
|
-
"edge-light": "./dist/index.js",
|
|
11
10
|
"default": "./dist/index.js"
|
|
12
11
|
}
|
|
13
12
|
},
|
|
@@ -30,7 +29,10 @@
|
|
|
30
29
|
},
|
|
31
30
|
"devDependencies": {
|
|
32
31
|
"@types/node": "^20",
|
|
33
|
-
"typescript": "^5"
|
|
32
|
+
"typescript": "^5",
|
|
33
|
+
"next": ">=13.0.0",
|
|
34
|
+
"@types/react": "^18",
|
|
35
|
+
"@types/react-dom": "^18"
|
|
34
36
|
},
|
|
35
37
|
"peerDependencies": {
|
|
36
38
|
"next": ">=13.0.0"
|