@ahhaohho/auth-middleware 1.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/README.md ADDED
@@ -0,0 +1,214 @@
1
+ # @ahhaohho/auth-middleware
2
+
3
+ Shared authentication middleware with Passport.js for ahhaohho microservices.
4
+
5
+ ## Features
6
+
7
+ - ✅ Passport.js JWT authentication strategy
8
+ - ✅ Multi-key JWT verification with fallback support
9
+ - ✅ Redis-based token blacklist
10
+ - ✅ AWS Secrets Manager integration
11
+ - ✅ Express middleware ready
12
+
13
+ ## Installation
14
+
15
+ ### Using Git (recommended for private packages)
16
+
17
+ ```bash
18
+ npm install git+ssh://git@github.com:ahhaohho/auth-middleware.git#v1.0.0
19
+ ```
20
+
21
+ Or add to `package.json`:
22
+
23
+ ```json
24
+ {
25
+ "dependencies": {
26
+ "@ahhaohho/auth-middleware": "git+ssh://git@github.com:ahhaohho/auth-middleware.git#v1.0.0"
27
+ }
28
+ }
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ### Basic Setup
34
+
35
+ ```javascript
36
+ const express = require('express');
37
+ const { authenticateJWT, authenticateRefresh } = require('@ahhaohho/auth-middleware');
38
+
39
+ const app = express();
40
+
41
+ // Environment variables required
42
+ // AWS_REGION=ap-northeast-2
43
+ // REDIS_HOST=your-redis-host
44
+ // REDIS_PORT=6379
45
+ // JWT_SECRET_NAME=your-secret-name
46
+
47
+ // Protected routes
48
+ app.get('/api/verify', authenticateJWT, (req, res) => {
49
+ res.json({
50
+ userId: req.user.userId,
51
+ userRole: req.user.userRole
52
+ });
53
+ });
54
+
55
+ app.get('/api/refresh', authenticateRefresh, (req, res) => {
56
+ // Generate new access token
57
+ res.json({ newAccessToken: '...' });
58
+ });
59
+
60
+ app.listen(3000);
61
+ ```
62
+
63
+ ### Environment Variables
64
+
65
+ ```bash
66
+ # Required
67
+ AWS_REGION=ap-northeast-2
68
+ REDIS_HOST=your-redis-host
69
+ REDIS_PORT=6379
70
+ JWT_SECRET_NAME=your-secret-name
71
+
72
+ # Optional
73
+ ELASTICACHE_ENDPOINT=your-elasticache-endpoint # If using ElastiCache
74
+ ```
75
+
76
+ ## Architecture
77
+
78
+ ### JWT Verification Flow
79
+
80
+ ```
81
+ Request with JWT
82
+
83
+ authenticateJWT middleware
84
+
85
+ Extract token from Authorization header
86
+
87
+ Verify with current JWT key
88
+ ↓ (if fails)
89
+ Fallback to previous JWT key
90
+
91
+ Check Redis blacklist
92
+
93
+ Inject user data to req.user
94
+
95
+ Next middleware
96
+ ```
97
+
98
+ ### Multi-Key Support
99
+
100
+ Supports seamless JWT key rotation:
101
+ - Verifies with current key first
102
+ - Falls back to previous key if current fails
103
+ - Allows zero-downtime key rotation
104
+
105
+ ### Token Blacklist
106
+
107
+ Uses Redis to maintain revoked tokens:
108
+ - Stores blacklisted tokens per user
109
+ - Automatically expires with token TTL
110
+ - Checked on every authentication
111
+
112
+ ## API Reference
113
+
114
+ ### `authenticateJWT(req, res, next)`
115
+
116
+ Passport.js middleware for JWT authentication.
117
+
118
+ **Headers:**
119
+ - `Authorization: Bearer <access_token>`
120
+
121
+ **Sets:**
122
+ - `req.user`: `{ userId, userRole, phoneNumber }`
123
+
124
+ **Errors:**
125
+ - 401: Unauthorized (invalid or expired token)
126
+ - 500: Authentication error
127
+
128
+ ### `authenticateRefresh(req, res, next)`
129
+
130
+ Passport.js middleware for refresh token authentication.
131
+
132
+ **Headers:**
133
+ - `Refresh-Token: Bearer <refresh_token>`
134
+
135
+ **Sets:**
136
+ - `req.user`: `{ userId, userRole, phoneNumber }`
137
+
138
+ **Errors:**
139
+ - 401: Invalid refresh token
140
+ - 500: Token refresh error
141
+
142
+ ## Development
143
+
144
+ ### Project Structure
145
+
146
+ ```
147
+ auth-middleware/
148
+ ├── src/
149
+ │ ├── index.js # Main export
150
+ │ ├── strategies/
151
+ │ │ ├── jwt.strategy.js # Passport JWT strategy
152
+ │ │ └── refresh.strategy.js # Refresh token strategy
153
+ │ ├── middleware/
154
+ │ │ └── auth.js # Express middleware
155
+ │ ├── utils/
156
+ │ │ ├── jwtValidator.js # Multi-key verification
157
+ │ │ ├── blacklist.js # Redis blacklist
158
+ │ │ └── secretManager.js # AWS Secrets Manager
159
+ │ └── config/
160
+ │ └── redis.js # Redis client singleton
161
+ ├── package.json
162
+ └── README.md
163
+ ```
164
+
165
+ ### Testing Locally
166
+
167
+ ```bash
168
+ # Clone the repository
169
+ git clone git@github.com:ahhaohho/auth-middleware.git
170
+ cd auth-middleware
171
+
172
+ # Install dependencies
173
+ npm install
174
+
175
+ # Link locally for testing
176
+ npm link
177
+
178
+ # In your service directory
179
+ npm link @ahhaohho/auth-middleware
180
+ ```
181
+
182
+ ## Versioning
183
+
184
+ This package follows [Semantic Versioning](https://semver.org/).
185
+
186
+ ### Creating a New Version
187
+
188
+ ```bash
189
+ # Update version in package.json
190
+ npm version patch # 1.0.0 -> 1.0.1
191
+ npm version minor # 1.0.0 -> 1.1.0
192
+ npm version major # 1.0.0 -> 2.0.0
193
+
194
+ # Push with tags
195
+ git push origin main --tags
196
+ ```
197
+
198
+ ### Using Specific Versions
199
+
200
+ ```json
201
+ {
202
+ "dependencies": {
203
+ "@ahhaohho/auth-middleware": "git+ssh://git@github.com:ahhaohho/auth-middleware.git#v1.0.0"
204
+ }
205
+ }
206
+ ```
207
+
208
+ ## Migration Guide
209
+
210
+ See [MIGRATION.md](./MIGRATION.md) for detailed migration guide from HTTP-based authentication to Passport.js.
211
+
212
+ ## License
213
+
214
+ MIT
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@ahhaohho/auth-middleware",
3
+ "version": "1.0.0",
4
+ "description": "Shared authentication middleware with Passport.js for ahhaohho microservices",
5
+ "main": "src/index.js",
6
+ "scripts": {
7
+ "test": "echo \"Warning: no test specified\" && exit 0",
8
+ "prepublishOnly": "echo \"Publishing package...\""
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/ahhaohho/auth-middleware.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/ahhaohho/auth-middleware/issues"
16
+ },
17
+ "homepage": "https://github.com/ahhaohho/auth-middleware#readme",
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "keywords": [
22
+ "passport",
23
+ "jwt",
24
+ "authentication",
25
+ "middleware",
26
+ "ahhaohho"
27
+ ],
28
+ "author": "ahhaohho",
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "passport": "^0.7.0",
32
+ "passport-jwt": "^4.0.1",
33
+ "jsonwebtoken": "^9.0.2",
34
+ "ioredis": "^5.4.1",
35
+ "@aws-sdk/client-secrets-manager": "^3.552.0"
36
+ },
37
+ "peerDependencies": {
38
+ "express": "^4.x"
39
+ },
40
+ "engines": {
41
+ "node": ">=16.0.0"
42
+ }
43
+ }
@@ -0,0 +1,70 @@
1
+ const Redis = require('ioredis');
2
+
3
+ /**
4
+ * Redis 클라이언트 싱글톤
5
+ * 모든 서비스에서 하나의 Redis 연결만 사용
6
+ */
7
+ class RedisManager {
8
+ constructor() {
9
+ this.clients = {};
10
+ }
11
+
12
+ /**
13
+ * Redis 클라이언트 가져오기
14
+ * @param {string} type - 클라이언트 타입 (session, token, keys)
15
+ * @returns {Redis} Redis 클라이언트 인스턴스
16
+ */
17
+ getClient(type = 'default') {
18
+ if (!this.clients[type]) {
19
+ this.clients[type] = this.createClient(type);
20
+ }
21
+ return this.clients[type];
22
+ }
23
+
24
+ /**
25
+ * Redis 클라이언트 생성
26
+ * @param {string} type - 클라이언트 타입
27
+ * @returns {Redis} Redis 클라이언트
28
+ */
29
+ createClient(type) {
30
+ const config = {
31
+ host: process.env.REDIS_HOST || process.env.ELASTICACHE_ENDPOINT || 'localhost',
32
+ port: parseInt(process.env.REDIS_PORT || '6379', 10),
33
+ connectTimeout: 10000,
34
+ retryDelayOnFailover: 100,
35
+ enableReadyCheck: false,
36
+ maxRetriesPerRequest: null
37
+ };
38
+
39
+ // ElastiCache 사용 시 TLS 활성화
40
+ if (process.env.ELASTICACHE_ENDPOINT) {
41
+ config.tls = {};
42
+ }
43
+
44
+ const client = new Redis(config);
45
+
46
+ client.on('error', (err) => {
47
+ console.error(`[@ahhaohho/auth-middleware] Redis (${type}) error:`, err.message);
48
+ });
49
+
50
+ client.on('connect', () => {
51
+ console.log(`[@ahhaohho/auth-middleware] Redis (${type}) connected`);
52
+ });
53
+
54
+ return client;
55
+ }
56
+
57
+ /**
58
+ * 모든 클라이언트 연결 종료
59
+ */
60
+ async closeAll() {
61
+ const closePromises = Object.values(this.clients).map(client => client.quit());
62
+ await Promise.all(closePromises);
63
+ this.clients = {};
64
+ }
65
+ }
66
+
67
+ // 싱글톤 인스턴스
68
+ const redisManager = new RedisManager();
69
+
70
+ module.exports = redisManager;
package/src/index.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @ahhaohho/auth-middleware
3
+ *
4
+ * Shared authentication middleware with Passport.js
5
+ * for ahhaohho microservices
6
+ */
7
+
8
+ const { authenticateJWT, authenticateRefresh, optionalAuth } = require('./middleware/auth');
9
+ const { verifyTokenWithFallback, signToken, getCurrentSigningKey } = require('./utils/jwtValidator');
10
+ const { isBlacklisted, addToBlacklist, clearBlacklist } = require('./utils/blacklist');
11
+ const { getJwtKeys, invalidateCache } = require('./utils/secretManager');
12
+ const redisManager = require('./config/redis');
13
+
14
+ // 메인 export - Express 미들웨어
15
+ module.exports = {
16
+ // 미들웨어
17
+ authenticateJWT,
18
+ authenticateRefresh,
19
+ optionalAuth,
20
+
21
+ // 유틸리티 함수 (필요시 직접 사용)
22
+ utils: {
23
+ verifyTokenWithFallback,
24
+ signToken,
25
+ getCurrentSigningKey,
26
+ isBlacklisted,
27
+ addToBlacklist,
28
+ clearBlacklist,
29
+ getJwtKeys,
30
+ invalidateCache
31
+ },
32
+
33
+ // Redis 관리자 (필요시 직접 접근)
34
+ redisManager
35
+ };
@@ -0,0 +1,121 @@
1
+ const passport = require('passport');
2
+ const createJwtStrategy = require('../strategies/jwt.strategy');
3
+ const createRefreshStrategy = require('../strategies/refresh.strategy');
4
+
5
+ /**
6
+ * Passport 초기화 상태
7
+ */
8
+ let initialized = false;
9
+
10
+ /**
11
+ * Passport 전략 초기화
12
+ * 한 번만 실행됨
13
+ */
14
+ function initializePassport() {
15
+ if (!initialized) {
16
+ console.log('[@ahhaohho/auth-middleware] Initializing Passport strategies...');
17
+
18
+ passport.use('jwt', createJwtStrategy());
19
+ passport.use('refresh', createRefreshStrategy());
20
+
21
+ initialized = true;
22
+ console.log('[@ahhaohho/auth-middleware] Passport strategies initialized');
23
+ }
24
+ }
25
+
26
+ /**
27
+ * JWT Access Token 인증 미들웨어
28
+ *
29
+ * @example
30
+ * router.get('/verify', authenticateJWT, (req, res) => {
31
+ * res.json({ userId: req.user.userId });
32
+ * });
33
+ */
34
+ function authenticateJWT(req, res, next) {
35
+ initializePassport();
36
+
37
+ passport.authenticate('jwt', { session: false }, (err, user, info) => {
38
+ if (err) {
39
+ console.error('[@ahhaohho/auth-middleware] Authentication error:', err.message);
40
+ return res.status(500).json({
41
+ error: 'Authentication error',
42
+ message: err.message
43
+ });
44
+ }
45
+
46
+ if (!user) {
47
+ return res.status(401).json({
48
+ error: 'Unauthorized',
49
+ message: info?.message || 'Invalid or expired token'
50
+ });
51
+ }
52
+
53
+ // req.user에 사용자 정보 주입
54
+ req.user = user;
55
+ next();
56
+ })(req, res, next);
57
+ }
58
+
59
+ /**
60
+ * Refresh Token 인증 미들웨어
61
+ *
62
+ * @example
63
+ * router.get('/refresh', authenticateRefresh, (req, res) => {
64
+ * // Generate new access token
65
+ * res.json({ newAccessToken: '...' });
66
+ * });
67
+ */
68
+ function authenticateRefresh(req, res, next) {
69
+ initializePassport();
70
+
71
+ passport.authenticate('refresh', { session: false }, (err, user, info) => {
72
+ if (err) {
73
+ console.error('[@ahhaohho/auth-middleware] Refresh token error:', err.message);
74
+ return res.status(500).json({
75
+ error: 'Token refresh error',
76
+ message: err.message
77
+ });
78
+ }
79
+
80
+ if (!user) {
81
+ return res.status(401).json({
82
+ error: 'Invalid refresh token',
83
+ message: info?.message || 'Invalid or expired refresh token'
84
+ });
85
+ }
86
+
87
+ req.user = user;
88
+ next();
89
+ })(req, res, next);
90
+ }
91
+
92
+ /**
93
+ * 선택적 인증 미들웨어 (인증 실패해도 통과)
94
+ * req.user가 있으면 인증된 사용자, 없으면 비인증 사용자
95
+ *
96
+ * @example
97
+ * router.get('/public', optionalAuth, (req, res) => {
98
+ * if (req.user) {
99
+ * res.json({ message: 'Authenticated', userId: req.user.userId });
100
+ * } else {
101
+ * res.json({ message: 'Anonymous' });
102
+ * }
103
+ * });
104
+ */
105
+ function optionalAuth(req, res, next) {
106
+ initializePassport();
107
+
108
+ passport.authenticate('jwt', { session: false }, (err, user) => {
109
+ // 에러나 인증 실패해도 통과
110
+ if (user) {
111
+ req.user = user;
112
+ }
113
+ next();
114
+ })(req, res, next);
115
+ }
116
+
117
+ module.exports = {
118
+ authenticateJWT,
119
+ authenticateRefresh,
120
+ optionalAuth
121
+ };
@@ -0,0 +1,96 @@
1
+ const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
2
+ const { getJwtKeys } = require('../utils/secretManager');
3
+ const { isBlacklisted } = require('../utils/blacklist');
4
+ const jwt = require('jsonwebtoken');
5
+
6
+ /**
7
+ * Passport JWT 전략 생성
8
+ * Access Token 검증용
9
+ */
10
+ function createJwtStrategy() {
11
+ const options = {
12
+ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
13
+ // 다중 키 지원을 위한 secretOrKeyProvider 사용
14
+ secretOrKeyProvider: async (request, rawJwtToken, done) => {
15
+ try {
16
+ const keys = await getJwtKeys();
17
+
18
+ // 다중 키 검증 로직
19
+ let decoded;
20
+ let keyUsed;
21
+ let actualKey;
22
+
23
+ // 1. 현재 키로 검증 시도
24
+ try {
25
+ decoded = jwt.verify(rawJwtToken, keys.current);
26
+ keyUsed = 'current';
27
+ actualKey = keys.current;
28
+ } catch (currentKeyError) {
29
+ // 2. 이전 키가 있으면 fallback 검증 시도
30
+ if (keys.previous) {
31
+ try {
32
+ decoded = jwt.verify(rawJwtToken, keys.previous);
33
+ keyUsed = 'previous';
34
+ actualKey = keys.previous;
35
+ console.warn(
36
+ '[@ahhaohho/auth-middleware] ⚠️ Token verified with previous key (fallback)'
37
+ );
38
+ } catch (previousKeyError) {
39
+ return done(currentKeyError, false);
40
+ }
41
+ } else {
42
+ return done(currentKeyError, false);
43
+ }
44
+ }
45
+
46
+ // 검증 성공 시 decoded와 keyUsed를 request에 임시 저장
47
+ request._jwtDecoded = decoded;
48
+ request._jwtKeyUsed = keyUsed;
49
+
50
+ // Passport에게 사용된 실제 키 반환 (이미 검증 완료되었으므로 다시 검증해도 성공)
51
+ done(null, actualKey);
52
+ } catch (error) {
53
+ console.error('[@ahhaohho/auth-middleware] ❌ JWT verification failed:', error.message);
54
+ done(error, false);
55
+ }
56
+ },
57
+ passReqToCallback: true // request 객체를 verify 콜백으로 전달
58
+ };
59
+
60
+ return new JwtStrategy(options, async (request, jwtPayload, done) => {
61
+ try {
62
+ // request에서 미리 검증된 decoded 가져오기
63
+ const decoded = request._jwtDecoded;
64
+ const keyUsed = request._jwtKeyUsed;
65
+
66
+ if (!decoded || !decoded.userId) {
67
+ return done(new Error('Invalid token payload'), false);
68
+ }
69
+
70
+ // 블랙리스트 확인
71
+ const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
72
+ const blacklisted = await isBlacklisted(decoded.userId, 'access', token);
73
+ if (blacklisted) {
74
+ return done(new Error('Token has been revoked'), false);
75
+ }
76
+
77
+ console.log(
78
+ `[@ahhaohho/auth-middleware] ✅ JWT verified with ${keyUsed} key for user ${decoded.userId}`
79
+ );
80
+
81
+ // req.user에 주입할 사용자 정보 반환
82
+ const user = {
83
+ userId: decoded.userId,
84
+ userRole: decoded.userRole,
85
+ phoneNumber: decoded.phoneNumber
86
+ };
87
+
88
+ return done(null, user);
89
+ } catch (error) {
90
+ console.error('[@ahhaohho/auth-middleware] Error in JWT strategy:', error.message);
91
+ return done(error, false);
92
+ }
93
+ });
94
+ }
95
+
96
+ module.exports = createJwtStrategy;
@@ -0,0 +1,71 @@
1
+ const { Strategy: JwtStrategy } = require('passport-jwt');
2
+ const { verifyTokenWithFallback } = require('../utils/jwtValidator');
3
+ const { isBlacklisted } = require('../utils/blacklist');
4
+
5
+ /**
6
+ * Refresh Token 헤더에서 토큰 추출
7
+ */
8
+ function extractRefreshToken(req) {
9
+ let token = null;
10
+
11
+ if (req && req.headers && req.headers['refresh-token']) {
12
+ let refreshToken = req.headers['refresh-token'];
13
+
14
+ // Bearer 접두사 제거
15
+ if (refreshToken.startsWith('Bearer ')) {
16
+ refreshToken = refreshToken.substring(7);
17
+ }
18
+
19
+ token = refreshToken.trim();
20
+ }
21
+
22
+ return token;
23
+ }
24
+
25
+ /**
26
+ * Passport Refresh Token 전략 생성
27
+ */
28
+ function createRefreshStrategy() {
29
+ const options = {
30
+ jwtFromRequest: extractRefreshToken,
31
+ secretOrKeyProvider: async (request, rawJwtToken, done) => {
32
+ try {
33
+ // 1. 다중 키로 토큰 검증
34
+ const { decoded, keyUsed } = await verifyTokenWithFallback(rawJwtToken);
35
+
36
+ if (!decoded || !decoded.userId) {
37
+ return done(new Error('Invalid refresh token payload'), false);
38
+ }
39
+
40
+ // 2. 블랙리스트 확인 (refresh 타입)
41
+ const blacklisted = await isBlacklisted(decoded.userId, 'refresh', rawJwtToken);
42
+ if (blacklisted) {
43
+ return done(new Error('Refresh token has been revoked'), false);
44
+ }
45
+
46
+ // 3. 검증 성공
47
+ console.log(
48
+ `[@ahhaohho/auth-middleware] ✅ Refresh token verified with ${keyUsed} key for user ${decoded.userId}`
49
+ );
50
+
51
+ done(null, decoded);
52
+ } catch (error) {
53
+ console.error('[@ahhaohho/auth-middleware] ❌ Refresh token verification failed:', error.message);
54
+ done(error, false);
55
+ }
56
+ },
57
+ passReqToCallback: false
58
+ };
59
+
60
+ return new JwtStrategy(options, (jwtPayload, done) => {
61
+ const user = {
62
+ userId: jwtPayload.userId,
63
+ userRole: jwtPayload.userRole,
64
+ phoneNumber: jwtPayload.phoneNumber
65
+ };
66
+
67
+ return done(null, user);
68
+ });
69
+ }
70
+
71
+ module.exports = createRefreshStrategy;
@@ -0,0 +1,84 @@
1
+ const redisManager = require('../config/redis');
2
+
3
+ /**
4
+ * Redis 기반 토큰 블랙리스트 관리
5
+ */
6
+
7
+ /**
8
+ * 토큰이 블랙리스트에 있는지 확인
9
+ * @param {string} userId - 사용자 ID
10
+ * @param {string} tokenType - 토큰 타입 ('access' | 'refresh')
11
+ * @param {string} token - JWT 토큰
12
+ * @returns {Promise<boolean>} 블랙리스트에 있으면 true
13
+ */
14
+ async function isBlacklisted(userId, tokenType, token) {
15
+ try {
16
+ const redisClient = redisManager.getClient('token');
17
+ const key = `blacklist:${userId}:${tokenType}`;
18
+
19
+ const tokens = await redisClient.smembers(key);
20
+ return tokens.includes(token);
21
+ } catch (error) {
22
+ console.error('[@ahhaohho/auth-middleware] Error checking blacklist:', error.message);
23
+ // Redis 에러 시 안전하게 false 반환 (통과시킴)
24
+ return false;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * 토큰을 블랙리스트에 추가
30
+ * @param {string} userId - 사용자 ID
31
+ * @param {string} tokenType - 토큰 타입 ('access' | 'refresh')
32
+ * @param {string} token - JWT 토큰
33
+ * @param {number} ttl - TTL (초 단위, 기본값: 토큰 만료 시간)
34
+ * @returns {Promise<void>}
35
+ */
36
+ async function addToBlacklist(userId, tokenType, token, ttl = null) {
37
+ try {
38
+ const redisClient = redisManager.getClient('token');
39
+ const key = `blacklist:${userId}:${tokenType}`;
40
+
41
+ await redisClient.sadd(key, token);
42
+
43
+ // TTL 설정
44
+ if (ttl) {
45
+ await redisClient.expire(key, ttl);
46
+ } else {
47
+ // 기본 TTL: Access Token 1시간, Refresh Token 90일
48
+ const defaultTtl = tokenType === 'access' ? 60 * 60 : 90 * 24 * 60 * 60;
49
+ await redisClient.expire(key, defaultTtl);
50
+ }
51
+
52
+ console.log(`[@ahhaohho/auth-middleware] Token added to blacklist: ${userId}:${tokenType}`);
53
+ } catch (error) {
54
+ console.error('[@ahhaohho/auth-middleware] Error adding to blacklist:', error.message);
55
+ throw error;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * 사용자의 모든 토큰을 블랙리스트에서 제거
61
+ * @param {string} userId - 사용자 ID
62
+ * @returns {Promise<void>}
63
+ */
64
+ async function clearBlacklist(userId) {
65
+ try {
66
+ const redisClient = redisManager.getClient('token');
67
+
68
+ await Promise.all([
69
+ redisClient.del(`blacklist:${userId}:access`),
70
+ redisClient.del(`blacklist:${userId}:refresh`)
71
+ ]);
72
+
73
+ console.log(`[@ahhaohho/auth-middleware] Blacklist cleared for user: ${userId}`);
74
+ } catch (error) {
75
+ console.error('[@ahhaohho/auth-middleware] Error clearing blacklist:', error.message);
76
+ throw error;
77
+ }
78
+ }
79
+
80
+ module.exports = {
81
+ isBlacklisted,
82
+ addToBlacklist,
83
+ clearBlacklist
84
+ };
@@ -0,0 +1,68 @@
1
+ const jwt = require('jsonwebtoken');
2
+ const { getJwtKeys } = require('./secretManager');
3
+
4
+ /**
5
+ * 다중 키를 사용한 JWT 토큰 검증
6
+ * 현재 키로 검증 실패 시 이전 키로 fallback 검증
7
+ *
8
+ * @param {string} token - JWT 토큰
9
+ * @returns {Promise<{decoded: object, keyUsed: string}>}
10
+ */
11
+ async function verifyTokenWithFallback(token) {
12
+ const keys = await getJwtKeys();
13
+
14
+ // 1. 현재 키로 검증 시도
15
+ try {
16
+ const decoded = jwt.verify(token, keys.current);
17
+ return { decoded, keyUsed: 'current' };
18
+ } catch (currentKeyError) {
19
+ // 2. 이전 키가 있으면 fallback 검증 시도
20
+ if (keys.previous) {
21
+ try {
22
+ const decoded = jwt.verify(token, keys.previous);
23
+ console.warn(
24
+ '[@ahhaohho/auth-middleware] ⚠️ Token verified with previous key (fallback) for userId:',
25
+ decoded.userId
26
+ );
27
+ return { decoded, keyUsed: 'previous' };
28
+ } catch (previousKeyError) {
29
+ // 두 키 모두 실패한 경우 현재 키의 에러를 던짐 (더 중요한 에러)
30
+ throw currentKeyError;
31
+ }
32
+ } else {
33
+ // 이전 키가 없으면 현재 키 에러를 그대로 던짐
34
+ throw currentKeyError;
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * 토큰 서명을 위한 현재 키 가져오기
41
+ * @returns {Promise<string>}
42
+ */
43
+ async function getCurrentSigningKey() {
44
+ const keys = await getJwtKeys();
45
+ return keys.current;
46
+ }
47
+
48
+ /**
49
+ * JWT 토큰 생성 (항상 현재 키 사용)
50
+ * @param {object} payload - JWT payload
51
+ * @param {object} options - JWT options (expiresIn 등)
52
+ * @returns {Promise<string>} JWT 토큰
53
+ */
54
+ async function signToken(payload, options = {}) {
55
+ const signingKey = await getCurrentSigningKey();
56
+
57
+ if (!signingKey) {
58
+ throw new Error('JWT signing key is not available');
59
+ }
60
+
61
+ return jwt.sign(payload, signingKey, options);
62
+ }
63
+
64
+ module.exports = {
65
+ verifyTokenWithFallback,
66
+ getCurrentSigningKey,
67
+ signToken
68
+ };
@@ -0,0 +1,79 @@
1
+ const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');
2
+ const redisManager = require('../config/redis');
3
+
4
+ /**
5
+ * AWS Secrets Manager에서 JWT 키 가져오기
6
+ */
7
+ class SecretManager {
8
+ constructor() {
9
+ this.client = new SecretsManagerClient({
10
+ region: process.env.AWS_REGION || 'ap-northeast-2'
11
+ });
12
+ this.secretName = process.env.JWT_SECRET_NAME;
13
+ }
14
+
15
+ /**
16
+ * JWT 키 가져오기 (Redis 캐싱 포함)
17
+ * @returns {Promise<{current: string, previous: string|null}>}
18
+ */
19
+ async getJwtKeys() {
20
+ if (!this.secretName) {
21
+ throw new Error('JWT_SECRET_NAME environment variable is not set');
22
+ }
23
+
24
+ try {
25
+ // 1. Redis 캐시 확인
26
+ const redisClient = redisManager.getClient('keys');
27
+ const cachedKeys = await redisClient.get(`jwt-keys:${this.secretName}`);
28
+
29
+ if (cachedKeys) {
30
+ console.log('[@ahhaohho/auth-middleware] Using cached JWT keys from Redis');
31
+ return JSON.parse(cachedKeys);
32
+ }
33
+
34
+ // 2. AWS Secrets Manager에서 가져오기
35
+ console.log('[@ahhaohho/auth-middleware] Fetching JWT keys from AWS Secrets Manager');
36
+ const command = new GetSecretValueCommand({ SecretId: this.secretName });
37
+ const response = await this.client.send(command);
38
+
39
+ if (!response.SecretString) {
40
+ throw new Error('Secret value is empty');
41
+ }
42
+
43
+ const secret = JSON.parse(response.SecretString);
44
+ const keys = {
45
+ current: secret.current || secret.jwt_secret_key || secret.dev,
46
+ previous: secret.previous || null
47
+ };
48
+
49
+ // 3. Redis에 캐싱 (5분 TTL)
50
+ await redisClient.set(
51
+ `jwt-keys:${this.secretName}`,
52
+ JSON.stringify(keys),
53
+ 'EX',
54
+ 300 // 5분
55
+ );
56
+
57
+ return keys;
58
+ } catch (error) {
59
+ console.error('[@ahhaohho/auth-middleware] Error fetching JWT keys:', error.message);
60
+ throw new Error('Failed to fetch JWT keys from AWS Secrets Manager');
61
+ }
62
+ }
63
+
64
+ /**
65
+ * 캐시 무효화
66
+ */
67
+ async invalidateCache() {
68
+ const redisClient = redisManager.getClient('keys');
69
+ await redisClient.del(`jwt-keys:${this.secretName}`);
70
+ console.log('[@ahhaohho/auth-middleware] JWT keys cache invalidated');
71
+ }
72
+ }
73
+
74
+ const secretManager = new SecretManager();
75
+
76
+ module.exports = {
77
+ getJwtKeys: () => secretManager.getJwtKeys(),
78
+ invalidateCache: () => secretManager.invalidateCache()
79
+ };