Skip to content

Sign-in and email

Out of the box, people sign in with an email address and password, and nothing is emailed. Two optional additions make a team install much easier to run:

  • Google and GitHub sign-in. Each provider’s button appears on the sign-in page once its client id and secret are set.
  • SMTP email. Needed for password-reset and email-confirmation links, and used for project invitations and failure alerts.

Both are configured on the server with environment variables; there’s nothing to switch on in the app.

The Plainwright sign-in page with Continue with Google and Continue with GitHub buttons above the email and password form

Sign-in uses the OAuth authorization-code flow. The provider sends the browser back to a callback URL on your server, so you register that URL with the provider:

<base>/api/oauth/google/callback
<base>/api/oauth/github/callback

<base> is OAUTH_REDIRECT_BASE, or APP_BASE_URL when that’s empty: the public origin people use, such as https://plainwright.example.com, with no trailing slash. For a local install it’s http://localhost:8080. The URL you register must match exactly, scheme and port included.

  1. In the Google Cloud console, open APIs & Services → OAuth consent screen and set up the consent screen (app name, support email; the openid, email and profile scopes are enough).

  2. Open Credentials → Create credentials → OAuth client ID, choose Web application, and add https://plainwright.example.com/api/oauth/google/callback under Authorized redirect URIs.

  3. Copy the client id and secret into .env:

    .env
    GOOGLE_CLIENT_ID=1234567890-abc.apps.googleusercontent.com
    GOOGLE_CLIENT_SECRET=<client-secret>

Then recreate the backend so it reads the new values, and reload the sign-in page:

Terminal window
docker compose up -d

If the app is served on a different origin from APP_BASE_URL (for example behind a second domain), set OAUTH_REDIRECT_BASE to the origin the provider should redirect to.

  • Only verified emails are used. Google must report the email as verified; for GitHub, the primary verified address is used (the user:email scope allows reading private addresses). A provider account without a verified email can’t sign in.
  • A provider account that’s already linked signs in to its Plainwright account.
  • An existing account with the same email is linked, ignoring case, but only if that account’s address is confirmed. Anyone can register any address with a password, so linking an unconfirmed account would hand the real owner’s sign-in to whoever registered first. Otherwise sign-in stops with “An account with this email already exists, but its email address hasn’t been confirmed…”: sign in with the password, confirm the email (or use Forgot password?), then continue with the provider again.
  • A new email creates an account with a random password that is never shown. The address counts as confirmed, because the provider verified it. That person signs in with the provider, or sets a password through Forgot password? once email works.
  • Pending invitations attach as soon as the address is confirmed, so a provider sign-in attaches them straight away.

Sign-in only completes in the browser that started it, and no session token ever appears in a URL:

  1. Start. GET /api/oauth/<provider>/start sets an HttpOnly, SameSite=Lax cookie with a random nonce and sends the browser to the provider with a signed state (valid 10 minutes) that carries the same nonce.
  2. Callback. The provider returns to /api/oauth/<provider>/callback. The server checks that the state matches the cookie, links or creates the account, and redirects to /auth/callback#code=… with a one-time login code, valid for 2 minutes.
  3. Exchange. The app posts the code to POST /api/oauth/exchange. The server returns a session token only if the code is unused and arrives with the nonce cookie of the browser that started sign-in, then deletes the cookie.

A code (or a token) planted in someone else’s browser therefore can’t sign them in to another account. A reloaded callback page, or one finished after 2 minutes, answers “This sign-in link was already used” or “Sign-in link expired or is invalid”; start again from the sign-in page.

Plainwright sends email with any SMTP server: your company’s relay or a transactional provider. Email counts as configured when SMTP_HOST and SMTP_FROM are both set.

.env
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=plainwright@example.com
SMTP_PASSWORD=<smtp-password>
SMTP_FROM=Plainwright <plainwright@example.com>
SMTP_TLS=true
# Failure and flaky alert emails (needs the SMTP settings above).
EMAIL_ALERTS_ENABLED=true
Setting Default Notes
SMTP_HOST (empty) Your SMTP server.
SMTP_PORT 587 The submission port with STARTTLS. Port 465 (implicit TLS) isn’t supported; most providers also offer 587, and some 2525.
SMTP_USER, SMTP_PASSWORD (empty) Leave the user empty for a relay without login. Gmail needs an app password, not the account password.
SMTP_FROM (empty) The sender. Use an address your provider lets you send from, or messages get rejected or land in spam.
SMTP_TLS true Upgrade the connection with STARTTLS before logging in. Set false only for a trusted relay on a private network.
EMAIL_ALERTS_ENABLED true false turns off alert emails only; invitations and password resets still send.

