@ahhaohho/auth-middleware 1.0.8 → 2.0.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ahhaohho/auth-middleware",
3
- "version": "1.0.8",
4
- "description": "Shared authentication middleware with Passport.js for ahhaohho microservices",
3
+ "version": "2.0.0",
4
+ "description": "Shared authentication and authorization middleware for ahhaohho microservices",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
7
7
  "test": "echo \"Warning: no test specified\" && exit 0",
@@ -22,6 +22,9 @@
22
22
  "passport",
23
23
  "jwt",
24
24
  "authentication",
25
+ "authorization",
26
+ "rbac",
27
+ "api-key",
25
28
  "middleware",
26
29
  "ahhaohho"
27
30
  ],
package/src/index.js CHANGED
@@ -1,36 +1,79 @@
1
1
  /**
2
2
  * @ahhaohho/auth-middleware
3
3
  *
4
- * Shared authentication middleware with Passport.js
4
+ * Shared authentication and authorization middleware
5
5
  * for ahhaohho microservices
6
+ *
7
+ * @version 2.0.0
6
8
  */
7
9
 
10
+ // JWT 인증 미들웨어
8
11
  const { authenticateJWT, authenticateRefresh, optionalAuth, authenticateHybrid } = require('./middleware/auth');
12
+
13
+ // API 키 인증 미들웨어
14
+ const { authenticateApiKey, optionalApiKey, combinedAuth, combinedAuthHybrid } = require('./middleware/apiKey');
15
+
16
+ // 역할/권한 인가 미들웨어
17
+ const {
18
+ requireRoles,
19
+ requirePermission,
20
+ requireAccess,
21
+ requireOwnership,
22
+ requireAny,
23
+ clearPermissionCache,
24
+ fetchRolePermissions
25
+ } = require('./middleware/authorization');
26
+
27
+ // 유틸리티
9
28
  const { verifyTokenWithFallback, signToken, getCurrentSigningKey } = require('./utils/jwtValidator');
10
29
  const { isBlacklisted, addToBlacklist, clearBlacklist } = require('./utils/blacklist');
11
30
  const { getJwtKeys, invalidateCache } = require('./utils/secretManager');
31
+ const { verifyApiKey, getApiKeyTTL } = require('./utils/apiKeyValidator');
12
32
  const redisManager = require('./config/redis');
13
33
 
14
- // 메인 export - Express 미들웨어
15
34
  module.exports = {
16
- // 미들웨어
17
- authenticateJWT,
18
- authenticateRefresh,
19
- optionalAuth,
20
- authenticateHybrid,
35
+ // ===== JWT 인증 미들웨어 =====
36
+ authenticateJWT, // JWT Access Token 필수 인증
37
+ authenticateRefresh, // Refresh Token 인증
38
+ optionalAuth, // JWT 선택적 인증 (없어도 통과)
39
+ authenticateHybrid, // JWT + Refresh Token 자동 갱신
40
+
41
+ // ===== API 키 인증 미들웨어 =====
42
+ authenticateApiKey, // API 키 필수 인증
43
+ optionalApiKey, // API 키 선택적 인증
44
+ combinedAuth, // API 키 + JWT 복합 인증
45
+ combinedAuthHybrid, // API 키 + JWT Hybrid 복합 인증
46
+
47
+ // ===== 역할/권한 인가 미들웨어 =====
48
+ requireRoles, // 역할 기반 인가: requireRoles(['admin', 'teacher'])
49
+ requirePermission, // 권한 기반 인가: requirePermission('content:write')
50
+ requireAccess, // 접근 범위 기반 인가: requireAccess('AdminPanel')
51
+ requireOwnership, // 리소스 소유자 검증: requireOwnership('userId')
52
+ requireAny, // 복합 조건 인가: requireAny({ roles: [...], permissions: [...] })
21
53
 
22
- // 유틸리티 함수 (필요시 직접 사용)
54
+ // ===== 유틸리티 함수 =====
23
55
  utils: {
56
+ // JWT 관련
24
57
  verifyTokenWithFallback,
25
58
  signToken,
26
59
  getCurrentSigningKey,
60
+ getJwtKeys,
61
+ invalidateCache,
62
+
63
+ // 블랙리스트 관련
27
64
  isBlacklisted,
28
65
  addToBlacklist,
29
66
  clearBlacklist,
30
- getJwtKeys,
31
- invalidateCache
67
+
68
+ // API 키 관련
69
+ verifyApiKey,
70
+ getApiKeyTTL,
71
+
72
+ // 권한 캐시 관련
73
+ clearPermissionCache,
74
+ fetchRolePermissions
32
75
  },
33
76
 
34
- // Redis 관리자 (필요시 직접 접근)
77
+ // Redis 관리자
35
78
  redisManager
36
79
  };
