Skip to main content
Status indicator: Under construction — coming soon

Authenticated scans (auth profiles)

Run the WCAG Audit CLI against your authenticated pages in CI — without an interactive login, and with support for multiple roles when different routes need different sessions.

Why this exists

Most accessibility issues hide behind login. The CLI's default --auth flag opens an interactive Chromium window and waits for you to log in. That works on a developer's laptop but not in CI where there's no human to click through SSO.

Auth profiles solve this. Capture a session once locally, export it as a base64 secret, and the CLI uses it in CI. Define multiple profiles when different parts of your app need different roles.

The 3-minute version

  1. Capture a profile locally. A Chromium window opens — you log in, the CLI captures the session.
    wcag-audit auth login --profile admin --url https://your-app.com/login
  2. Export it for CI. Pipe the base64 payload into your secret store.
    wcag-audit auth export --profile admin | pbcopy   # macOS
    wcag-audit auth export --profile admin | xclip   # Linux
    Then add it as a repository secret named WCAG_AUTH_ADMIN (the CLI infers the env-var name from the profile name).
  3. Map routes to profiles. Create wcag-audit.config.js in your repo:
    js
    // wcag-audit.config.js
    export default {
      authProfiles: {
        admin: { fromEnv: "WCAG_AUTH_ADMIN" },
        user:  { fromEnv: "WCAG_AUTH_USER"  },
      },
      routeAuth: [
        { path: "/",          auth: null    },  // public
        { path: "/dashboard", auth: "user"  },
        { path: "/admin/**",  auth: "admin" },
      ],
    };
  4. Run in CI. Pass the secrets as env vars; the CLI picks them up automatically.
    yaml
    # .github/workflows/wcag.yml
    - run: wcag-audit scan --require-auth-strict
      env:
        WCAG_LICENSE_KEY: ${{ secrets.WCAG_LICENSE_KEY }}
        WCAG_AUTH_ADMIN:  ${{ secrets.WCAG_AUTH_ADMIN }}
        WCAG_AUTH_USER:   ${{ secrets.WCAG_AUTH_USER }}

Profile naming

  • Lowercase letters, digits, hyphens. Must start with a letter or digit.
  • 1–32 chars. admin, user, read-only all work.
  • Env-var conversion is automatic: admin WCAG_AUTH_ADMIN, read-only WCAG_AUTH_READ_ONLY. Override with { fromEnv: "MY_CUSTOM_NAME" } if you need to.

Route → profile mapping

Rules in routeAuth are matched in order; first match wins. Set auth: null for public routes (no session attached).

Glob syntax (no extra dependencies):

  • * — any chars except / (one segment fragment)
  • ** — any chars including / (any number of segments, including zero)
  • ? — any single non-slash char
  • Everything else is literal (regex meta-chars are escaped for you)

Examples:

  • /admin/* matches /admin/users but NOT /admin/users/123
  • /admin/** matches /admin, /admin/users, AND /admin/users/123
  • /users/? matches /users/1 but NOT /users/12

Programmatic login (SSO, MFA, rotating creds)

If your login can't be captured as a static storageState — Okta / Entra / one-time codes / short-lived JWTs — write the login flow as a Playwright function in wcag-audit.config.js. The CLI runs it once per scan, captures the resulting session, and reuses it across all routes that map to that profile.

js
// wcag-audit.config.js
export default {
  authProfiles: {
    sso: {
      // Runs in Playwright. Drive whatever login flow you need.
      // CLI grabs storageState immediately after this resolves.
      async login(page) {
        await page.goto("https://your-app.com/login");
        await page.fill("#email",    process.env.SSO_EMAIL);
        await page.fill("#password", process.env.SSO_PASSWORD);
        await page.click("button[type=submit]");
        await page.waitForURL("**/dashboard", { timeout: 30_000 });
      },
      loginTimeoutMs: 60_000,  // optional, default 5 min
    },
  },
  routeAuth: [{ path: "/**", auth: "sso" }],
};

Resolution order is: env var (preferred) programmatic hookdisk file. So you can keep the hook for local development and override with a captured env var in CI for speed.

--require-auth-strict

Catches the most common silent CI failure: your stored session expired, admin routes got redirected to /login, the scan reports 0 issues when the real failure is "no auth." With this flag, any auth-blocked route fails the scan with a clear remediation message.

bash
wcag-audit scan --require-auth-strict

Strongly recommended for CI. Pair with a 30-day session re-capture cadence (the CLI warns when a profile is older than that).

Security guidance

StorageState carries live session tokens. Treat it like any other secret:

  • Use a dedicated CI test user per role, not your own account. If a CI secret leaks, you rotate one test user — not your admin login.
  • Never commit ~/.wcag-audit/auth/*.json or paste the base64 payload anywhere outside a secret store. The CLI writes those files at 0600 in a 0700 directory specifically so they don't leak via shared dev hosts.
  • Rotate every 30 days. The CLI warns when a profile is older than 30d (configurable per-profile via maxAgeDays). Sessions usually expire well before tokens do, but rotating defends against compromise too.
  • Scope your test users. Give them read-only access to dashboards. They're scanning for accessibility, not editing data.
  • Use --require-auth-strict in CI so a rotated/expired session fails loudly instead of silently scanning public pages.

Subcommand reference

Auth subcommands and their purposes
CommandPurpose
auth login --profile X --url YOpen Chromium, wait for login, capture as profile X
auth listShow saved profiles + age + origin
auth export --profile XPrint base64 payload to stdout (pipe to clipboard / secret store)
auth import --profile X [--base64 …]Load a base64 payload from stdin or flag, save under profile X
auth clear --profile XDelete the saved profile

Storage details

  • Files: ~/.wcag-audit/auth/<name>.json
  • File mode: 0600 (owner read/write only)
  • Directory mode: 0700
  • Schema: { profile, savedAt, schemaVersion: 1, meta: { urlOrigin }, storageState: { cookies, origins } }
  • Base64 export wraps the entire payload (including savedAt) so the receiving end can still warn on stale sessions in CI.

Troubleshooting

"Profile X could not be resolved"

The loader tried env var, programmatic hook, and disk file — none provided a session. Either capture locally with auth login --profile X or set the env var (or both).

"Login hook returned no cookies or origin storage"

Your async login(page) ran without throwing but didn't actually authenticate. Check that the post-login URL/selectors are correct, and that your page.waitForURL("**/dashboard") isn't timing out.

"Profile X is N days old"

The session is approaching expiry. Re-run auth login --profile X locally and re-export. Set authProfiles.X.maxAgeDays to tune the warning threshold per profile.

Routes silently scan as "0 issues"

Almost always stale auth. Run with --require-auth-strict — the CLI will list every route that hit a login wall and fail the scan.

← Back to command reference · Troubleshooting →