Authentication
The dashboard supports optional user authentication with role-based access control. Authentication is disabled by default.
Roles
| Role | Description |
|---|---|
| Administrator | Full access to all features including editing projects, managing users, and deleting runs |
| Reporter | Can only call submission API endpoints (/api/test-runs/submit and /api/test-runs/upload) |
| User | Read-only access to all dashboard pages and data |
Enabling authentication
Copy the example environment file:
bashcd 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 first enabled and no users exist, create the first administrator account:
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 $bodyThis endpoint is only available when the users table is empty.
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
Each non-admin user's project access (affectations) is managed from the Project access action on this page (per user), or from a project's Members tab (per project). A user can be granted global access (all projects) or scoped to specific ones.
Try it in the demo: the live demo ships with several pre-seeded identities. Use the Acting as picker in the demo banner to switch between them and watch how each user's project affectations change what they can see. See UI overview → Demo user switcher.
API authentication
When authentication is enabled:
POST/PUT/DELETEendpoints require an active session with appropriate role permissions.GETendpoints remain publicly accessible (read-only), exceptGET /api/users, which requires an authenticated session so the user list (usernames and roles) is not exposed to anonymous callers.- 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.
- 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.