@@ -0,0 +1,196 @@
1
+ const { verifyApiKey } = require('../utils/apiKeyValidator');
2
+
3
+ /**
4
+ * API 키 인증 미들웨어
5
+ * Redis에서 로컬 검증 수행
6
+ */
7
+
8
+ /**
9
+ * API 키 필수 인증 미들웨어
10
+ * x-api-key와 device-id 헤더가 필수
11
+ *
12
+ * @example
13
+ * router.post('/internal', authenticateApiKey, handler);
14
+ */
15
+ async function authenticateApiKey(req, res, next) {
16
+ const apiKey = req.headers['x-api-key'];
17
+ const deviceId = req.headers['device-id'];
18
+
19
+ if (!apiKey || !deviceId) {
20
+ return res.status(401).json({
21
+ type: 'client',
22
+ error: 'Unauthorized',
23
+ message: 'API key and device ID are required'
24
+ });
25
+ }
26
+
27
+ const result = await verifyApiKey(deviceId, apiKey);
28
+
29
+ if (!result.valid) {
30
+ return res.status(401).json({
31
+ type: 'client',
32
+ error: 'Unauthorized',
33
+ message: result.error || 'Invalid API key'
34
+ });
35
+ }
36
+
37
+ // 인증 성공: 플래그 및 정보 설정
38
+ req.apiKeyVerified = true;
39
+ req.skipAuthorization = true; // 하위 호환성
40
+ req.deviceId = deviceId;
41
+
42
+ console.log('[@ahhaohho/auth-middleware] API key verified for device:', deviceId);
43
+ next();
44
+ }
45
+
46
+ /**
47
+ * API 키 선택적 인증 미들웨어
48
+ * API 키가 없어도 통과, 있으면 검증
49
+ *
50
+ * @example
51
+ * router.get('/public', optionalApiKey, handler);
52
+ */
53
+ async function optionalApiKey(req, res, next) {
54
+ const apiKey = req.headers['x-api-key'];
55
+ const deviceId = req.headers['device-id'];
56
+
57
+ if (!apiKey || !deviceId) {
58
+ return next();
59
+ }
60
+
61
+ const result = await verifyApiKey(deviceId, apiKey);
62
+
63
+ if (result.valid) {
64
+ req.apiKeyVerified = true;
65
+ req.skipAuthorization = true;
66
+ req.deviceId = deviceId;
67
+ console.log('[@ahhaohho/auth-middleware] Optional API key verified for device:', deviceId);
68
+ }
69
+
70
+ next();
71
+ }
72
+
73
+ /**
74
+ * API 키 + JWT 복합 인증 미들웨어
75
+ * API 키 먼저 시도, 실패하면 JWT로 fallback
76
+ *
77
+ * @example
78
+ * router.get('/resource', combinedAuth, handler);
79
+ */
80
+ async function combinedAuth(req, res, next) {
81
+ const apiKey = req.headers['x-api-key'];
82
+ const deviceId = req.headers['device-id'];
83
+
84
+ // 1. API 키가 있으면 먼저 시도
85
+ if (apiKey && deviceId) {
86
+ const result = await verifyApiKey(deviceId, apiKey);
87
+
88
+ if (result.valid) {
89
+ req.apiKeyVerified = true;
90
+ req.skipAuthorization = true;
91
+ req.deviceId = deviceId;
92
+
93
+ // 하위 호환성: req.userId, req.userRole 설정
94
+ req.userId = null;
95
+ req.userRole = 'api_client';
96
+
97
+ console.log('[@ahhaohho/auth-middleware] Combined auth: API key verified');
98
+ return next();
99
+ }
100
+
101
+ console.log('[@ahhaohho/auth-middleware] Combined auth: API key failed, trying JWT...');
102
+ }
103
+
104
+ // 2. JWT 인증으로 fallback
105
+ const { authenticateJWT } = require('./auth');
106
+
107
+ authenticateJWT(req, res, (err) => {
108
+ if (err) {
109
+ return res.status(401).json({
110
+ type: 'client',
111
+ error: 'Unauthorized',
112
+ message: 'No valid authentication provided'
113
+ });
114
+ }
115
+
116
+ // JWT 인증 실패 (req.user가 없음)
117
+ if (!req.user) {
118
+ return res.status(401).json({
119
+ type: 'client',
120
+ error: 'Unauthorized',
121
+ message: 'No valid authentication provided'
122
+ });
123
+ }
124
+
125
+ // 하위 호환성: req.userId, req.userRole 설정
126
+ req.userId = req.user.userId;
127
+ req.userRole = req.user.userRole;
128
+
129
+ next();
130
+ });
131
+ }
132
+
133
+ /**
134
+ * API 키 + JWT Hybrid 인증 미들웨어
135
+ * API 키 먼저 시도, 실패하면 JWT Hybrid(자동 갱신)로 fallback
136
+ *
137
+ * @example
138
+ * router.get('/resource', combinedAuthHybrid, handler);
139
+ */
140
+ async function combinedAuthHybrid(req, res, next) {
141
+ const apiKey = req.headers['x-api-key'];
142
+ const deviceId = req.headers['device-id'];
143
+
144
+ // 1. API 키가 있으면 먼저 시도
145
+ if (apiKey && deviceId) {
146
+ const result = await verifyApiKey(deviceId, apiKey);
147
+
148
+ if (result.valid) {
149
+ req.apiKeyVerified = true;
150
+ req.skipAuthorization = true;
151
+ req.deviceId = deviceId;
152
+
153
+ req.userId = null;
154
+ req.userRole = 'api_client';
155
+
156
+ console.log('[@ahhaohho/auth-middleware] Combined hybrid auth: API key verified');
157
+ return next();
158
+ }
159
+
160
+ console.log('[@ahhaohho/auth-middleware] Combined hybrid auth: API key failed, trying JWT hybrid...');
161
+ }
162
+
163
+ // 2. JWT Hybrid 인증으로 fallback (자동 토큰 갱신)
164
+ const { authenticateHybrid } = require('./auth');
165
+
166
+ authenticateHybrid(req, res, (err) => {
167
+ if (err) {
168
+ return res.status(401).json({
169
+ type: 'client',
170
+ error: 'Unauthorized',
171
+ message: 'No valid authentication provided'
172
+ });
173
+ }
174
+
175
+ if (!req.user) {
176
+ return res.status(401).json({
177
+ type: 'client',
178
+ error: 'Unauthorized',
179
+ message: 'No valid authentication provided'
180
+ });
181
+ }
182
+
183
+ // 하위 호환성
184
+ req.userId = req.user.userId;
185
+ req.userRole = req.user.userRole;
186
+
187
+ next();
188
+ });
189
+ }
190
+
191
+ module.exports = {
192
+ authenticateApiKey,
193
+ optionalApiKey,
194
+ combinedAuth,
195
+ combinedAuthHybrid
196
+ };
@@ -52,6 +52,11 @@ function authenticateJWT(req, res, next) {
52
52
 
53
53
  // req.user에 사용자 정보 주입
54
54
  req.user = user;
55
+
56
+ // 하위 호환성: req.userId, req.userRole 설정
57
+ req.userId = user.userId;
58
+ req.userRole = user.userRole;
59
+
55
60
  next();
56
61
  })(req, res, next);
