fullstackhero/dotnet-starter-kit · error · UnauthorizedException

email not confirmed

Error message

email not confirmed

What it means

ValidateUserStatus throws UnauthorizedException("email not confirmed") when the user account exists and is active but EmailConfirmed is false. ASP.NET Identity's confirmation flag gates token issuance here, so unconfirmed accounts cannot log in or refresh.

Solutions

  1. Confirm the email: resend the confirmation link (SendConfirmEmail/forgot-password style flow) and complete it.
  2. Manually set EmailConfirmed=true for trusted/dev accounts (admin update or DB).
  3. Fix SMTP/email provider configuration and check logs if confirmation mails are not being delivered.

Example fix

// before (seed user cannot log in)
new FshUser { Email = "test@root.dev", EmailConfirmed = false }
// after
new FshUser { Email = "test@root.dev", EmailConfirmed = true }
Defensive patterns

Strategy: try-catch

Validate before calling

// app state knows whether the signup flow finished confirmation
if (signupState.awaitingEmailConfirmation) {
  showResendConfirmationUi();
  return; // skip login attempt
}

Try / catch

catch (ApiError e) when (e.StatusCode === 401 && e.Message.includes('email not confirmed')) {
  await api.post('/api/users/self/confirm-email/resend');
  show('Check your inbox to confirm your email, then sign in.');
}

Prevention

When it happens

Trigger: Login/refresh for a user who signed up but never clicked the confirmation link; SMTP delivery failed so the confirmation email never arrived; a user created by an admin/seed without EmailConfirmed=true.

Common situations: Misconfigured email provider (SendGrid/Mailgun key invalid) silently dropping confirmation mails; dev environments without email infrastructure; tests seeding users with EmailConfirmed=false; user's mail client spam-filtering the message.

Related errors


AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15). Data as JSON: /api/errors/fa6d785265592804. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Identity/Modules.Identity/Services/IdentityService.cs:273

        if (user.RefreshTokenExpiryTime <= now)
        {
            _logger.LogWarning(
                "Refresh token expired for user {UserId}. Expired at: {ExpiryTime}, Current time: {CurrentTime}",
                user.Id, user.RefreshTokenExpiryTime, now);
            throw new UnauthorizedException("refresh token is invalid or expired");
        }
    }

    private static void ValidateUserStatus(FshUser user)
    {
        if (!user.IsActive)
        {
            throw new UnauthorizedException("user is deactivated");
        }

        if (!user.EmailConfirmed)
        {
            throw new UnauthorizedException("email not confirmed");
        }
    }

    private void ValidateTenantStatus(AppTenantInfo tenant)
    {
        if (tenant.Id == MultitenancyConstants.Root.Id)
        {
            return;
        }

        if (!tenant.IsActive)
        {
            throw new UnauthorizedException($"tenant {tenant.Id} is deactivated");
        }

        // Honor the billing grace period: a lapsed tenant can still authenticate until
        // ValidUpto + grace (matching the request-time guard in MultitenancyModule).
        if (_timeProvider.GetUtcNow().UtcDateTime > tenant.ValidUpto.AddDays(_gracePeriodDays))

View on GitHub (pinned to 3f2959e683)