@ahhaohho/auth-middleware 2.1.1 → 2.2.1
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/package.json +3 -2
- package/src/index.js +6 -0
- package/src/middleware/auth.js +0 -2
- package/src/strategies/jwt.strategy.js +23 -36
- package/src/strategies/refresh.strategy.js +10 -12
- package/src/utils/extractUserId.js +91 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ahhaohho/auth-middleware",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Shared authentication and authorization middleware for ahhaohho microservices",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
"passport-jwt": "^4.0.1"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"express": "^4.x"
|
|
42
|
+
"express": "^4.x",
|
|
43
|
+
"mongoose": "^8.x"
|
|
43
44
|
},
|
|
44
45
|
"engines": {
|
|
45
46
|
"node": ">=16.0.0"
|
package/src/index.js
CHANGED
|
@@ -24,6 +24,9 @@ const {
|
|
|
24
24
|
fetchRolePermissions
|
|
25
25
|
} = require('./middleware/authorization');
|
|
26
26
|
|
|
27
|
+
// userId 추출 유틸리티
|
|
28
|
+
const { extractUserId } = require('./utils/extractUserId');
|
|
29
|
+
|
|
27
30
|
// 유틸리티
|
|
28
31
|
const { verifyTokenWithFallback, signToken, getCurrentSigningKey } = require('./utils/jwtValidator');
|
|
29
32
|
const { isBlacklisted, addToBlacklist, clearBlacklist } = require('./utils/blacklist');
|
|
@@ -61,6 +64,9 @@ module.exports = {
|
|
|
61
64
|
requireOwnership, // 리소스 소유자 검증: requireOwnership('userId')
|
|
62
65
|
requireAny, // 복합 조건 인가: requireAny({ roles: [...], permissions: [...] })
|
|
63
66
|
|
|
67
|
+
// ===== userId 추출 =====
|
|
68
|
+
extractUserId, // userId 추출: await extractUserId(req, isRequired)
|
|
69
|
+
|
|
64
70
|
// ===== 유틸리티 함수 =====
|
|
65
71
|
utils: {
|
|
66
72
|
// JWT 관련
|
package/src/middleware/auth.js
CHANGED
|
@@ -179,8 +179,6 @@ async function authenticateHybrid(req, res, next) {
|
|
|
179
179
|
|
|
180
180
|
// 3. Refresh token 검증
|
|
181
181
|
passport.authenticate('refresh', { session: false }, async (refreshErr, refreshUser, refreshInfo) => {
|
|
182
|
-
console.log('[@ahhaohho/auth-middleware] 🔍 Refresh callback:', { hasError: !!refreshErr, hasUser: !!refreshUser, userId: refreshUser?.userId });
|
|
183
|
-
|
|
184
182
|
if (refreshErr) {
|
|
185
183
|
console.error('[@ahhaohho/auth-middleware] Refresh token error:', refreshErr.message);
|
|
186
184
|
return res.status(500).json({
|
|
@@ -4,36 +4,37 @@ const { isBlacklisted } = require('../utils/blacklist');
|
|
|
4
4
|
const jwt = require('jsonwebtoken');
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
* 우선순위: 1.
|
|
7
|
+
* Bearer 헤더 또는 쿠키에서 토큰을 추출하는 커스텀 함수
|
|
8
|
+
* 우선순위: 1. Authorization 헤더 (Bearer) 2. 쿠키 (flc_auth_token)
|
|
9
|
+
*
|
|
10
|
+
* Bearer 헤더 우선: 클라이언트가 명시적으로 설정한 토큰이 가장 신선함.
|
|
11
|
+
* 쿠키는 브라우저가 자동 전송하므로 stale 토큰이 남아있을 수 있음.
|
|
9
12
|
*/
|
|
10
13
|
const extractJwtFromRequest = (req) => {
|
|
11
|
-
// 1.
|
|
12
|
-
if (req.cookies && req.cookies.flc_auth_token) {
|
|
13
|
-
return req.cookies.flc_auth_token;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// 2. Authorization 헤더에서 토큰 확인 (기존 방식 호환)
|
|
14
|
+
// 1. Authorization 헤더에서 토큰 확인 (Bearer 방식 - 우선)
|
|
17
15
|
const authHeader = req.headers.authorization || req.headers.Authorization;
|
|
18
16
|
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
if (authHeader) {
|
|
18
|
+
// Bearer 접두사가 있는 경우
|
|
19
|
+
if (authHeader.startsWith('Bearer ')) {
|
|
20
|
+
return authHeader.substring(7);
|
|
21
|
+
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
// bearer (소문자)인 경우
|
|
24
|
+
if (authHeader.startsWith('bearer ')) {
|
|
25
|
+
return authHeader.substring(7);
|
|
26
|
+
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
// Bearer 접두사 없이 토큰만 있는 경우
|
|
29
|
+
// JWT 형식인지 확인 (xxx.yyy.zzz 형태)
|
|
30
|
+
if (authHeader.split('.').length === 3) {
|
|
31
|
+
return authHeader;
|
|
32
|
+
}
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return authHeader;
|
|
35
|
+
// 2. 쿠키에서 FLC 토큰 확인 (HttpOnly 쿠키 방식 - 폴백)
|
|
36
|
+
if (req.cookies && req.cookies.flc_auth_token) {
|
|
37
|
+
return req.cookies.flc_auth_token;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
return null;
|
|
@@ -77,23 +78,9 @@ function createJwtStrategy() {
|
|
|
77
78
|
'[@ahhaohho/auth-middleware] ⚠️ Token verified with previous key (fallback)'
|
|
78
79
|
);
|
|
79
80
|
} catch (previousKeyError) {
|
|
80
|
-
// 🚨 임시: invalid signature도 허용 (다음 앱 배포 전까지)
|
|
81
|
-
if (currentKeyError.message.includes('invalid signature') || currentKeyError.message.includes('jwt malformed')) {
|
|
82
|
-
console.warn('[@ahhaohho/auth-middleware] ⚠️ [TEMPORARY] Allowing invalid signature');
|
|
83
|
-
request._jwtDecoded = { userId: 'unknown', userRole: 'guest' };
|
|
84
|
-
request._jwtKeyUsed = 'bypassed';
|
|
85
|
-
return done(null, keys.current);
|
|
86
|
-
}
|
|
87
81
|
return done(currentKeyError, false);
|
|
88
82
|
}
|
|
89
83
|
} else {
|
|
90
|
-
// 🚨 임시: invalid signature도 허용 (다음 앱 배포 전까지)
|
|
91
|
-
if (currentKeyError.message.includes('invalid signature') || currentKeyError.message.includes('jwt malformed')) {
|
|
92
|
-
console.warn('[@ahhaohho/auth-middleware] ⚠️ [TEMPORARY] Allowing invalid signature');
|
|
93
|
-
request._jwtDecoded = { userId: 'unknown', userRole: 'guest' };
|
|
94
|
-
request._jwtKeyUsed = 'bypassed';
|
|
95
|
-
return done(null, keys.current);
|
|
96
|
-
}
|
|
97
84
|
return done(currentKeyError, false);
|
|
98
85
|
}
|
|
99
86
|
}
|
|
@@ -3,19 +3,12 @@ const { verifyTokenWithFallback } = require('../utils/jwtValidator');
|
|
|
3
3
|
const { isBlacklisted } = require('../utils/blacklist');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* 우선순위: 1.
|
|
6
|
+
* 헤더 또는 쿠키에서 Refresh Token 추출
|
|
7
|
+
* 우선순위: 1. refresh-token 헤더 2. 쿠키 (flc_refresh_token)
|
|
8
8
|
* Bearer 접두사가 있든 없든 처리
|
|
9
9
|
*/
|
|
10
10
|
function extractRefreshToken(req) {
|
|
11
|
-
// 1.
|
|
12
|
-
if (req && req.cookies && req.cookies.flc_refresh_token) {
|
|
13
|
-
return req.cookies.flc_refresh_token;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// 2. 헤더에서 리프레시 토큰 확인 (기존 방식 호환)
|
|
17
|
-
let token = null;
|
|
18
|
-
|
|
11
|
+
// 1. 헤더에서 리프레시 토큰 확인 (우선)
|
|
19
12
|
if (req && req.headers) {
|
|
20
13
|
let refreshToken = req.headers['refresh-token'] || req.headers['refreshtoken'];
|
|
21
14
|
|
|
@@ -27,11 +20,16 @@ function extractRefreshToken(req) {
|
|
|
27
20
|
refreshToken = refreshToken.substring(7);
|
|
28
21
|
}
|
|
29
22
|
|
|
30
|
-
|
|
23
|
+
return refreshToken.trim();
|
|
31
24
|
}
|
|
32
25
|
}
|
|
33
26
|
|
|
34
|
-
|
|
27
|
+
// 2. 쿠키에서 FLC 리프레시 토큰 확인 (폴백)
|
|
28
|
+
if (req && req.cookies && req.cookies.flc_refresh_token) {
|
|
29
|
+
return req.cookies.flc_refresh_token;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return null;
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
/**
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Request 객체에서 userId를 추출하는 유틸리티 함수
|
|
5
|
+
*
|
|
6
|
+
* 우선순위:
|
|
7
|
+
* 1. req.user.userId (JWT 인증 미들웨어에서 설정)
|
|
8
|
+
* 2. req.userId (이미 설정된 경우)
|
|
9
|
+
* 3. req.headers['user-id'] (NODE_ENV !== 'production'일 때만)
|
|
10
|
+
* 4. req.query.userId (NODE_ENV !== 'production'일 때만)
|
|
11
|
+
*
|
|
12
|
+
* @param {Object} req - Express request 객체 또는 req-like 객체
|
|
13
|
+
* @param {boolean} [isRequired=false] - true면 userId 없을 시 에러 throw
|
|
14
|
+
* @returns {Promise<mongoose.Types.ObjectId|null>} userId (ObjectId) 또는 null
|
|
15
|
+
* @throws {Error} isRequired=true이고 userId가 없거나 유효하지 않은 경우
|
|
16
|
+
*/
|
|
17
|
+
async function extractUserId(req, isRequired = false) {
|
|
18
|
+
const isDebug = process.env.NODE_ENV !== 'production';
|
|
19
|
+
|
|
20
|
+
let userId = null;
|
|
21
|
+
|
|
22
|
+
// 1. JWT 인증 미들웨어에서 설정한 req.user.userId (최우선)
|
|
23
|
+
if (req.user && req.user.userId) {
|
|
24
|
+
userId = req.user.userId;
|
|
25
|
+
}
|
|
26
|
+
// 2. 이미 설정된 req.userId
|
|
27
|
+
else if (req.userId) {
|
|
28
|
+
userId = req.userId;
|
|
29
|
+
}
|
|
30
|
+
// 3. 디버그 모드에서만: 헤더
|
|
31
|
+
else if (isDebug && req.headers && req.headers['user-id']) {
|
|
32
|
+
userId = req.headers['user-id'];
|
|
33
|
+
}
|
|
34
|
+
// 4. 디버그 모드에서만: 쿼리 파라미터
|
|
35
|
+
else if (isDebug && req.query && req.query.userId) {
|
|
36
|
+
userId = req.query.userId;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// userId가 필수가 아니고 없는 경우
|
|
40
|
+
if (!isRequired && !userId) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// userId가 필수이고 없는 경우
|
|
45
|
+
if (isRequired && (!userId || (typeof userId === 'string' && userId.trim() === ''))) {
|
|
46
|
+
const error = new Error('유효하지 않은 요청: userId가 없거나 유효하지 않습니다.');
|
|
47
|
+
error.name = 'ValidationError';
|
|
48
|
+
error.statusCode = 400;
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// userId가 있는 경우 형식 검증 및 ObjectId 변환
|
|
53
|
+
if (userId) {
|
|
54
|
+
// 이미 ObjectId인 경우 그대로 반환
|
|
55
|
+
if (typeof userId !== 'string') {
|
|
56
|
+
if (mongoose.Types.ObjectId.isValid(userId)) {
|
|
57
|
+
return userId;
|
|
58
|
+
}
|
|
59
|
+
const error = new Error('유효하지 않은 userId 형식입니다.');
|
|
60
|
+
error.name = 'ValidationError';
|
|
61
|
+
error.statusCode = 400;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (userId.trim() === '') {
|
|
66
|
+
if (isRequired) {
|
|
67
|
+
const error = new Error('유효하지 않은 userId 형식입니다.');
|
|
68
|
+
error.name = 'ValidationError';
|
|
69
|
+
error.statusCode = 400;
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (mongoose.Types.ObjectId.isValid(userId)) {
|
|
76
|
+
return new mongoose.Types.ObjectId(userId);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isRequired) {
|
|
80
|
+
const error = new Error('유효하지 않은 userId 형식입니다.');
|
|
81
|
+
error.name = 'ValidationError';
|
|
82
|
+
error.statusCode = 400;
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { extractUserId };
|