propro-utils 1.3.8
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/.github/workflows/.deploy +31 -0
- package/.idea/modules.xml +8 -0
- package/.idea/propro-auth-middleware.iml +12 -0
- package/.idea/vcs.xml +6 -0
- package/README.md +89 -0
- package/dist/NotificationService.js +64 -0
- package/dist/baseStrategy.js +44 -0
- package/dist/emailStrategy.js +79 -0
- package/dist/index.js +16 -0
- package/dist/mailjetStrategy.js +71 -0
- package/dist/template.data.js +298 -0
- package/dist/testemail.js +12 -0
- package/dist/twilloStrategy.js +69 -0
- package/notify/NotifLocal.js +81 -0
- package/notify/NotifLocal.jsx +89 -0
- package/notify/NotificationService.js +31 -0
- package/notify/README.md +110 -0
- package/notify/baseStrategy.js +13 -0
- package/notify/emailStrategy.js +57 -0
- package/notify/index.js +11 -0
- package/notify/mailjetStrategy.js +27 -0
- package/notify/template.data.js +292 -0
- package/notify/testemail.js +15 -0
- package/notify/twilloStrategy.js +30 -0
- package/package.json +69 -0
- package/src/.env.example +5 -0
- package/src/client/index.js +24 -0
- package/src/index.js +68 -0
- package/src/server/index.js +108 -0
- package/src/server/middleware/index.js +3 -0
- package/src/server/middleware/oauth.test.js +203 -0
- package/src/server/middleware/refreshToken.js +82 -0
- package/src/server/middleware/refreshToken.test.js +121 -0
- package/src/server/middleware/validateEnv.js +12 -0
- package/src/server/middleware/verifyToken.js +119 -0
- package/src/server/middleware/verifyToken.test.js +171 -0
- package/src/server/server.test.js +37 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const {
|
|
2
|
+
validateEnvironmentVariables,
|
|
3
|
+
} = require("./server/middleware/validateEnv");
|
|
4
|
+
let _serverAuth, _clientAuth;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Middleware for handling both server-side and client-side authentication.
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} options - Configuration options for the middleware.
|
|
10
|
+
* @param {boolean} [options.useServerAuth=true] - A boolean flag to enable server-side authentication.
|
|
11
|
+
* @param {Object} [options.serverOptions={}] - Configuration options for server-side authentication.
|
|
12
|
+
* Example:
|
|
13
|
+
* {
|
|
14
|
+
* jwtSecret: 'HubHubJWTSecret', // Secret key for JWT token verification
|
|
15
|
+
* tokenExpiry: 3600, // Token expiry time in seconds
|
|
16
|
+
* authUrl: 'https://propro.so/auth', // URL of the authentication server
|
|
17
|
+
* clientId: 'propro', // Client ID
|
|
18
|
+
* clientSecret: 'propro', // Client secret
|
|
19
|
+
* clientUrl: 'https://propro.so', // URL of the authentication client
|
|
20
|
+
* redirectUri: 'https://propro.so/callback', // Redirect URI
|
|
21
|
+
* validateUser: async (userId) => { }, // Function to validate user
|
|
22
|
+
* onAuthFailRedirect: '/login', // URL to redirect on authentication failure
|
|
23
|
+
* additionalChecks: async (req) => { }, // Additional custom checks for requests
|
|
24
|
+
* }
|
|
25
|
+
* @param {boolean} [options.useClientAuth=false] - A boolean flag to enable client-side authentication.
|
|
26
|
+
* @param {Object} [options.clientOptions={}] - Configuration options for client-side authentication.
|
|
27
|
+
*
|
|
28
|
+
* @returns {Function} An Express middleware function.
|
|
29
|
+
*
|
|
30
|
+
* Example usage:
|
|
31
|
+
* app.use(proproAuthMiddleware({
|
|
32
|
+
* useServerAuth: true,
|
|
33
|
+
* serverOptions: {
|
|
34
|
+
* jwtSecret: 'HubHubJWTSecret',
|
|
35
|
+
* tokenExpiry: 3600,
|
|
36
|
+
* validateUser: async (userId) => { },
|
|
37
|
+
* onAuthFailRedirect: '/login',
|
|
38
|
+
* additionalChecks: async (req) => { },
|
|
39
|
+
* },
|
|
40
|
+
* useClientAuth: false,
|
|
41
|
+
* }));
|
|
42
|
+
*/
|
|
43
|
+
module.exports = function proproAuthMiddleware(options = {}) {
|
|
44
|
+
validateEnvironmentVariables([
|
|
45
|
+
"AUTH_URL",
|
|
46
|
+
"CLIENT_ID",
|
|
47
|
+
"CLIENT_SECRET",
|
|
48
|
+
"CLIENT_URL",
|
|
49
|
+
"REDIRECT_URI",
|
|
50
|
+
]);
|
|
51
|
+
return (req, res, next) => {
|
|
52
|
+
try {
|
|
53
|
+
// Lazy loading and initializing server and client authentication modules with options
|
|
54
|
+
if (options.useServerAuth) {
|
|
55
|
+
_serverAuth = _serverAuth || require("./server")(options.serverOptions);
|
|
56
|
+
_serverAuth(req, res, next);
|
|
57
|
+
} else if (options.useClientAuth) {
|
|
58
|
+
_clientAuth = _clientAuth || require("./client")(options.clientOptions);
|
|
59
|
+
_clientAuth(req, res, next);
|
|
60
|
+
} else {
|
|
61
|
+
next();
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error("Error in authentication middleware:", error);
|
|
65
|
+
next(error);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
require("dotenv").config();
|
|
2
|
+
const {
|
|
3
|
+
verifyJWT,
|
|
4
|
+
exchangeToken,
|
|
5
|
+
VerifyAccount,
|
|
6
|
+
} = require("./middleware/verifyToken");
|
|
7
|
+
// Main middleware function
|
|
8
|
+
function proproAuthMiddleware(options = {}) {
|
|
9
|
+
const {
|
|
10
|
+
secret = "RESTFULAPIs",
|
|
11
|
+
authUrl = process.env.AUTH_URL,
|
|
12
|
+
clientId = process.env.CLIENT_ID,
|
|
13
|
+
clientSecret = process.env.CLIENT_SECRET,
|
|
14
|
+
clientUrl = process.env.CLIENT_URL,
|
|
15
|
+
redirectUri = process.env.REDIRECT_URI,
|
|
16
|
+
appName = process.env.APP_NAME,
|
|
17
|
+
} = options;
|
|
18
|
+
|
|
19
|
+
return async (req, res, next) => {
|
|
20
|
+
try {
|
|
21
|
+
// Try to get the token from the Authorization header
|
|
22
|
+
let token;
|
|
23
|
+
if (req.headers.authorization?.startsWith("Bearer ")) {
|
|
24
|
+
token = req.headers.authorization.split(" ")[1];
|
|
25
|
+
} else {
|
|
26
|
+
// If not present in Authorization header, try to get it from cookies
|
|
27
|
+
token = req.cookies['x-access-token'];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (token) {
|
|
31
|
+
const verifiedToken = verifyJWT(token, secret);
|
|
32
|
+
if (verifiedToken) {
|
|
33
|
+
req.account = verifiedToken;
|
|
34
|
+
} else {
|
|
35
|
+
// If token is invalid or expired, try to refresh it
|
|
36
|
+
const refreshToken = req.cookies['x-refresh-token'];
|
|
37
|
+
if (refreshToken) {
|
|
38
|
+
const newTokenData = await refreshToken(authUrl, refreshToken, clientId, clientSecret);
|
|
39
|
+
if (newTokenData) {
|
|
40
|
+
// Set the new tokens in the cookies
|
|
41
|
+
res.cookie("x-refresh-token", newTokenData.tokens.refresh.token, {
|
|
42
|
+
httpOnly: true,
|
|
43
|
+
secure: process.env.NODE_ENV === "production",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
res.cookie("x-access-token", newTokenData.tokens.access.token, {
|
|
47
|
+
httpOnly: true,
|
|
48
|
+
secure: process.env.NODE_ENV === "production",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
req.account = verifyJWT(newTokenData.tokens.access.token, secret);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
req.account = undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!["/api/auth", "/api/callback"].includes(req.path)) {
|
|
60
|
+
return next();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (req.path === "/api/auth") {
|
|
64
|
+
const authClientUrl = `${clientUrl}/signin`;
|
|
65
|
+
const redirectUrl = `${authClientUrl}?response_type=code&appName=${appName}&client_id=${clientId}&redirect_uri=${encodeURIComponent(
|
|
66
|
+
redirectUri
|
|
67
|
+
)}`;
|
|
68
|
+
res.status(200).json({ redirectUrl });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (req.path === "/api/callback") {
|
|
72
|
+
const code = req.query.code;
|
|
73
|
+
if (!code) {
|
|
74
|
+
return res.status(400).send("No code received");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log("code", code);
|
|
78
|
+
|
|
79
|
+
const tokenData = await exchangeToken(
|
|
80
|
+
authUrl,
|
|
81
|
+
code,
|
|
82
|
+
clientId,
|
|
83
|
+
clientSecret,
|
|
84
|
+
redirectUri
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
res.cookie("x-refresh-token", tokenData.tokens.refresh.token, {
|
|
88
|
+
httpOnly: true,
|
|
89
|
+
secure: process.env.NODE_ENV === "production",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
res.cookie("x-access-token", tokenData.tokens.access.token, {
|
|
93
|
+
httpOnly: true,
|
|
94
|
+
secure: process.env.NODE_ENV === "production",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const redirectUrl = `http://${tokenData.redirectUrl}/`;
|
|
98
|
+
|
|
99
|
+
return res.redirect(redirectUrl);
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error("Error in proproAuthMiddleware:", error);
|
|
103
|
+
res.status(500).send("Internal Server Error");
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = proproAuthMiddleware;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
describe('Test Middleware', () => {
|
|
2
|
+
// Middleware correctly verifies JWT token and sets user in request object
|
|
3
|
+
const jwt = require('jsonwebtoken');
|
|
4
|
+
const middleware = require('../middleware');
|
|
5
|
+
|
|
6
|
+
jest.mock('jsonwebtoken');
|
|
7
|
+
|
|
8
|
+
describe('Set user if the jwt is valid', () => {
|
|
9
|
+
it('should set user in request object when JWT token is valid', () => {
|
|
10
|
+
const req = {
|
|
11
|
+
headers: {
|
|
12
|
+
authorization: 'JWT validToken',
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
const res = {};
|
|
16
|
+
const next = jest.fn();
|
|
17
|
+
const decode = { username: 'testUser' };
|
|
18
|
+
jwt.verify.mockImplementation((token, secret, callback) => {
|
|
19
|
+
callback(null, decode);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
middleware(req, res, next);
|
|
23
|
+
|
|
24
|
+
expect(req.user).toEqual(decode);
|
|
25
|
+
expect(next).toHaveBeenCalled();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// GET request to '/test-refresh-token' returns JSON with message and access token
|
|
30
|
+
const request = require('supertest');
|
|
31
|
+
const app = require('..');
|
|
32
|
+
|
|
33
|
+
describe('return a message and an access token', () => {
|
|
34
|
+
it('should return JSON with message and access token', async () => {
|
|
35
|
+
const response = await request(app)
|
|
36
|
+
.get('/test-refresh-token')
|
|
37
|
+
.set('Authorization', 'JWT validToken');
|
|
38
|
+
|
|
39
|
+
expect(response.status).toBe(200);
|
|
40
|
+
expect(response.body.message).toBe('Access granted with a new token');
|
|
41
|
+
expect(response.body.accessToken).toBeDefined();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// GET request to '/auth' redirects to authorization server URL with correct parameters
|
|
46
|
+
const request = require('supertest');
|
|
47
|
+
const app = require('..');
|
|
48
|
+
|
|
49
|
+
describe('redirect with correct parameter', () => {
|
|
50
|
+
it('should redirect to authorization server URL with correct parameters', async () => {
|
|
51
|
+
process.env.AUTH_URL = 'http://auth-server.com';
|
|
52
|
+
process.env.CLIENT_ID = 'testClientId';
|
|
53
|
+
process.env.REDIRECT_URI = 'http://client.com/callback';
|
|
54
|
+
|
|
55
|
+
const response = await request(app).get('/auth');
|
|
56
|
+
|
|
57
|
+
expect(response.status).toBe(302);
|
|
58
|
+
expect(response.header.location).toBe(
|
|
59
|
+
'http://auth-server.com/oauth/authorize?response_type=code&client_id=testClientId&redirect_uri=http%3A%2F%2Fclient.com%2Fcallback',
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Authorization header is missing or invalid
|
|
65
|
+
const middleware = require('../middleware');
|
|
66
|
+
|
|
67
|
+
describe('Set user to undefined on missing header', () => {
|
|
68
|
+
it('should set user to undefined when authorization header is missing or invalid', () => {
|
|
69
|
+
const req = {
|
|
70
|
+
headers: {},
|
|
71
|
+
};
|
|
72
|
+
const res = {};
|
|
73
|
+
const next = jest.fn();
|
|
74
|
+
|
|
75
|
+
middleware(req, res, next);
|
|
76
|
+
|
|
77
|
+
expect(req.user).toBeUndefined();
|
|
78
|
+
expect(next).toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// No authorization code received in '/callback' endpoint
|
|
83
|
+
const request = require('supertest');
|
|
84
|
+
const app = require('..');
|
|
85
|
+
|
|
86
|
+
describe('Error when excchange code is received', () => {
|
|
87
|
+
it('should return 400 when no authorization code is received', async () => {
|
|
88
|
+
const response = await request(app).get('/callback');
|
|
89
|
+
|
|
90
|
+
expect(response.status).toBe(400);
|
|
91
|
+
expect(response.text).toBe('No code received');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Error occurs during token exchange in '/callback' endpoint
|
|
96
|
+
const request = require('supertest');
|
|
97
|
+
const axios = require('axios');
|
|
98
|
+
const app = require('..');
|
|
99
|
+
|
|
100
|
+
jest.mock('axios');
|
|
101
|
+
|
|
102
|
+
describe('Error during token exchange', () => {
|
|
103
|
+
it('should return 500 when error occurs during token exchange', async () => {
|
|
104
|
+
const req = {
|
|
105
|
+
query: {
|
|
106
|
+
code: 'validCode',
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
const res = {};
|
|
110
|
+
axios.post.mockRejectedValue(new Error('Test error'));
|
|
111
|
+
|
|
112
|
+
const response = await request(app).get('/callback').query(req.query);
|
|
113
|
+
|
|
114
|
+
expect(response.status).toBe(500);
|
|
115
|
+
expect(response.text).toBe('Internal Server Error');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// GET request to '/callback' exchanges authorization code for access token and sets cookie
|
|
120
|
+
it('should exchange authorization code for access token and set cookie', async () => {
|
|
121
|
+
const req = {
|
|
122
|
+
query: {
|
|
123
|
+
code: 'authorizationCode',
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
const res = {
|
|
127
|
+
status: jest.fn().mockReturnThis(),
|
|
128
|
+
send: jest.fn(),
|
|
129
|
+
redirect: jest.fn(),
|
|
130
|
+
cookie: jest.fn(),
|
|
131
|
+
};
|
|
132
|
+
const tokenResponse = {
|
|
133
|
+
data: {
|
|
134
|
+
access_token: 'accessToken',
|
|
135
|
+
refresh_token: 'refreshToken',
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
axios.post.mockResolvedValue(tokenResponse);
|
|
139
|
+
|
|
140
|
+
await callback(req, res);
|
|
141
|
+
|
|
142
|
+
expect(axios.post).toHaveBeenCalledWith(
|
|
143
|
+
`${process.env.AUTH_URL}/oauth/token`,
|
|
144
|
+
qs.stringify({
|
|
145
|
+
grant_type: 'authorization_code',
|
|
146
|
+
code: req.query.code,
|
|
147
|
+
redirect_uri: process.env.REDIRECT_URI,
|
|
148
|
+
client_id: process.env.CLIENT_ID,
|
|
149
|
+
client_secret: process.env.CLIENT_SECRET,
|
|
150
|
+
}),
|
|
151
|
+
{
|
|
152
|
+
headers: {
|
|
153
|
+
'Content-Type': 'application/json',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
expect(res.cookie).toHaveBeenCalledWith(
|
|
158
|
+
'token',
|
|
159
|
+
tokenResponse.data.access_token,
|
|
160
|
+
{
|
|
161
|
+
httpOnly: true,
|
|
162
|
+
secure: process.env.NODE_ENV === 'production',
|
|
163
|
+
},
|
|
164
|
+
);
|
|
165
|
+
expect(res.redirect).toHaveBeenCalledWith(
|
|
166
|
+
`${process.env.CLIENT_URL}/?access_token=${tokenResponse.data.access_token}&refresh_token=${tokenResponse.data.refresh_token}`,
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Unauthorized user receives 401 status code
|
|
171
|
+
it('should return 401 status code for unauthorized user', () => {
|
|
172
|
+
const req = {};
|
|
173
|
+
const res = {
|
|
174
|
+
status: jest.fn().mockReturnThis(),
|
|
175
|
+
send: jest.fn(),
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
middleware(req, res);
|
|
179
|
+
|
|
180
|
+
expect(res.status).toHaveBeenCalledWith(401);
|
|
181
|
+
expect(res.send).toHaveBeenCalledWith('Unauthorized');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Invalid redirect URI returns error in '/auth' endpoint
|
|
185
|
+
it('should return error for invalid redirect URI', () => {
|
|
186
|
+
const req = {};
|
|
187
|
+
const res = {
|
|
188
|
+
status: jest.fn().mockReturnThis(),
|
|
189
|
+
send: jest.fn(),
|
|
190
|
+
};
|
|
191
|
+
const authServerUrl = `${process.env.AUTH_URL}/oauth/authorize`;
|
|
192
|
+
const clientId = process.env.CLIENT_ID;
|
|
193
|
+
const redirectUri = process.env.REDIRECT_URI;
|
|
194
|
+
|
|
195
|
+
auth(req, res);
|
|
196
|
+
|
|
197
|
+
expect(res.redirect).toHaveBeenCalledWith(
|
|
198
|
+
`${authServerUrl}?response_type=code&client_id=${clientId}&redirect_uri=${encodeURIComponent(
|
|
199
|
+
redirectUri,
|
|
200
|
+
)}`,
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const axios = require("axios");
|
|
2
|
+
const rateLimit = require("express-rate-limit");
|
|
3
|
+
const refreshTokenCache = new Map();
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Rate limiter middleware for refresh requests.
|
|
7
|
+
*
|
|
8
|
+
* @type {import('express-rate-limit').RateLimit}
|
|
9
|
+
*/
|
|
10
|
+
const refreshLimiter = rateLimit({
|
|
11
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
12
|
+
max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
|
|
13
|
+
message:
|
|
14
|
+
"Too many refresh requests from this IP, please try again after 15 minutes",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Middleware function to refresh access token using refresh token.
|
|
19
|
+
* @async
|
|
20
|
+
* @function
|
|
21
|
+
* @param {Object} req - Express request object.
|
|
22
|
+
* @param {Object} res - Express response object.
|
|
23
|
+
* @param {Function} next - Express next middleware function.
|
|
24
|
+
* @returns {Promise<void>} - Promise object that represents the completion of the middleware function.
|
|
25
|
+
*/
|
|
26
|
+
const refreshTokenMiddleware = async (req, res, next) => {
|
|
27
|
+
// Apply rate limiting
|
|
28
|
+
refreshLimiter(req, res, async () => {
|
|
29
|
+
const refreshToken = req.headers["x-refresh-token"];
|
|
30
|
+
|
|
31
|
+
if (!refreshToken) {
|
|
32
|
+
return res.status(401).json({ error: "No refresh token provided" });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!isValidRefreshTokenFormat(refreshToken)) {
|
|
36
|
+
return res.status(400).json({ error: "Invalid refresh token format" });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (refreshTokenCache.has(refreshToken)) {
|
|
40
|
+
req.newAccessToken = refreshTokenCache.get(refreshToken);
|
|
41
|
+
return next();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const response = await axios.post(
|
|
46
|
+
`${process.env.AUTH_URL}/oauth/token`,
|
|
47
|
+
URLSearchParams({
|
|
48
|
+
grant_type: "refresh_token",
|
|
49
|
+
refresh_token: refreshToken,
|
|
50
|
+
client_id: process.env.CLIENT_ID,
|
|
51
|
+
client_secret: process.env.CLIENT_SECRET,
|
|
52
|
+
}),
|
|
53
|
+
{ headers: { "Content-Type": "application/x-www-form-urlencoded" } }
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (response.data && response.data.access_token) {
|
|
57
|
+
refreshTokenCache.set(refreshToken, response.data.access_token);
|
|
58
|
+
req.newAccessToken = response.data.access_token;
|
|
59
|
+
return next();
|
|
60
|
+
} else {
|
|
61
|
+
return res.status(401).json({ error: "Unable to refresh token" });
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const statusCode = error.response?.status || 500;
|
|
65
|
+
const message = error.response?.data?.error || "Error refreshing token";
|
|
66
|
+
return res.status(statusCode).json({ error: message });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Checks if the given token is in a valid JWT format.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} token - The token to validate.
|
|
75
|
+
* @returns {boolean} - True if the token is in a valid format, false otherwise.
|
|
76
|
+
*/
|
|
77
|
+
function isValidRefreshTokenFormat(token) {
|
|
78
|
+
const jwtPattern = /^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/;
|
|
79
|
+
return jwtPattern.test(token);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = refreshTokenMiddleware;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const rateLimit = require("express-rate-limit");
|
|
2
|
+
const refreshToken = require("./refreshToken");
|
|
3
|
+
|
|
4
|
+
jest.mock("express-rate-limit");
|
|
5
|
+
|
|
6
|
+
describe("refreshLimiter", () => {
|
|
7
|
+
it("should be a rate limiter middleware", () => {
|
|
8
|
+
expect(rateLimit).toHaveBeenCalledWith({
|
|
9
|
+
windowMs: 15 * 60 * 1000,
|
|
10
|
+
max: 100,
|
|
11
|
+
message:
|
|
12
|
+
"Too many refresh requests from this IP, please try again after 15 minutes",
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should be exported as a middleware function", () => {
|
|
17
|
+
expect(typeof refreshToken).toBe("function");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should use the rate limiter middleware", () => {
|
|
21
|
+
const mockUse = jest.fn();
|
|
22
|
+
const app = { use: mockUse };
|
|
23
|
+
|
|
24
|
+
refreshToken(app);
|
|
25
|
+
|
|
26
|
+
expect(mockUse).toHaveBeenCalledWith(rateLimit());
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("refreshTokenMiddleware", () => {
|
|
31
|
+
const mockRequest = (refreshToken) => ({
|
|
32
|
+
headers: { "x-refresh-token": refreshToken },
|
|
33
|
+
});
|
|
34
|
+
const mockResponse = () => {
|
|
35
|
+
const res = {};
|
|
36
|
+
res.status = jest.fn().mockReturnValue(res);
|
|
37
|
+
res.json = jest.fn().mockReturnValue(res);
|
|
38
|
+
return res;
|
|
39
|
+
};
|
|
40
|
+
const mockNext = jest.fn();
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
jest.clearAllMocks();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should return 401 if no refresh token is provided", async () => {
|
|
47
|
+
const req = mockRequest();
|
|
48
|
+
const res = mockResponse();
|
|
49
|
+
|
|
50
|
+
await refreshTokenMiddleware(req, res, mockNext);
|
|
51
|
+
|
|
52
|
+
expect(res.status).toHaveBeenCalledWith(401);
|
|
53
|
+
expect(res.json).toHaveBeenCalledWith({
|
|
54
|
+
error: "No refresh token provided",
|
|
55
|
+
});
|
|
56
|
+
expect(mockNext).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should return 400 if refresh token has invalid format", async () => {
|
|
60
|
+
const req = mockRequest("invalid-token");
|
|
61
|
+
const res = mockResponse();
|
|
62
|
+
|
|
63
|
+
await refreshTokenMiddleware(req, res, mockNext);
|
|
64
|
+
|
|
65
|
+
expect(res.status).toHaveBeenCalledWith(400);
|
|
66
|
+
expect(res.json).toHaveBeenCalledWith({
|
|
67
|
+
error: "Invalid refresh token format",
|
|
68
|
+
});
|
|
69
|
+
expect(mockNext).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should return 401 if unable to refresh token", async () => {
|
|
73
|
+
const req = mockRequest("valid-token");
|
|
74
|
+
const res = mockResponse();
|
|
75
|
+
axios.post.mockRejectedValue({ response: { status: 401 } });
|
|
76
|
+
|
|
77
|
+
await refreshTokenMiddleware(req, res, mockNext);
|
|
78
|
+
|
|
79
|
+
expect(res.status).toHaveBeenCalledWith(401);
|
|
80
|
+
expect(res.json).toHaveBeenCalledWith({ error: "Unable to refresh token" });
|
|
81
|
+
expect(mockNext).not.toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should return 500 if an error occurs while refreshing token", async () => {
|
|
85
|
+
const req = mockRequest("valid-token");
|
|
86
|
+
const res = mockResponse();
|
|
87
|
+
axios.post.mockRejectedValue(new Error("Some error"));
|
|
88
|
+
|
|
89
|
+
await refreshTokenMiddleware(req, res, mockNext);
|
|
90
|
+
|
|
91
|
+
expect(res.status).toHaveBeenCalledWith(500);
|
|
92
|
+
expect(res.json).toHaveBeenCalledWith({ error: "Error refreshing token" });
|
|
93
|
+
expect(mockNext).not.toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should set new access token and call next if refresh token is valid", async () => {
|
|
97
|
+
const req = mockRequest("valid-token");
|
|
98
|
+
const res = mockResponse();
|
|
99
|
+
axios.post.mockResolvedValue({
|
|
100
|
+
data: { access_token: "new-access-token" },
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await refreshTokenMiddleware(req, res, mockNext);
|
|
104
|
+
|
|
105
|
+
expect(refreshTokenCache.has("valid-token")).toBe(true);
|
|
106
|
+
expect(refreshTokenCache.get("valid-token")).toBe("new-access-token");
|
|
107
|
+
expect(req.newAccessToken).toBe("new-access-token");
|
|
108
|
+
expect(mockNext).toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should use cached access token and call next if refresh token is already cached", async () => {
|
|
112
|
+
refreshTokenCache.set("valid-token", "cached-access-token");
|
|
113
|
+
const req = mockRequest("valid-token");
|
|
114
|
+
const res = mockResponse();
|
|
115
|
+
|
|
116
|
+
await refreshTokenMiddleware(req, res, mockNext);
|
|
117
|
+
|
|
118
|
+
expect(req.newAccessToken).toBe("cached-access-token");
|
|
119
|
+
expect(mockNext).toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
function validateEnvironmentVariables(requiredVars) {
|
|
2
|
+
const missingVars = requiredVars.filter((varName) => !process.env[varName]);
|
|
3
|
+
if (missingVars.length > 0) {
|
|
4
|
+
throw new Error(
|
|
5
|
+
`Missing required environment variables: ${missingVars.join(", ")}`
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
validateEnvironmentVariables,
|
|
12
|
+
};
|