57
62
  }
@@ -85,6 +90,11 @@ function authenticateRefresh(req, res, next) {
85
90
  }
86
91
 
87
92
  req.user = user;
93
+
94
+ // 하위 호환성: req.userId, req.userRole 설정
95
+ req.userId = user.userId;
96
+ req.userRole = user.userRole;
97
+
88
98
  next();
89
99
  })(req, res, next);
90
100
  }
@@ -109,6 +119,10 @@ function optionalAuth(req, res, next) {
109
119
  // 에러나 인증 실패해도 통과
110
120
  if (user) {
111
121
  req.user = user;
122
+
123
+ // 하위 호환성: req.userId, req.userRole 설정
124
+ req.userId = user.userId;
125
+ req.userRole = user.userRole;
112
126
  }
113
127
  next();
114
128
  })(req, res, next);
@@ -144,6 +158,11 @@ async function authenticateHybrid(req, res, next) {
144
158
  // Access token이 유효한 경우
145
159
  if (user) {
146
160
  req.user = user;
161
+
162
+ // 하위 호환성: req.userId, req.userRole 설정
163
+ req.userId = user.userId;
164
+ req.userRole = user.userRole;
165
+
147
166
  return next();
148
167
  }
149
168
 
@@ -197,6 +216,11 @@ async function authenticateHybrid(req, res, next) {
197
216
 
198
217
  // 6. req.user 설정하고 계속 진행
199
218
  req.user = refreshUser;
219
+
220
+ // 하위 호환성: req.userId, req.userRole 설정
221
+ req.userId = refreshUser.userId;
222
+ req.userRole = refreshUser.userRole;
223
+
200
224
  next();
201
225
  } catch (tokenError) {
202
226
  console.error('[@ahhaohho/auth-middleware] Failed to generate new token:', tokenError.message);
@@ -0,0 +1,348 @@
1
+ const redisManager = require('../config/redis');
2
+
3
+ /**
4
+ * 역할/권한 기반 인가 미들웨어
5
+ */
6
+
7
+ // 권한 캐시 (메모리, 5분 TTL)
8
+ let permissionCache = {};
9
+ const CACHE_TTL = 5 * 60 * 1000;
10
+
11
+ /**
12
+ * 역할 권한 정보 조회 (캐시 포함)
13
+ * @param {string} role - 역할 이름
14
+ * @returns {Promise<{access: string[], permissions: string[]} | null>}
15
+ */
16
+ async function fetchRolePermissions(role) {
17
+ const cacheKey = `role:${role}`;
18
+ const cached = permissionCache[cacheKey];
19
+
20
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
21
+ return cached.data;
22
+ }
23
+
24
+ try {
25
+ const redis = redisManager.getClient('default');
26
+ const roleDataStr = await redis.get(`role_permissions:${role}`);
27
+
28
+ if (roleDataStr) {
29
+ const roleData = JSON.parse(roleDataStr);
30
+ permissionCache[cacheKey] = {
31
+ data: roleData,
32
+ timestamp: Date.now()
33
+ };
34
+ return roleData;
35
+ }
36
+
37
+ return null;
38
+ } catch (error) {
39
+ console.error('[@ahhaohho/auth-middleware] Failed to fetch role permissions:', error.message);
40
+ return null;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * 권한 캐시 초기화
46
+ */
47
+ function clearPermissionCache() {
48
+ permissionCache = {};
49
+ console.log('[@ahhaohho/auth-middleware] Permission cache cleared');
50
+ }
51
+
52
+ /**
53
+ * 역할 기반 인가 미들웨어
54
+ * 사용자가 지정된 역할 중 하나를 가지고 있는지 확인
55
+ *
56
+ * @param {string[]} allowedRoles - 허용된 역할 배열
57
+ * @returns {Function} Express 미들웨어
58
+ *
59
+ * @example
60
+ * router.post('/admin', authenticateJWT, requireRoles(['admin']), handler);
61
+ * router.get('/content', combinedAuth, requireRoles(['admin', 'teacher', 'user']), handler);
62
+ */
63
+ function requireRoles(allowedRoles) {
64
+ return async (req, res, next) => {
65
+ // API 키 인증인 경우 스킵
66
+ if (req.skipAuthorization || req.apiKeyVerified) {
67
+ return next();
68
+ }
69
+
70
+ // 사용자 역할 확인 (req.user 또는 req.userRole)
71
+ const userRole = req.user?.userRole || req.userRole;
72
+
73
+ if (!userRole) {
74
+ return res.status(403).json({
75
+ type: 'client',
76
+ error: 'Forbidden',
77
+ message: 'No role information found'
78
+ });
79
+ }
80
+
81
+ if (!allowedRoles.includes(userRole)) {
82
+ return res.status(403).json({
83
+ type: 'client',
84
+ error: 'Forbidden',
85
+ message: `Role '${userRole}' is not authorized. Required: ${allowedRoles.join(', ')}`
86
+ });
87
+ }
88
+
89
+ next();
90
+ };
91
+ }
92
+
93
+ /**
94
+ * 권한 기반 인가 미들웨어
95
+ * 사용자가 특정 권한을 가지고 있는지 확인
96
+ *
97
+ * @param {string} requiredPermission - 필요한 권한 (예: 'challenge:content:write')
98
+ * @returns {Function} Express 미들웨어
99
+ *
100
+ * @example
101
+ * router.delete('/post/:id', combinedAuth, requirePermission('community:post:delete'), handler);
102
+ */
103
+ function requirePermission(requiredPermission) {
104
+ return async (req, res, next) => {
105
+ // API 키 인증인 경우 스킵
106
+ if (req.skipAuthorization || req.apiKeyVerified) {
107
+ return next();
108
+ }
109
+
110
+ const userRole = req.user?.userRole || req.userRole;
111
+
112
+ if (!userRole) {
113
+ return res.status(403).json({
114
+ type: 'client',
115
+ error: 'Forbidden',
116
+ message: 'No role information found'
117
+ });
118
+ }
119
+
120
+ // 1. 토큰에 권한이 포함되어 있는지 확인
121
+ const tokenPermissions = req.user?.permissions;
122
+ if (tokenPermissions && Array.isArray(tokenPermissions)) {
123
+ if (tokenPermissions.includes('*') || tokenPermissions.includes(requiredPermission)) {
124
+ return next();
125
+ }
126
+
127
+ // 와일드카드 매칭 (예: 'challenge:*' → 'challenge:content:write' 허용)
128
+ const hasWildcardMatch = tokenPermissions.some(perm => {
129
+ if (perm.endsWith(':*')) {
130
+ const prefix = perm.slice(0, -1);
131
+ return requiredPermission.startsWith(prefix);
132
+ }
133
+ return false;
134
+ });
135
+
136
+ if (hasWildcardMatch) {
137
+ return next();
138
+ }
139
+ }
140
+
141
+ // 2. 캐시/Redis에서 역할 권한 조회
142
+ const roleData = await fetchRolePermissions(userRole);
143
+
144
+ if (roleData && roleData.permissions) {
145
+ const permissions = roleData.permissions;
146
+
147
+ if (permissions.includes('*') || permissions.includes(requiredPermission)) {
148
+ return next();
149
+ }
150
+
151
+ // 와일드카드 매칭
152
+ const hasWildcardMatch = permissions.some(perm => {
153
+ if (perm.endsWith(':*')) {
154
+ const prefix = perm.slice(0, -1);
155
+ return requiredPermission.startsWith(prefix);
156
+ }
157
+ return false;
158
+ });
159
+
160
+ if (hasWildcardMatch) {
161
+ return next();
162
+ }
163
+ }
164
+
165
+ return res.status(403).json({
166
+ type: 'client',
167
+ error: 'Forbidden',
168
+ message: `Permission '${requiredPermission}' is required`
169
+ });
170
+ };
171
+ }
172
+
173
+ /**
174
+ * 접근 범위 기반 인가 미들웨어
175
+ * 사용자 역할이 특정 접근 범위를 가지고 있는지 확인
176
+ *
177
+ * @param {string} requiredAccess - 필요한 접근 범위 (예: 'Content', 'AdminPanel')
178
+ * @returns {Function} Express 미들웨어
179
+ *
180
+ * @example
181
+ * router.get('/admin/dashboard', combinedAuth, requireAccess('AdminPanel'), handler);
182
+ */
183
+ function requireAccess(requiredAccess) {
184
+ return async (req, res, next) => {
185
+ // API 키 인증인 경우 스킵
186
+ if (req.skipAuthorization || req.apiKeyVerified) {
187
+ return next();
188
+ }
189
+
190
+ const userRole = req.user?.userRole || req.userRole;
191
+
192
+ if (!userRole) {
193
+ return res.status(403).json({
194
+ type: 'client',
195
+ error: 'Forbidden',
196
+ message: 'No role information found'
197
+ });
198
+ }
199
+
200
+ const roleData = await fetchRolePermissions(userRole);
201
+
202
+ if (!roleData || !roleData.access) {
203
+ return res.status(403).json({
204
+ type: 'client',
205
+ error: 'Forbidden',
206
+ message: 'Failed to verify access'
207
+ });
208
+ }
209
+
210
+ if (!roleData.access.includes('*') && !roleData.access.includes(requiredAccess)) {
211
+ return res.status(403).json({
212
+ type: 'client',
213
+ error: 'Forbidden',
214
+ message: `Access to '${requiredAccess}' is required`
215
+ });
216
+ }
217
+
218
+ next();
219
+ };
220
+ }
221
+
222
+ /**
223
+ * 리소스 소유자 검증 미들웨어
224
+ * 사용자가 자신의 리소스에만 접근할 수 있도록 제한
225
+ * admin 역할은 모든 리소스 접근 가능
226
+ *
227
+ * @param {string} paramName - 라우트 파라미터 이름 (기본값: 'userId')
228
+ * @returns {Function} Express 미들웨어
229
+ *
230
+ * @example
231
+ * router.get('/users/:userId/profile', combinedAuth, requireOwnership('userId'), handler);
232
+ * router.delete('/posts/:postUserId', combinedAuth, requireOwnership('postUserId'), handler);
233
+ */
234
+ function requireOwnership(paramName = 'userId') {
235
+ return (req, res, next) => {
236
+ // API 키 인증인 경우 스킵
237
+ if (req.skipAuthorization || req.apiKeyVerified) {
238
+ return next();
239
+ }
240
+
241
+ const userId = req.user?.userId || req.userId;
242
+ const resourceOwnerId = req.params[paramName];
243
+
244
+ // admin은 모든 리소스 접근 가능
245
+ const userRole = req.user?.userRole || req.userRole;
246
+ if (userRole === 'admin') {
247
+ return next();
248
+ }
249
+
250
+ if (!userId) {
251
+ return res.status(403).json({
252
+ type: 'client',
253
+ error: 'Forbidden',
254
+ message: 'User ID not found'
255
+ });
256
+ }
257
+
258
+ if (userId !== resourceOwnerId) {
259
+ return res.status(403).json({
260
+ type: 'client',
261
+ error: 'Forbidden',
262
+ message: 'You can only access your own resources'
263
+ });
264
+ }
265
+
266
+ next();
267
+ };
268
+ }
269
+
270
+ /**
271
+ * 복합 권한 검증 미들웨어
272
+ * 여러 조건 중 하나라도 만족하면 통과
273
+ *
274
+ * @param {Object} options - 권한 옵션
275
+ * @param {string[]} [options.roles] - 허용된 역할 배열
276
+ * @param {string[]} [options.permissions] - 필요한 권한 중 하나
277
+ * @param {string[]} [options.access] - 필요한 접근 범위 중 하나
278
+ * @returns {Function} Express 미들웨어
279
+ *
280
+ * @example
281
+ * router.put('/content/:id', combinedAuth, requireAny({
282
+ * roles: ['admin'],
283
+ * permissions: ['content:write:any']
284
+ * }), handler);
285
+ */
286
+ function requireAny(options = {}) {
287
+ return async (req, res, next) => {
288
+ // API 키 인증인 경우 스킵
289
+ if (req.skipAuthorization || req.apiKeyVerified) {
290
+ return next();
291
+ }
292
+
293
+ const userRole = req.user?.userRole || req.userRole;
294
+
295
+ if (!userRole) {
296
+ return res.status(403).json({
297
+ type: 'client',
298
+ error: 'Forbidden',
299
+ message: 'No role information found'
300
+ });
301
+ }
302
+
303
+ // 1. 역할 확인
304
+ if (options.roles && options.roles.includes(userRole)) {
305
+ return next();
306
+ }
307
+
308
+ // 역할 권한 데이터 조회
309
+ const roleData = await fetchRolePermissions(userRole);
310
+ const tokenPermissions = req.user?.permissions || [];
311
+ const allPermissions = roleData?.permissions || [];
312
+ const allAccess = roleData?.access || [];
313
+
314
+ // 2. 권한 확인
315
+ if (options.permissions) {
316
+ for (const perm of options.permissions) {
317
+ if (tokenPermissions.includes(perm) || allPermissions.includes(perm) || allPermissions.includes('*')) {
318
+ return next();
319
+ }
320
+ }
321
+ }
322
+
323
+ // 3. 접근 범위 확인
324
+ if (options.access) {
325
+ for (const acc of options.access) {
326
+ if (allAccess.includes(acc) || allAccess.includes('*')) {
327
+ return next();
328
+ }
329
+ }
330
+ }
331
+
332
+ return res.status(403).json({
333
+ type: 'client',
334
+ error: 'Forbidden',
335
+ message: 'Insufficient permissions'
336
+ });
337
+ };
338
+ }
339
+
340
+ module.exports = {
341
+ requireRoles,
342
+ requirePermission,
343
+ requireAccess,
344
+ requireOwnership,
345
+ requireAny,
346
+ clearPermissionCache,
347
+ fetchRolePermissions
348
+ };
@@ -0,0 +1,71 @@
1
+ const redisManager = require('../config/redis');
2
+
3
+ /**
4
+ * API 키 검증 유틸리티
5
+ * Redis에 저장된 API 키와 비교하여 검증
6
+ */
7
+
8
+ /**
9
+ * API 키 검증
10
+ * @param {string} deviceId - 디바이스 ID (Redis 키)
11
+ * @param {string} apiKey - 검증할 API 키
12
+ * @returns {Promise<{valid: boolean, deviceId?: string, error?: string}>}
13
+ */
14
+ async function verifyApiKey(deviceId, apiKey) {
15
+ if (!deviceId || !apiKey) {
16
+ return {
17
+ valid: false,
18
+ error: 'Device ID and API key are required'
19
+ };
20
+ }
21
+
22
+ try {
23
+ const redis = redisManager.getClient('apikey');
24
+ const storedKey = await redis.get(deviceId);
25
+
26
+ if (!storedKey) {
27
+ return {
28
+ valid: false,
29
+ error: 'API key not found or expired'
30
+ };
31
+ }
32
+
33
+ if (apiKey !== storedKey) {
34
+ return {
35
+ valid: false,
36
+ error: 'Invalid API key'
37
+ };
38
+ }
39
+
40
+ return {
41
+ valid: true,
42
+ deviceId: deviceId
43
+ };
44
+ } catch (error) {
45
+ console.error('[@ahhaohho/auth-middleware] API key verification error:', error.message);
46
+ return {
47
+ valid: false,
48
+ error: 'API key verification failed'
49
+ };
50
+ }
51
+ }
52
+
53
+ /**
54
+ * API 키 TTL 확인
55
+ * @param {string} deviceId - 디바이스 ID
56
+ * @returns {Promise<number>} TTL in seconds (-1 if no expiry, -2 if not exists)
57
+ */
58
+ async function getApiKeyTTL(deviceId) {
59
+ try {
60
+ const redis = redisManager.getClient('apikey');
61
+ return await redis.ttl(deviceId);
62
+ } catch (error) {
63
+ console.error('[@ahhaohho/auth-middleware] API key TTL check error:', error.message);
64
+ return -2;
65
+ }
66
+ }
67
+
68
+ module.exports = {
69
+ verifyApiKey,
70
+ getApiKeyTTL
71
+ };