fullstackhero/dotnet-starter-kit · error · CustomException

Email claim is required for external authentication.

Error message

Email claim is required for external authentication.

What it means

Thrown as a CustomException by ExtractEmailFromPrincipal when an external authentication principal (Google, Azure AD, etc.) carries no email claim. The method tries ClaimTypes.Email then the raw 'email' claim and throws if both are absent, because the service creates/links the local user by email. Some providers omit email unless a scope is explicitly requested.

Solutions

  1. Request the 'email' (and 'profile') scope in the external provider options
  2. Read the email from userinfo endpoint or map an alternative claim (e.g. preferred_username / upn) via ClaimActions
  3. Ask the user to add/verify an email on the provider account, or fall back to an email-entry screen
  4. Check JwtSecurityTokenHandler's InboundClaimTypeMap isn't renaming the claim unexpectedly

Example fix

// before
options.Scope.Add("openid");
// after
options.Scope.Add("openid");
options.Scope.Add("email");
options.Scope.Add("profile");
options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
Defensive patterns

Strategy: validation

Validate before calling

var email = principal.FindFirstValue(ClaimTypes.Email) ?? principal.FindFirstValue("email"); if (string.IsNullOrWhiteSpace(email)) throw new InvalidOperationException("External provider did not return an email claim.");

Type guard

bool HasEmailClaim(ClaimsPrincipal p) => !string.IsNullOrWhiteSpace(p.FindFirstValue(ClaimTypes.Email) ?? p.FindFirstValue("email"));

Try / catch

catch (CustomException ex) when (ex.Message.Contains("Email claim")) { return Results.Redirect("/collect-email?provider=external"); }

Prevention

When it happens

Trigger: External login callback where the ID token / userinfo response has no email claim; provider scopes missing 'email'; provider account has no verified email and the provider hides it; claim type mapping stripped the claim.

Common situations: Google sign-in without the email scope; Azure AD with email claim absent for guest accounts; custom OIDC provider not returning email; ClaimActions in JwtBearer/OIDC options deleting the email claim during mapping.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


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

Appendix: source

Thrown at src/Modules/Identity/Modules.Identity/Services/UserRegistrationService.cs:169

        return result.Succeeded
            ? string.Format(CultureInfo.InvariantCulture, "Phone number {0} confirmed successfully.", user.PhoneNumber)
            : throw new CustomException(string.Format(CultureInfo.InvariantCulture, "An error occurred while confirming phone number {0}", user.PhoneNumber));
    }

    private void EnsureValidTenant()
    {
        if (string.IsNullOrWhiteSpace(multiTenantContextAccessor?.MultiTenantContext?.TenantInfo?.Id))
        {
            throw new UnauthorizedException("invalid tenant");
        }
    }

    private static string ExtractEmailFromPrincipal(ClaimsPrincipal principal)
    {
        return principal.FindFirstValue(ClaimTypes.Email)
            ?? principal.FindFirstValue("email")
            ?? throw new CustomException("Email claim is required for external authentication.");
    }

    private async Task<FshUser> CreateUserFromPrincipalAsync(ClaimsPrincipal principal, string email)
    {
        var (firstName, lastName, userName) = ExtractUserInfoFromPrincipal(principal, email);

        userName = await EnsureUniqueUserNameAsync(userName);

        var user = new FshUser
        {
            Email = email,
            UserName = userName,
            FirstName = firstName,
            LastName = lastName,
            EmailConfirmed = true,
            PhoneNumberConfirmed = false,
            IsActive = true
        };

View on GitHub (pinned to 3f2959e683)