101 lines
3.8 KiB
JavaScript
101 lines
3.8 KiB
JavaScript
(async () => {
|
|
try {
|
|
// 1. Validate required kme_CSA_settings fields
|
|
const requiredFields = ['tokenUrl', 'username', 'password', 'clientId', 'scope'];
|
|
for (const field of requiredFields) {
|
|
if (!kme_CSA_settings[field]) {
|
|
throw new Error('missing required field: ' + field);
|
|
}
|
|
}
|
|
|
|
const { tokenUrl, username, clientId, scope } = kme_CSA_settings;
|
|
|
|
// 2. Read token cache from Redis
|
|
console.debug({ message: 'Checking token cache', url: req.url, method: req.method });
|
|
const token = await redis.hGet('authorization', 'token');
|
|
const expiry = parseFloat(await redis.hGet('authorization', 'expiry') ?? '0');
|
|
const isValid = token !== null && Date.now() / 1000 < expiry;
|
|
|
|
// 3. Cache HIT → respond immediately
|
|
if (isValid) {
|
|
console.debug({ message: 'Token cache hit', expiresIn: Math.round(expiry - Date.now() / 1000) + 's' });
|
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
res.end('Authorized');
|
|
return;
|
|
}
|
|
|
|
// 4. Stampede guard — if a fetch is already in flight, queue on it
|
|
if (kme_CSA_settings._pendingFetch && typeof kme_CSA_settings._pendingFetch.then === 'function') {
|
|
console.debug({ message: 'Token fetch in flight, queuing request' });
|
|
await kme_CSA_settings._pendingFetch;
|
|
console.debug({ message: 'Queued request unblocked, responding' });
|
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
res.end('Authorized');
|
|
return;
|
|
}
|
|
|
|
// 5. Cache MISS → fetch fresh token
|
|
console.info({ message: 'Token cache miss, fetching fresh token', tokenUrl });
|
|
const params = new URLSearchParams({
|
|
grant_type: 'password',
|
|
username,
|
|
password: kme_CSA_settings.password,
|
|
client_id: clientId,
|
|
scope,
|
|
});
|
|
|
|
// Set up stampede guard before fetching
|
|
let resolvePending;
|
|
let rejectPending;
|
|
kme_CSA_settings._pendingFetch = new Promise((resolve, reject) => {
|
|
resolvePending = resolve;
|
|
rejectPending = reject;
|
|
});
|
|
// Prevent an unhandled-rejection when no concurrent request is waiting on this promise
|
|
kme_CSA_settings._pendingFetch.catch(() => {});
|
|
|
|
try {
|
|
console.debug({ message: 'Requesting new token', url: tokenUrl, method: 'POST' });
|
|
const response = await axios.post(tokenUrl, params, {
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
timeout: 5000,
|
|
});
|
|
|
|
const { id_token, expires_in } = response.data;
|
|
if (!id_token) throw new Error('id_token missing from response');
|
|
if (!expires_in) throw new Error('expires_in missing from response');
|
|
|
|
// 6. Write to Redis cache
|
|
await redis.hSet('authorization', 'token', id_token);
|
|
await redis.hSet('authorization', 'expiry', String(expires_in));
|
|
console.info({ message: 'Token fetched and cached', expiresAt: new Date(expires_in * 1000).toISOString() });
|
|
|
|
// Resolve the pending fetch promise so waiting requests can proceed
|
|
resolvePending();
|
|
} catch (fetchErr) {
|
|
console.error({ message: 'Token fetch failed', error: fetchErr.message, code: fetchErr.code });
|
|
rejectPending(fetchErr);
|
|
throw fetchErr;
|
|
} finally {
|
|
kme_CSA_settings._pendingFetch = null;
|
|
}
|
|
|
|
// 7. Respond success
|
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
res.end('Authorized');
|
|
|
|
} catch (err) {
|
|
let message;
|
|
if (err.response) {
|
|
message = 'HTTP ' + err.response.status;
|
|
} else if (err.code === 'ECONNABORTED' || err.code === 'ERR_CANCELED') {
|
|
message = 'token service timeout';
|
|
} else {
|
|
message = err.message;
|
|
}
|
|
console.error({ message: 'Auth failed', error: message, url: req.url });
|
|
res.writeHead(401, { 'Content-Type': 'text/plain' });
|
|
res.end('Unauthorized: ' + message);
|
|
}
|
|
})()
|