Skip to main content

Auth profiles

Auth profiles let the same API scenario run as different identities. Use them to test whether admins, regular users, viewers, unauthenticated clients, and invalid tokens get the right access.

How they work

An environment can carry many named auth profiles. Each profile has its own credentials and login config. At run time, a scenario picks one profile and inherits that profile’s token for every step. A profile has two flavors:
  • API: posts credentials to a login endpoint and extracts a token from the response.
  • UI: drives Playwright through the login form and captures storage state (plus sniffs a bearer token from network traffic for the API fallback).
Three concrete examples on a staging environment:
Profile nametypeWhat it logs in as
adminapiA privileged account that can read and write everything
userapiA regular account scoped to its own resources
viewerapiA read-only account
A scenario that tests IDOR can run once as one user, capture that user’s userId, then run as a different user profile and try to read the first user’s resource. The expected result is 403. If the API returns 200, the assertion fails and Qodex opens a critical finding.

Precedence

For API steps, the runner picks an auth source in this exact order:
  1. Static authToken on the environment, if set directly. Overrides everything.
  2. Cached bearer token if the cache is still fresh (TTL 30 minutes).
  3. api_login_config on the environment (or the chosen auth_profile.login_config). Run the login, extract the token.
  4. Browser fallback via ui_login_steps. Drive Playwright through the login form, sniff the bearer token off the network.
The cache is per environment and per profile. Successful logins are cached for 30 minutes. Saving an environment clears the cache. Cached tokens are redacted in API responses. For UI steps, the runner uses the cached storage state if fresh, otherwise runs ui_login_steps.

The api_login_config shape

The HTTP login configuration tells Qodex how to exchange credentials for a token.
{
  "url": "${API_BASE_URL}/auth/login",
  "method": "POST",
  "headers": { "Content-Type": "application/json" },
  "body": {
    "email": "${AUTH_EMAIL}",
    "password": "${AUTH_PASSWORD}"
  },
  "tokenPath": "data.access_token"
}
What each field does:
  • url: where to POST. Must resolve to an absolute URL after ${var} substitution. Bare paths are rejected.
  • method: usually POST. The runner accepts any HTTP verb.
  • headers: any headers the login endpoint needs. Default Content-Type: application/json.
  • body: the request body. Plain JSON for most APIs, form-encoded for legacy.
  • tokenPath: dot-notation JSONPath into the response body. data.access_token reads response.data.access_token. The runner uses a small JSONPath subset, dots only. For exotic paths, write a postscript instead.
The token returned here becomes the cached bearer for every API step in scenarios that pick this profile.

When this matters

The whole point of running tests as multiple identities is authorization correctness:
  • IDOR (insecure direct object reference): a user profile should not be able to read another user’s resources. Run the same scenario as two different user profiles, capture an ID from one, request it as the other, assert 403.
  • BOLA (broken object-level authorization): same idea at the object level. List your orders as one user, then try to load one of those order IDs as another user.
  • Role escalation: a viewer should not be able to write. Run the write scenarios as viewer and assert 403 across the board.
  • Endpoint-level auth gating: an unauthenticated client should get 401 on protected routes. Use a profile with no token (or a deliberately invalid one) to assert that.
You cannot test these correctly with a single shared admin token. The token has to be missing, invalid, lower-privilege, or wrong for the resource on purpose.

Sample environment with two profiles

{
  "name": "staging",
  "hosts": {
    "api": "https://staging.example.com",
    "ui": "https://app-staging.example.com"
  },
  "auth_profiles": [
    {
      "name": "admin",
      "type": "api",
      "credentials": {
        "AUTH_EMAIL": "admin@example.com",
        "AUTH_PASSWORD": "${ADMIN_PASSWORD}"
      },
      "login_config": {
        "url": "${API_BASE_URL}/auth/login",
        "method": "POST",
        "body": { "email": "${AUTH_EMAIL}", "password": "${AUTH_PASSWORD}" },
        "tokenPath": "access_token"
      }
    },
    {
      "name": "user",
      "type": "api",
      "credentials": {
        "AUTH_EMAIL": "user@example.com",
        "AUTH_PASSWORD": "${USER_PASSWORD}"
      },
      "login_config": {
        "url": "${API_BASE_URL}/auth/login",
        "method": "POST",
        "body": { "email": "${AUTH_EMAIL}", "password": "${AUTH_PASSWORD}" },
        "tokenPath": "access_token"
      }
    }
  ]
}
Same login endpoint, different credentials, different tokens.

On the roadmap

OAuth2 four-grant support with refresh-first caching (Playground phase 2). The plumbing already lives in login-runner.ts for oauth2_client_credentials and oauth2_auth_code; the rest of the surface (UI for reconnecting expired refresh tokens, per-profile expiry resolution) is in flight.

Scenarios

See where step.auth attaches.

Chaining and postscripts

Capture tokens from a login response.

API Playground

Run requests with any auth profile.

Auto-verification on save

What gets checked the moment you save.