Common providers:

Provider SMTP_HOST Port
Gmail / Google Workspace smtp.gmail.com 587 (with an app password)
Brevo smtp-relay.brevo.com 587
Mailgun smtp.mailgun.org 587
SMTP2GO mail.smtp2go.com 587 or 2525

Set the SMTP values on the backend and the workers: the backend sends invitations and password resets, and the workers send alerts. On Docker Compose, .env reaches both. Recreate the containers after changing it with docker compose up -d.

Email Sent by When
Email confirmation backend Right after someone registers with a password, and again when they ask for a new link. The link is valid for 72 hours.
Project invitation backend An admin invites someone in Project → Settings → Members. The invitation is also recorded in the app, and attaches once the person’s address is confirmed.
Password reset backend Someone uses Forgot password? on the sign-in page.
Failure and flaky alerts worker A run finishes and the project’s notification settings ask for it, sent to the project’s alert email addresses. Set them up in Alerts.

To check delivery, use Send test email in a project’s notification settings.

Anyone can register any address with a password, so an address only counts once its owner has proven they receive mail there. A confirmed address is what lets pending project invitations attach and lets Google or GitHub sign-in link to an existing account.

An address is confirmed by any of these:

  • The confirmation link emailed after registration (needs SMTP). The link is valid for 72 hours. A signed-in user whose address isn’t confirmed sees Not confirmed under Settings → Profile, with a button that sends a new link (POST /api/auth/verify-email/send, at most 5 times an hour). Opening the link shows Email confirmed in the app.
  • Signing in with Google or GitHub with that address, which the provider has verified.
  • Completing a password reset, which proves access to the mailbox.

How invitations behave as a result:

The invited address belongs to… What happens
A confirmed account The person is added to the project straight away.
An unconfirmed account A pending invitation. The email asks them to confirm the address; it attaches once they do.
No account yet A pending invitation. It attaches when they register and confirm, or sign in with Google or GitHub.

Pending invitations carry an Invited badge in the members list. The members API also says why each one is waiting: pending_reason is email_unverified or no_account.

Password reset only works with SMTP, by design: the reset link is never shown in the app and never written to a log. Without SMTP, the forgot-password page says so.

The reset-password page saying this server can't send email yet and asking the user to contact whoever runs Plainwright

How reset links behave:

  • One-time and short-lived. A link is valid for PASSWORD_RESET_TTL_MINUTES (default 60) and only once. Requesting a new link cancels older ones. Only a hash of the token is stored.
  • No account enumeration. The form always answers the same way, and the email is sent in the background, so neither the reply nor its timing reveals whether an account exists.
  • Throttled: at most one link a minute and five an hour per account, and 20 requests an hour per client address.
  • New password rules: at least 8 characters, at most 72 bytes (the bcrypt limit), and not the email address or the part before the @. The same rules apply at registration and when changing a password.
  • Completing a reset signs out every other session and confirms the email address, so pending invitations attach.

Without SMTP, an operator can still help a locked-out user by having them sign in with Google or GitHub (if configured), since that links to the same email.

Symptom Likely cause
No Google or GitHub button Only one of the id and secret is set, or the backend wasn’t recreated after editing .env. GET /api/oauth/providers shows what the server sees.
Google shows redirect_uri_mismatch, or GitHub says the redirect URI doesn’t match the registered callback URL The registered callback URL differs from <base>/api/oauth/<provider>/callback. Check scheme, host, port and OAUTH_REDIRECT_BASE.
“Sign-in was started in a different browser session” The sign-in cookie didn’t come back: the callback ran on a different host than /start, or the browser blocks cookies. Use one origin throughout.
“Sign-in link expired or is invalid” or “This sign-in link was already used” The one-time code is older than 2 minutes, or the callback page was reloaded. Start again from the sign-in page.
“An account with this email already exists, but its email address hasn’t been confirmed…” Sign in with the password, confirm the address (or use Forgot password?), then use the provider again. See Confirming email addresses.
“Your GitHub account has no verified email address” Verify an email on GitHub (Settings → Emails).
An invited person doesn’t see the project Their address isn’t confirmed yet. See Confirming email addresses.
“Too many attempts — wait a few minutes and try again.” (HTTP 429) The sign-in or registration throttle. See Rate limits.
After an invite the app says “Email isn’t set up on this server”, or no reset or confirmation emails arrive SMTP isn’t configured on the backend, or the server refused the login. Try Send test email, and check SMTP_PORT (587, not 465) and the app password.
Alert emails missing but invitations work The workers don’t have the SMTP settings, EMAIL_ALERTS_ENABLED=false, or the project has no alert addresses.