Sign-in, email, and OAuth
Diagnose authentication issues through sign-in state, email verification, email delivery, and OAuth callbacks.
Failed sign-in, missing emails, and failed third-party callbacks often occur within the same workflow, but require different fixes. Find the failed request in the browser's Network panel first, then inspect its response body and server logs. Do not diagnose the issue solely from the page where the browser ends up.
1. Sign-in redirects back to the sign-in page
Inspect two requests in order: the sign-in response, then the first request that requires authentication after sign-in.
- Did sign-in succeed, or does the response require further email or two-factor verification?
- Did the response set a cookie, and did the browser report rejecting it?
- Does the next request include the cookie, and do its host and protocol match those used for sign-in?
- Can the server read a valid session, and does the user's status still allow access?
Do not mix localhost and 127.0.0.1 locally. In production, do not sign in through workers.dev and then switch to a custom domain to continue testing. Cookies are not automatically shared across hosts.
In production, also check VITE_SITE_URL, HTTPS, whether a proxy rewrites response headers, and whether the browser is actually visiting the old site. If you suspect old cookies, reproduce the issue in a new browser session. However, “clearing cookies fixed it” does not replace investigating the domain or session problem.
Do not copy raw cookie values into logs or support tickets. To correlate a session, inspect its expiration and associated user in saas_session within a controlled environment.
2. Distinguish 401, 403, and 429
| Status | Common cause | What else to inspect |
|---|---|---|
| 401 | No valid sign-in session or token | Cookies, session expiration, and the endpoint's authentication method |
| 403 | Verification status, permissions, account status, or request origin does not meet requirements | code, error, details, and middleware logs |
| 429 | The request was rate-limited | The triggering operation, any required wait indicated in the response, and rate limit configuration |
A 403 does not always prove that the visitor is signed in. Origin checks or security policies may reject a request before application processing begins. Nor does every 403 mean that an administrator role is missing.
If the response includes details.redirectUrl, check whether it points to email verification, two-factor authentication, or another flow. Do not fix authentication issues by removing guards or granting administrator roles to regular users.
Some authentication blocking and throttling middleware is mounted only in production mode. Repeated successful attempts locally do not mean that production limits will not apply. Continuous retries may also prolong troubleshooting. Stop repeated operations before checking the current configuration.
3. Why email verification is required after signup
The current template defaults auth.emailVerification.defaultRequired to false, so it does not require every new user to verify their email first. If a user is asked to verify, check whether your project changed this setting or whether they are performing an account operation with its own verification requirement.
Email verification involves a verification session, email content, and the current browser session. If multiple emails arrive, use the latest one generated by the current operation first. Do not mix codes from separate signup, email change, or password reset flows.
If a code is reported as invalid or expired, check the corresponding verification session and timestamps instead of manually marking the user's email as verified. Password resets may also require two-factor verification. Successful email verification does not mean the entire reset flow is complete.
4. Local email sending succeeds, but no email arrives
The current npm run dev runs in development mode, where the email component prints this marker in the terminal:
[DEV EMAIL PREVIEW]The preview includes the recipient, subject, and body, and the send result uses a development preview ID. No real email service is called. Even if .env contains a Resend key, inbox delivery cannot tell you whether this development flow succeeded.
Find the preview in the local terminal and use its verification information to complete the flow. It may contain verification codes and links. Do not upload an unredacted screenshot of the terminal.
npm run preview uses a bundled build. Do not assume that development-mode email simulation applies to every preview or production runtime. To test real delivery, use the relevant environment with complete configuration and confirm delivery on the provider's side.
5. Email sending fails in production
Run npm run doctor:remote first, then inspect the actual failed request. The diagnostic script reads local .env.production, so it cannot prove that the deployed Worker has loaded this latest configuration.
The current template uses Resend by default:
| Location | What to check |
|---|---|
config/deploy.ts | Whether emailProvider.type is resend |
.env.production | Whether RESEND_API_KEY is set and belongs to the intended account |
config/base.ts | The actual sender address, fromEmailAddress.email, and reply address, supportEmail |
| Resend dashboard | Whether the sending domain is verified, the key has permission to send, and delivery has failed |
| Deployed Worker | Whether an update deployment was run after changing the secret |
If you switch to Cloudflare Email, check the EMAIL binding of type send_email. If allowed_sender_addresses is configured, it must include the actual sender address. Also check recipient restrictions in the current email service configuration. Adding a binding does not necessarily allow sending to any address.
If only one kind of email fails, inspect its registration in src/libs/email/template/factory.ts, the data passed to it, and the localized text. Configuration errors, template rendering errors, provider rejections, and eventual bounces occur at different stages.
If no email arrives after the provider accepts the request, inspect delivery status, bounces, and spam folders. A successful send result from the application only means that the current sending call succeeded. It does not guarantee that the email reached the inbox.
6. emailSendTooOften
The configuration is at deploy.spam.resourceProtection.emailSendService, with a default window of 1d and a limit of 5. Although the field is named maxEmailsPerUser, the current protected sending flow generates its counter key from the recipient's email address.
This is a quota window. Do not assume it resets at midnight in the recipient's time zone. In development mode, quota issues produce a log message. In production, they may prevent further sends.
Check for repeated clicks, automatic frontend retries, or multiple flows repeatedly sending to the same address. Stop repeated calls before evaluating the configuration against your requirements. Do not clear production counters or raise the limit indefinitely just to pass a test.
7. Invalid two-factor or recovery codes
First confirm that the account has completed two-factor authentication setup. Then check the authenticator device's clock, the account entry being used, and whether the code came from an old setup.
Recovery codes are generally designed for one-time use. If a user submits a code that has already been used, they should use a remaining recovery method. Do not modify the database to mark the code as unused again.
If two-factor authentication data cannot be decrypted for multiple users at the same time, check whether SAAS_SECRET changed during deployment. It also protects OAuth client secrets and other encrypted data. Restore the project's original configuration and assess the impact instead of generating another secret.
8. An administrator email is configured, but dashboard access is denied
adminEmails in config/base.ts does not automatically make a user an administrator every time they sign in.
The current UserEmailVerifiedEvent creates an administrator role grant Command only during the site's account email verification flow, based on an exact match between the verified address and the list. Password reset email verification does not grant this role, and a third-party OAuth provider reporting a verified email cannot replace this flow.
Check, in order, that the list matches the verified address, the site's account verification was completed, and the related Event and grant_role Command succeeded. Removing an email from the configuration list does not automatically revoke an already granted role. Revoke it through the appropriate access management flow.
9. Google or GitHub sign-in fails
Third-party sign-in is configured separately from the application's own OAuth service. Google and GitHub sign-in use these callbacks:
| Provider | Callback URL |
|---|---|
{VITE_SITE_URL}/api/auth/oauth2/google | |
| GitHub | {VITE_SITE_URL}/api/auth/oauth2/github |
For a callback mismatch, compare the redirect_uri actually sent by the browser with the value in the provider's dashboard, including protocol, host, port, and path. Do not rely only on an address in .env that looks correct.
If a button is missing or initialization fails, check that both the corresponding client ID and client secret are set. Google One Tap also requires auth.enableGoogleOneTap, which is disabled by default in the template.
If state validation fails after the callback, check whether sign-in started and finished in the same browser session at the same site URL, and whether an authorization link from an old tab was used. After changing domains, update both the provider's callback and the application configuration.
For provider setup instructions, see Google OAuth and GitHub OAuth.
10. Authorization fails in the built-in OAuth 2.0 service
The built-in service uses /oauth/*, and its clients are managed within this application. You cannot use a Google or GitHub client secret to request tokens from this application.
Check the client's status, redirect URI, allowed grant types, requested scopes, and PKCE parameters. The template requires S256 by default. The challenge in the authorization request and the verifier used to exchange the code for a token must come from the same authorization flow.
An authorization code cannot be reused as a long-term credential. To troubleshoot a failed token exchange, start a new, complete authorization flow. Record the redacted protocol error name without logging raw authorization codes, client secrets, or tokens.
If the authorization page prompts for an upgrade, check config/upgrade.ts. The current template's default configuration is empty. Upgrade options based on role or entitlement conditions appear only after rules are customized. Verify rule matching and application API permission checks separately.
Verify the fix
Use a regular account to sign in, sign out, and sign in again. If the fix involves email, confirm that verification status actually updates. If it involves OAuth, complete a fresh authorization flow from the initial request through the return to the application. If it involves administrator roles, also confirm that a regular account still cannot access the dashboard.