secure-scan-js
Version:
A JavaScript implementation of Yelp's detect-secrets tool - no Python required
272 lines (246 loc) • 8.55 kB
JavaScript
// Web-based CLI Authentication Flow
const express = require('express');
const open = require('open');
const fs = require('fs');
const path = require('path');
const http = require('http');
const crypto = require('crypto');
const chalk = require('chalk');
// Configuration
const AUTH_CONFIG = {
// Default to 3 minutes (180 seconds) expiration
tokenExpirationSeconds: 300 * 60 * 24,
callbackPort: 3005,
tokenFile: path.join(process.env.HOME || process.env.USERPROFILE, '.detect-secrets-token.json'),
// Replace this with your actual web app auth URL
authUrl: 'https://app.securesecretsai.com/dashboard/cli' || 'https://mywebapp.com/auth/cli'
};
/**
* CLI Authentication class for web-based login flow
*/
class WebAuth {
constructor(config = {}) {
this.config = { ...AUTH_CONFIG, ...config };
this.app = express();
this.server = null;
// Generate state parameter for CSRF protection
this.state = crypto.randomBytes(16).toString('hex');
}
/**
* Initialize the authentication server
* @returns {Promise} Promise that resolves when auth is complete
*/
async login() {
console.log(chalk.cyan('Starting web authentication flow...'));
return new Promise((resolve, reject) => {
// Set up the callback endpoint
this.app.get('/callback', (req, res) => {
try {
// Verify state parameter to prevent CSRF
if (req.query.state !== this.state) {
res.status(400).send('Invalid state parameter. Authentication failed.');
reject(new Error('Invalid state parameter'));
return;
}
// Get token from query parameters
const token = req.query.token || req.query.code;
if (!token) {
res.status(400).send('No token or code provided');
reject(new Error('No token or code provided'));
return;
}
// Calculate expiration time (3 minutes from now)
const now = Math.floor(Date.now() / 1000);
const expiresAt = now + this.config.tokenExpirationSeconds;
// Store token with expiration
const tokenData = {
token,
expiresAt,
createdAt: now
};
// Save token to file
this.saveToken(tokenData);
// Send success page
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Authentication Successful</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
padding: 50px;
background-color: #f5f5f5;
}
.success-box {
background-color: white;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
max-width: 500px;
margin: 0 auto;
}
h1 { color: #4CAF50; }
p { color: #555; }
.cli-message {
background-color: #f0f0f0;
padding: 10px;
border-radius: 4px;
font-family: monospace;
margin: 20px 0;
}
</style>
</head>
<body>
<div class="success-box">
<h1>Authentication Successful</h1>
<p>You have successfully authenticated the CLI.</p>
<p>You can close this window and return to the terminal.</p>
<div class="cli-message">
Token valid for ${this.config.tokenExpirationSeconds} seconds (expires at ${new Date(expiresAt * 1000).toLocaleTimeString()})
</div>
</div>
<script>
// Close the window automatically after 5 seconds
setTimeout(() => window.close(), 5000);
</script>
</body>
</html>
`);
// Close server and resolve promise
this.closeServer();
resolve(tokenData);
} catch (error) {
res.status(500).send(`Authentication error: ${error.message}`);
reject(error);
}
});
// Start server
this.server = this.app.listen(this.config.callbackPort, () => {
console.log(chalk.blue(`Authentication server started on port ${this.config.callbackPort}`));
// Build auth URL with redirect to our local server
const callbackUrl = `http://localhost:${this.config.callbackPort}/callback`;
const authUrl = `${this.config.authUrl}?redirect_uri=${encodeURIComponent(callbackUrl)}&state=${this.state}`;
// Open browser
console.log(chalk.yellow('Opening browser for authentication...'));
console.log(chalk.yellow(authUrl));
open(authUrl).catch(err => {
console.error('Failed to open browser:', err);
console.log(chalk.yellow(`Please open this URL manually: ${authUrl}`));
});
});
// Handle server errors
this.server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(chalk.red(`Port ${this.config.callbackPort} is already in use.`));
console.error(chalk.yellow('You may have another authentication process running.'));
console.error(chalk.yellow('Please close it or use a different port.'));
} else {
console.error(chalk.red('Server error:'), err);
}
reject(err);
});
// Set a timeout for authentication (2 minutes)
setTimeout(() => {
if (this.server) {
this.closeServer();
reject(new Error('Authentication timed out. Please try again.'));
}
}, 120000); // 2 minutes
});
}
/**
* Close the authentication server
*/
closeServer() {
if (this.server) {
this.server.close();
this.server = null;
console.log(chalk.blue('Authentication server closed'));
}
}
/**
* Save token to file
* @param {Object} tokenData Token data object
*/
saveToken(tokenData) {
try {
fs.writeFileSync(this.config.tokenFile, JSON.stringify(tokenData, null, 2));
console.log(chalk.green('Token saved successfully'));
} catch (error) {
console.error(chalk.red('Failed to save token:'), error);
throw error;
}
}
/**
* Get token status
* @returns {Object} Token status
*/
getTokenStatus() {
try {
// Check if token file exists
if (!fs.existsSync(this.config.tokenFile)) {
return {
valid: false,
message: 'Token missing. Please run "yarn custom:login".'
};
}
// Read token
const tokenData = JSON.parse(fs.readFileSync(this.config.tokenFile, 'utf8'));
// console.log(tokenData, "__tokenData")
// Check if token is expired
const now = Math.floor(Date.now() / 1000);
if (now > tokenData.expiresAt) {
return {
valid: false,
message: 'Token expired. Please run "yarn custom:login".',
expiredAt: new Date(tokenData.expiresAt * 1000).toLocaleString()
};
}
// Calculate remaining time
const remainingSeconds = tokenData.expiresAt - now;
// Token is valid
return {
valid: true,
message: 'Token is valid.',
expiresIn: `${remainingSeconds} seconds`,
expiresAt: new Date(tokenData.expiresAt * 1000).toLocaleString()
};
} catch (error) {
console.error(chalk.red('Error checking token status:'), error);
return {
valid: false,
message: `Error checking token: ${error.message}`
};
}
}
/**
* Check if token is valid
* @returns {boolean} Whether token is valid
*/
isTokenValid() {
const status = this.getTokenStatus();
return status.valid;
}
/**
* Sign out - remove the token
*/
logout() {
try {
if (fs.existsSync(this.config.tokenFile)) {
fs.unlinkSync(this.config.tokenFile);
console.log(chalk.green('Token removed. You are now logged out.'));
return true;
} else {
console.log(chalk.yellow('No token found. Already logged out.'));
return false;
}
} catch (error) {
console.error(chalk.red('Error during logout:'), error);
return false;
}
}
}
// Create and export singleton instance
module.exports = new WebAuth();