开发全局NPM CLI包:如何保存用户登录凭证?
Great question! Building a CLI that requires authenticated actions (just like npm publish) is a common pattern, and integrating it with your Firebase Auth-backed Express backend is totally manageable. Let’s walk through the steps to handle credential storage securely and smoothly.
1. First: Implement the Login Handshake (CLI + Backend)
Before worrying about storage, you need to get the login flow working between your CLI and backend:
- When a user runs
myPackage login, use a library likeinquirerto prompt them for their pre-created email and password. - Send these credentials via a POST request to your Express login endpoint. Your backend will use Firebase Auth’s tools to verify the credentials, then return a Firebase ID Token (or a custom JWT if you prefer) to the CLI.
Example CLI login code snippet:
const inquirer = require('inquirer'); const axios = require('axios'); async function login() { const { email, password } = await inquirer.prompt([ { type: 'input', name: 'email', message: 'Enter your email:' }, { type: 'password', name: 'password', message: 'Enter your password:' } ]); try { const response = await axios.post('https://your-backend.com/api/login', { email, password }); const { idToken } = response.data; // We'll store this idToken next await saveCredentials(idToken); console.log('Login successful!'); } catch (err) { console.error('Login failed:', err.response?.data?.message || err.message); } }
Simplified Express backend login endpoint:
const admin = require('firebase-admin'); app.post('/api/login', async (req, res) => { const { email, password } = req.body; try { // Verify credentials via Firebase Auth (note: Admin SDK doesn't directly check passwords, so you can use Firebase's REST sign-in endpoint here) const authResponse = await axios.post('https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=YOUR_FIREBASE_API_KEY', { email, password, returnSecureToken: true }); const idToken = authResponse.data.idToken; res.json({ idToken }); } catch (err) { res.status(401).json({ message: 'Invalid credentials' }); } });
2. Securely Store the Credentials
Never store tokens in plaintext files! The safest approach is to leverage the system’s native secure storage:
- Use the
keytarlibrary: It integrates with macOS Keychain, Windows Credential Manager, and Linux’s libsecret. This is exactly whatnpmuses for storing publish credentials. - Alternative (simpler but less secure):
configstorewith encryption, butkeytaris the gold standard for sensitive data.
Example code to save/retrieve tokens with keytar:
const keytar = require('keytar'); // Use a unique service name for your CLI to avoid conflicts const SERVICE_NAME = 'myPackage-cli'; const ACCOUNT_NAME = 'auth-token'; async function saveCredentials(idToken) { await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, idToken); } async function getCredentials() { return keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME); } async function logout() { await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME); console.log('Logged out successfully!'); }
3. Validate Credentials for Authenticated Commands
For commands like myPackage publish [args], you’ll need to:
- Fetch the stored token using
getCredentials(). - Send the token to your backend (via an
Authorization: Bearer <token>header) to verify its validity. - If the token is valid, proceed with the publish action; if invalid (or missing), prompt the user to log in again.
Example publish command logic:
async function publish(args) { const idToken = await getCredentials(); if (!idToken) { console.error('You need to log in first. Run `myPackage login`'); process.exit(1); } try { // Verify token with backend first await axios.post('https://your-backend.com/api/verify-token', {}, { headers: { Authorization: `Bearer ${idToken}` } }); // Proceed with your publish logic console.log('Publishing your content...', args); // Add your publish implementation here } catch (err) { if (err.response?.status === 401) { console.error('Session expired. Please log in again.'); await logout(); // Remove invalid token from storage process.exit(1); } console.error('Publish failed:', err.message); } }
Bonus Tips
- Handle token expiration: Firebase ID Tokens expire after 1 hour. You can either refresh them using a refresh token (returned alongside the ID token from your login endpoint) or prompt users to log in again when a 401 error is received.
- Add a
logoutcommand: As shown above, let users explicitly remove their credentials from the system keychain. - Test cross-platform:
keytarhas prebuilt binaries for most OSes, but Linux users may need to install system dependencies likelibsecret-1-dev.
内容的提问来源于stack exchange,提问作者Guy

