|
#!/usr/bin/env node |
|
|
|
import { spawn } from 'node:child_process'; |
|
import * as http from 'node:http'; |
|
import * as fs from 'node:fs/promises'; |
|
import * as path from 'node:path'; |
|
|
|
const LOCAL_JWT_REDIRECT_PORT = 12765; |
|
|
|
let silentMode = false; |
|
let jwtCachePath = null; |
|
let loginUrl = null; |
|
|
|
const args = process.argv.slice(2); // Skip 'node' and script path |
|
for (let i = 0; i < args.length; i++) { |
|
const arg = args[i]; |
|
const value = args[i + 1]; |
|
|
|
if (arg === '-u' && value !== undefined && isUrl(value)) { |
|
loginUrl = value; |
|
i++; |
|
} else if (arg === '-c' && value !== undefined) { |
|
jwtCachePath = path.resolve(value); |
|
i++; |
|
} else if (arg === '-s') { |
|
silentMode = true; |
|
} |
|
} |
|
|
|
function decodeJwtPayload(token) { |
|
const parts = token.split('.'); |
|
if (parts.length !== 3) return null; |
|
const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/'); |
|
const padded = payload.padEnd(payload.length + ((4 - (payload.length % 4)) % 4), '='); |
|
try { |
|
const json = Buffer.from(padded, 'base64').toString('utf8'); |
|
return JSON.parse(json); |
|
} catch { |
|
return null; |
|
} |
|
} |
|
|
|
function isJwtValid(token) { |
|
const payload = decodeJwtPayload(token); |
|
if (!payload || typeof payload.exp !== 'number') return false; |
|
const nowSeconds = Math.floor(Date.now() / 1000); |
|
return payload.exp > nowSeconds; |
|
} |
|
|
|
async function readCachedJwt(cachePath) { |
|
try { |
|
const raw = await fs.readFile(cachePath, 'utf8'); |
|
const parsed = JSON.parse(raw); |
|
if (parsed && typeof parsed.jwt === 'string' && isJwtValid(parsed.jwt)) { |
|
return parsed.jwt; |
|
} |
|
return null; |
|
} catch { |
|
return null; |
|
} |
|
} |
|
|
|
async function writeCachedJwt(cachePath, jwt) { |
|
const payload = { |
|
jwt, |
|
savedAt: new Date().toISOString(), |
|
}; |
|
await fs.writeFile(cachePath, JSON.stringify(payload, null, 2), 'utf8'); |
|
} |
|
|
|
function isUrl(str) { |
|
try { |
|
new URL(str); |
|
return true; |
|
} catch { |
|
return false; |
|
} |
|
} |
|
|
|
async function retrieveJwtInteractive() { |
|
if (loginUrl) { |
|
// On Windows, 'start' opens the default browser |
|
spawn(`start "" "${loginUrl}"`, { shell: true, stdio: 'ignore', detached: true }); |
|
} |
|
|
|
return new Promise((resolve, reject) => { |
|
const server = http.createServer(async (req, res) => { |
|
res.setHeader('Access-Control-Allow-Origin', '*'); |
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); |
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); |
|
|
|
if (req.method === 'OPTIONS') { |
|
res.writeHead(204); // No content |
|
res.end(); |
|
return; |
|
} |
|
|
|
const requestUrl = new URL(req.url || '', `http://localhost:${LOCAL_JWT_REDIRECT_PORT}`); |
|
if (requestUrl.pathname === '/ext-login') { |
|
let body = ''; |
|
req.on('data', (chunk) => { |
|
body += chunk.toString(); |
|
}); |
|
|
|
req.on('end', () => { |
|
try { |
|
const jsonBody = JSON.parse(body); |
|
const jwt = jsonBody.jwt; |
|
|
|
if (jwt) { |
|
res.writeHead(200, { 'Content-Type': 'text/plain' }); |
|
res.end('Login success! You can close this window now.'); |
|
server.close(() => { |
|
resolve(jwt); |
|
}); |
|
} else { |
|
const errMessage = 'Could not find JWT in the request body.'; |
|
console.error(errMessage); |
|
res.writeHead(400, { 'Content-Type': 'text/plain' }); |
|
res.end(errMessage); |
|
reject(new Error(errMessage)); |
|
} |
|
} catch (error) { |
|
const errMessage = 'Error in processing the JSON body.'; |
|
console.error(errMessage, error); |
|
res.writeHead(400, { 'Content-Type': 'text/plain' }); |
|
res.end(errMessage); |
|
reject(new Error(errMessage)); |
|
} |
|
}); |
|
} else { |
|
res.writeHead(404); |
|
res.end(); |
|
} |
|
}); |
|
|
|
server.listen(LOCAL_JWT_REDIRECT_PORT, async () => { |
|
if (!silentMode) console.debug(`Local login HTTP server started on port ${LOCAL_JWT_REDIRECT_PORT}.`); |
|
|
|
}); |
|
|
|
server.on('error', (err) => { |
|
console.error('Error at local login HTTP server.', err); |
|
reject(err); |
|
}); |
|
}); |
|
} |
|
|
|
(async () => { |
|
const cachedJwt = jwtCachePath ? await readCachedJwt(jwtCachePath) : null; |
|
const jwt = cachedJwt || (await retrieveJwtInteractive()); |
|
|
|
if (jwtCachePath && !cachedJwt) { |
|
try { |
|
await writeCachedJwt(jwtCachePath, jwt); |
|
} catch (error) { |
|
if (!silentMode) console.warn('Failed to write JWT cache file.', error); |
|
} |
|
} |
|
|
|
const result = { |
|
data: { |
|
username: '', |
|
password: jwt, |
|
}, |
|
}; |
|
console.log(JSON.stringify(result, null, 2)); |
|
})().catch((error) => { |
|
console.error('JWT flow failed.', error); |
|
process.exitCode = 1; |
|
}); |