Authentication
The dashboard supports optional user authentication with role-based access control. Authentication is disabled by default.
Roles
| Role | Description |
|---|---|
| Administrator | Full access to every project and feature — editing projects, managing users, and deleting runs. Never restricted by project access. |
| Reporter | Submits results (/api/test-runs/submit, /api/test-runs/upload) and can triage, but only for the projects it's assigned to. |
| User | Read-only access to the projects it's assigned to. |
Administrators always see everything. Reporter and User accounts are additionally scoped by project access — they only see and act on the projects assigned to them.
Enabling authentication
Copy the example environment file:
bashcd apps/application cp .env.example .envEdit
.envand set:bashPIWI_SECRET_KEY=your-secret-key-here # encrypts DB secrets (AI keys, SCM tokens) PIWI_AUTH_ENABLED=true PIWI_AUTH_SECRET=your-auth-secret-here # encrypts session cookiesGenerate strong random values for both (run twice — once for
PIWI_SECRET_KEY, once forPIWI_AUTH_SECRET):bash# Cross-platform — works anywhere Node is installed (Node is already required) node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))" # …or with openssl on Linux / macOS / Git Bash openssl rand -hex 32Restart the application.
Initial setup
When authentication is enabled and no users exist yet, opening the dashboard lands on the login page, which shows a Create the first admin account form in place of the sign-in form. Fill it in and you are signed in as the administrator — no API call needed.
For provisioning an instance from a script, the same step is available as an API call:
curl -X POST http://localhost:3000/api/auth/setup \
-H "Content-Type: application/json" \
-d '{
"username": "admin",
"password": "your-secure-password",
"name": "Administrator"
}'$body = @{ username = 'admin'; password = 'your-secure-password'; name = 'Administrator' } | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri http://localhost:3000/api/auth/setup `
-ContentType 'application/json' -Body $bodyBoth routes go through POST /api/auth/setup, which is only available while the users table is empty and is rate-limited per client address.
Logging in
Navigate to /login in your browser, or use the API:
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "your-secure-password"}'$body = @{ username = 'admin'; password = 'your-secure-password' } | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri http://localhost:3000/api/auth/login `
-ContentType 'application/json' -Body $bodySessions are stored in encrypted cookies and last for 7 days.
OAuth (Google, GitHub)
The dashboard supports signing in with Google or GitHub as an alternative to username/password authentication.
Configuring OAuth
Register an OAuth application with each provider you want to use:
- Google: Go to the Google Cloud Console, create an OAuth 2.0 Client ID, and add
https://your-domain.com/api/auth/oauth/google/callbackto the authorized redirect URIs. - GitHub: Go to Settings → Developer settings → OAuth Apps on GitHub, create a new OAuth app, and set the callback URL to
https://your-domain.com/api/auth/oauth/github/callback.
- Google: Go to the Google Cloud Console, create an OAuth 2.0 Client ID, and add
Add the credentials to your
.envfile:bashPIWI_OAUTH_GOOGLE_CLIENT_ID=your-google-client-id PIWI_OAUTH_GOOGLE_CLIENT_SECRET=your-google-client-secret PIWI_OAUTH_GITHUB_CLIENT_ID=your-github-client-id PIWI_OAUTH_GITHUB_CLIENT_SECRET=your-github-client-secretOnly configure the providers you actually want to use. OAuth buttons appear automatically on the login page when both a provider's
CLIENT_IDandCLIENT_SECRETare set.Restart the application. The login page now shows Sign in with Google and/or Sign in with GitHub buttons above the password form.
Behind a reverse proxy: set
PIWI_SITE_URLto your public URL (e.g.https://piwi.example.com). The OAuthredirect_uriis built from it, so it stays consistent with the value you registered even when the proxy rewrites the request host. Without it the redirect URI is inferred from the incoming request.
Restricting who can sign in (allowlists)
By default any account at a configured provider can sign in (and a new user-role account is created). To restrict access:
PIWI_OAUTH_ALLOWED_DOMAINS— comma-separated email domains (e.g.example.com,acme.org). Only verified provider emails in these domains may sign in. Applies to all providers — ideal for limiting Google Workspace sign-in to your company domain.PIWI_OAUTH_GITHUB_ALLOWED_ORGS— comma-separated GitHub org logins. The user must be a member of at least one. Enabling this requests theread:orgscope so membership (including private) can be checked.
Rejected sign-ins are returned to the login page with an explanatory message.
How it works
- User clicks an OAuth button on the login page.
- The server redirects to the provider's authorization page with a cryptographically random
stateparameter stored in an httpOnly cookie. For providers that support it (Google), a PKCEcode_challenge(S256) is also sent and its verifier stored in an httpOnly cookie, so a stolen authorization code can't be redeemed without the verifier. - After authorization, the provider redirects back to the callback URL.
- The server validates the
statecookie (CSRF protection), exchanges the code for an access token (sending the PKCE verifier when used), and fetches the user's profile (name, email, avatar). - A local user is created or linked:
- If a user with the same OAuth provider + ID exists, their name/avatar/email are refreshed from the provider.
- If a user with the same verified email exists (and isn't already linked to a different provider), the existing account is linked to the OAuth provider. Linking only happens when the provider asserts the email is verified, which prevents account takeover via an attacker-controlled public email.
- Otherwise, a new user is created with the user role and an empty password (password login disabled for OAuth-only users). The provider email is stored on the account.
- A session is established (same encrypted cookie as password login), and the browser is redirected to the dashboard homepage.
Notes
- OAuth users have an empty password and cannot sign in with username/password. They must always use their OAuth provider.
- The provider's email address is stored on the OAuth account, so OAuth users can receive email notifications and appear with a verified email in the admin user list.
- If a sign-in's verified email matches an account that is already linked to a different provider, the login is rejected with an "already linked to a different sign-in method" message (the schema links one provider per account) — sign in with the original method instead.
- GitHub accounts with no verified primary email are still allowed to sign in, but a fresh account is created rather than linked.
- The reporter (CI/CD) authentication is unaffected — it continues to use API keys or username/password.
- OAuth is not available in demo mode; the buttons are not shown.
- Avatar URLs from the provider are displayed in the user menu when available.
- The dashboard does not store provider refresh/access tokens — the access token is used once at sign-in to read the profile, then discarded; only the dashboard's own session cookie persists.
Connecting / disconnecting a provider
Signed-in users can link a provider explicitly from Settings → Account → Connected accounts:
- Connect starts the OAuth flow in "link" mode and attaches the provider identity to the current account (rather than creating a new one). A provider identity already linked to another account is refused.
- Disconnect removes the link. It's only allowed when the account also has a password set, so a provider-only user can't lock themselves out — set a password first.
One provider can be connected per account.
User management
User accounts are managed through the admin interface at /settings/users.
This page is accessible to administrators (or to everyone when authentication is disabled, with an informational message).
To create additional users:
- Navigate to
/settings/users - Click Add user
- Set username, password, role, and optional display name
Project access
Administrators see every project. Reporter and User accounts see only the projects they are assigned to — so you can give each team its own slice of the dashboard.
An assignment is one of two kinds:
- Global — access to all projects, including ones created later. Good for a shared CI reporter or a team lead.
- Per-project — access to a specific set of projects only.
What scoping affects for a non-admin user:
- The project list, sidebar menu, home dashboard, recent runs, and search only include assigned projects.
- Runs, test cases, and clusters that belong to an unassigned project return 403.
- A reporter can submit results to an assigned project. Creating a new project on first submission requires global access — a per-project reporter can't invent projects.
Default is no access. A freshly created Reporter/User has no assignments and sees an empty dashboard until you grant some. (When authentication is first enabled, existing accounts are automatically backfilled with global access so nothing breaks on upgrade.)
Managing assignments
Assignments are administrator-only and can be edited from either direction:
- Per user — Settings → Users → Project access (the action next to a user). Choose global access or tick specific projects.
- Per project — a project's Members tab (
/projects/:id, admins only). Add or remove users for that one project.
Both edit the same underlying assignments, so use whichever is more convenient.
Try it in the demo: the live demo ships with several pre-seeded identities — an admin, a global CI reporter, and users scoped to one or two projects (plus one with none). Use the Acting as picker in the demo banner to switch between them and watch the project list, sidebar, and search change to match each user's access. Acting as the admin, change the assignments live and switch back to see the effect. See UI overview → Live demo.
API authentication
When authentication is enabled:
- Every endpoint requires an authenticated caller — a session cookie or an API key — with the role the endpoint declares (listed per endpoint in the API docs). The exceptions are
GET /api/health,GET /api/version, the auth flows themselves (login, initial setup, password reset, email verification, OAuth), and the live-run streaming endpoints, which authenticate with per-run stream tokens instead. - The reporter's submission endpoints (
/api/test-runs/submitand/api/test-runs/upload) accept both session cookies and API keys.
API keys
API keys are the recommended way to authenticate CI pipelines and the Playwright reporter. They are long-lived tokens tied to a specific user account.
Security properties
- Keys are generated with 256 bits of cryptographic entropy (
pd_prefix + 64-character hex string). - Only a SHA-256 hash of the key is stored in the database — the plaintext is shown once at creation time and never retrievable again.
- Each key displays a short prefix (
pd_xxxxxxxx…) in the UI for identification without revealing the secret. - Keys are sent as
Authorization: Bearer <key>orX-API-Key: <key>headers. - Keys can be given an optional expiry date.
Creating an API key
- Navigate to Settings → Users in the dashboard.
- Click the key icon next to the user you want to generate a key for.
- Click Create API key, enter a descriptive name (e.g. "GitHub Actions"), and set an optional expiry.
- Copy the key immediately — it will never be shown again.
- Store it as a CI secret (e.g.
PIWI_API_KEY).
Revoking an API key
- Navigate to Settings → Users and click the key icon.
- Click the trash icon next to the key you want to revoke.
- The key stops working immediately.
Using the API key in the reporter
// playwright.config.ts
export default defineConfig({
reporter: [
['@piwitests/reporter', {
serverUrl: 'https://your-dashboard.example.com',
projectName: 'my-project',
apiKey: process.env.PIWI_API_KEY,
}],
],
})Using the API key in raw HTTP calls
# Authorization: Bearer header (recommended)
curl -X POST https://your-dashboard.example.com/api/test-runs/submit \
-H "Authorization: Bearer pd_<your-key>" \
-H "Content-Type: application/json" \
-d '{ ... }'
# X-API-Key header (alternative)
curl -X POST https://your-dashboard.example.com/api/test-runs/submit \
-H "X-API-Key: pd_<your-key>" \
-H "Content-Type: application/json" \
-d '{ ... }'# Authorization: Bearer header (recommended)
Invoke-RestMethod -Method Post -Uri https://your-dashboard.example.com/api/test-runs/submit `
-Headers @{ Authorization = 'Bearer pd_<your-key>' } `
-ContentType 'application/json' -Body '{ ... }'
# X-API-Key header (alternative)
Invoke-RestMethod -Method Post -Uri https://your-dashboard.example.com/api/test-runs/submit `
-Headers @{ 'X-API-Key' = 'pd_<your-key>' } `
-ContentType 'application/json' -Body '{ ... }'Using the reporter with session authentication (username/password)
As an alternative to API keys, create a dedicated user with the reporter role for your CI pipelines:
Log in as an administrator in
/settings/usersand add a new user with the Reporter role.Configure the reporter with the credentials:
typescript// playwright.config.ts export default defineConfig({ reporter: [ ['@piwitests/reporter', { serverUrl: 'https://your-dashboard.example.com', projectName: 'my-project', username: process.env.PIWI_USERNAME, password: process.env.PIWI_PASSWORD, }], ], })Add
PIWI_USERNAMEandPIWI_PASSWORDas secrets in your CI provider.
The reporter automatically calls /api/auth/login before each upload and uses the resulting session for all subsequent requests.
Tip: API keys are preferred over username/password for CI because they don't require a login round-trip and can be individually revoked.
Security considerations
- Always use HTTPS in production.
- Use strong, unique passwords.
- Set
PIWI_SECRET_KEY(generate withnode -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))", oropenssl rand -hex 32) to encrypt AI API keys and SCM tokens at rest in the database. This is recommended even when authentication is disabled. - Set
PIWI_AUTH_SECRET(same generator) for session cookie encryption — required whenPIWI_AUTH_ENABLED=true. - Passwords are hashed using scrypt with per-password salts.
- Login, initial setup, and password-reset endpoints are rate-limited per client address — failed logins also per account — and throttled requests get a
429with aRetry-Afterheader. Behind a reverse proxy, setPIWI_TRUST_PROXYso those per-address limits see real client addresses; see the configuration reference. - Never use the default secrets in production.
Disabling authentication
To disable authentication:
- Set
PIWI_AUTH_ENABLED=falsein.env, or remove the variable entirely. - Restart the application.
When disabled, all endpoints are accessible without authentication.