DEV Community

Anakin
Anakin

Posted on

A safer pattern for authenticated automation: vault-backed sessions

Authenticated automation usually fails at the same boring place: login. Scraping a public page is easy compared with running a workflow inside a supplier portal, admin dashboard, CRM, or internal tool. At some point your automation needs a password, and the naive answer is to store that password in your app. That works until you need rotation, revocation, audit logs, or a security review.

A better pattern is to treat the password as a just-in-time input and the session as the reusable artifact.

The pattern: read the secret once, keep the session

For browser automation, you usually do not need the password on every run. You need it when no valid session exists.

The flow looks like this:

  1. Store the password in a vault your team already controls, such as 1Password, Bitwarden, Vault, or AWS Secrets Manager.
  2. Give the automation service a scoped token that can read only the required item.
  3. When the session is missing or expired, read the credential into memory.
  4. Log in with Playwright, Puppeteer, Selenium, or your browser driver of choice.
  5. Persist the resulting session state.
  6. Drop the password from memory and never write it to your database or logs.

Wire applies this model to authenticated browser automations by reading from 1Password when it needs to refresh a login, then running future tasks from the saved session rather than from the password.

The important distinction is this: your application stores something revocable and scoped, not the user's actual credential.

What this looks like in code

Here is a simplified Playwright example. It uses the 1Password CLI to fetch a login item, but the shape is the same if you use a vault SDK or a cloud secrets manager.

import { chromium } from 'playwright';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import fs from 'node:fs/promises';

const execFileAsync = promisify(execFile);

async function readLoginFrom1Password(itemId: string) {
  const { stdout } = await execFileAsync('op', [
    'item',
    'get',
    itemId,
    '--format',
    'json'
  ], {
    env: {
      ...process.env,
      OP_SERVICE_ACCOUNT_TOKEN: process.env.OP_SERVICE_ACCOUNT_TOKEN
    }
  });

  const item = JSON.parse(stdout);

  const username = item.fields.find((f: any) => f.id === 'username')?.value;
  const password = item.fields.find((f: any) => f.id === 'password')?.value;

  if (!username || !password) {
    throw new Error('Vault item is missing username or password');
  }

  return { username, password };
}

async function loginAndSaveSession(itemId: string, sessionPath: string) {
  const { username, password } = await readLoginFrom1Password(itemId);

  const browser = await chromium.launch();
  const context = await browser.newContext();
  const page = await context.newPage();

  await page.goto('https://example.com/login');
  await page.fill('input[name="email"]', username);
  await page.fill('input[name="password"]', password);
  await page.click('button[type="submit"]');

  await page.waitForURL('https://example.com/dashboard', { timeout: 30_000 });

  await context.storageState({ path: sessionPath });
  await browser.close();
}

async function runAuthenticatedTask(sessionPath: string) {
  const browser = await chromium.launch();
  const context = await browser.newContext({ storageState: sessionPath });
  const page = await context.newPage();

  await page.goto('https://example.com/dashboard');

  if (page.url().includes('/login')) {
    throw new Error('Session expired or rejected');
  }

  // Continue with the authenticated workflow.

  await browser.close();
}
Enter fullscreen mode Exit fullscreen mode

In production, I would avoid shelling out if a maintained SDK exists, but the example makes the boundary clear. The password enters memory for one login operation. The durable artifact is the browser storage state, not the password.

For Playwright, storageState usually contains cookies and localStorage. Treat it like a credential. Encrypt it at rest, restrict access to it, and expire it when the user revokes access.

The failure modes matter

This pattern does not remove authentication problems. It moves them to places you can reason about.

If the vault token is revoked, the next login refresh should fail clearly:

Error: failed to read credential from vault: access denied
Enter fullscreen mode Exit fullscreen mode

Existing sessions may continue until the target site expires them. That is normal browser behavior. If you need immediate cutoff, delete the saved session state when vault access is revoked.

If the password rotates, the next login refresh should pick up the new value automatically. If you copied the password into your own database, rotation becomes a coordination problem. If you read from the vault at login time, rotation stays where it belongs.

If the target site changes its login page, your automation still breaks. You will see symptoms like:

TimeoutError: page.waitForURL: Timeout 30000ms exceeded
Enter fullscreen mode Exit fullscreen mode

or:

Error: Session expired or rejected
Enter fullscreen mode Exit fullscreen mode

That failure has nothing to do with secret storage. You still need monitoring around login success rate, selector failures, MFA challenges, and redirects to unexpected pages.

Audit logs should live outside your app too

One underrated benefit of vault-backed auth is that the vault can log every read. That gives your security team an audit trail in a system they already trust.

Your app should still log its own authentication events:

{
  "event": "credential_read_for_login",
  "vault": "1password",
  "item_id": "supplier-portal-prod",
  "workflow_id": "daily-inventory-sync",
  "timestamp": "2026-06-08T10:15:30Z"
}
Enter fullscreen mode Exit fullscreen mode

Do not log usernames, passwords, session cookies, headers, or form payloads. Item IDs and workflow IDs are usually enough to debug access without leaking the secret itself.

Wire also relies on the vault provider's own activity log for credential reads, which means the audit trail does not only exist inside the automation platform.

What to store and what not to store

A reasonable implementation stores:

  • Encrypted session cookies or browser storage state
  • A pointer to the vault item
  • A scoped vault access token, encrypted separately
  • Metadata such as status, timestamps, and last refresh result

It should not store:

  • The password
  • A copy of the vault item contents
  • Screenshots of login pages containing filled password fields
  • Request logs containing auth headers or cookies
  • Full browser traces from login unless you scrub them carefully

The awkward part is the scoped vault token. It is still sensitive. If someone steals it, they may be able to read the same vault items your automation can read. Limit its scope, store it with envelope encryption, rotate it, and make revocation part of your offboarding process.

A practical way to start

If you already run authenticated automation, inspect where credentials live today. Search your database schema, environment variables, logs, and browser trace storage. Then change one workflow so it reads the password from a vault only when the saved session has expired. Keep the session encrypted, add a clear Session expired or rejected path, and verify that rotating the password in the vault does not require a deploy.

Top comments (0)