Skip to content

Accounts and Login

delabrcd edited this page Jun 6, 2026 · 1 revision

Accounts and Login

How the app stores National Grid logins, discovers billing accounts, and handles MFA — without ever putting a secret in the DB or git, and without becoming a public auth layer.

The encrypted credential store

A saved login is an NgLogin row: username plus the password sealed with AES-256-GCM into ciphertext / iv (12-byte nonce) / authTag. The seal/open math is a pure function in lib/crypto.ts; the AES key is scrypt-derived from raw key material that lives only outside the DB.

A decrypted password is held in memory just long enough to log in — it is never logged and never returned to the client. The API only ever reports a login's username/label/status.

Where the key comes from (lib/ngrid/secretKey.ts)

Resolution order — env always wins so existing installs are unchanged:

  1. NGRID_SECRET_KEY env var, if set and non-blank.
  2. A persisted file at DATA_DIR/session/secret.key, if present.
  3. Otherwise generate a strong 32-byte base64 key, write it 0600 (mkdir the dir 0700), and use it. This makes the prod cutover automatic — the encrypted store works out of the box.

Once generated, the key is stable across restarts (steps 2→3 happen once). Changing the key material would orphan every credential sealed under the old key, so the file lives in the root-only session volume (same place as the Playwright session). Keep that volume to keep stored logins decryptable. The decision logic (decideSecretKeyMaterial) is pure and tested; the I/O is best-effort (a transient FS error degrades to an in-memory key rather than crashing startup).

Env creds are the bootstrap/fallback

NGRID_USER / NGRID_PASS still work and remain the fallback when no NgLogin row exists. Accounts discovered through env creds get an Account row with a null loginId. On first run the app performs an automatic env → encrypted-store cutover (lib/ngrid/{bootstrap,firstRun}.ts) and a first-run setup flow so a fresh install lands in the encrypted store without manual steps.

Multiple billing accounts

One login can expose several billing accounts (multi-premise / linked). They're discovered during the scrape (lib/ngrid/accounts.ts), persisted as Account rows under the NgLogin, and the UI account switcher (components/AccountSwitcher.tsx, lib/accountSwitcher.ts) scopes every chart, export, and PDF view to the selected account.

Interactive MFA / OTP and re-authentication

The scraper reuses the session so it rarely logs in. When it must — a new login, an expired session, or a re-auth — and the account demands MFA, the in-app flow handles it interactively:

  • POST /api/ng-logins/preflight/[id] starts a login attempt; if an OTP is required it pauses.
  • POST /api/ng-logins/preflight/[id]/otp submits the code and finishes, seeding the session.
  • POST /api/ng-logins/[id]/reauth re-runs this for a login whose session went stale.

State lives in lib/ngrid/{preflight,preflightState,loginStatus}.ts. The unattended scheduler cannot answer an OTP prompt, so a login that always demands MFA surfaces a needs_reauth connection status instead of silently failing — you finish it from the UI.

Lifecycle: safe removal

Removing a login (DELETE /api/ng-logins/[id]) requires a password confirmation and lets you choose to keep or delete that account's data (bills/usage/PDFs). Account.loginId is onDelete: SetNull, so keeping the data simply detaches it from the removed login.

Security posture — read this

The in-app login-management UI is not a public application-login layer. It inherits the existing access gate (LAN-only / reverse proxy / SSO) and must never expose financial data un-gated. Don't turn it into, or document it as, a public auth front door. See Contributing.

Clone this wiki locally