{"record":{"id":"17dc9feea593f2d6","repo":"fullstackhero/dotnet-starter-kit","slug":"email-claim-is-required-for-external-authentication","errorCode":null,"errorMessage":"Email claim is required for external authentication.","messagePattern":"Email claim is required for external authentication\\.","errorType":"exception","errorClass":"CustomException","httpStatus":400,"severity":"error","filePath":"src/Modules/Identity/Modules.Identity/Services/UserRegistrationService.cs","lineNumber":169,"sourceCode":"\n        return result.Succeeded\n            ? string.Format(CultureInfo.InvariantCulture, \"Phone number {0} confirmed successfully.\", user.PhoneNumber)\n            : throw new CustomException(string.Format(CultureInfo.InvariantCulture, \"An error occurred while confirming phone number {0}\", user.PhoneNumber));\n    }\n\n    private void EnsureValidTenant()\n    {\n        if (string.IsNullOrWhiteSpace(multiTenantContextAccessor?.MultiTenantContext?.TenantInfo?.Id))\n        {\n            throw new UnauthorizedException(\"invalid tenant\");\n        }\n    }\n\n    private static string ExtractEmailFromPrincipal(ClaimsPrincipal principal)\n    {\n        return principal.FindFirstValue(ClaimTypes.Email)\n            ?? principal.FindFirstValue(\"email\")\n            ?? throw new CustomException(\"Email claim is required for external authentication.\");\n    }\n\n    private async Task<FshUser> CreateUserFromPrincipalAsync(ClaimsPrincipal principal, string email)\n    {\n        var (firstName, lastName, userName) = ExtractUserInfoFromPrincipal(principal, email);\n\n        userName = await EnsureUniqueUserNameAsync(userName);\n\n        var user = new FshUser\n        {\n            Email = email,\n            UserName = userName,\n            FirstName = firstName,\n            LastName = lastName,\n            EmailConfirmed = true,\n            PhoneNumberConfirmed = false,\n            IsActive = true\n        };","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Services/UserRegistrationService.cs#L151-L187","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Request the 'email' (and 'profile') scope in the external provider options","Read the email from userinfo endpoint or map an alternative claim (e.g. preferred_username / upn) via ClaimActions","Ask the user to add/verify an email on the provider account, or fall back to an email-entry screen","Check JwtSecurityTokenHandler's InboundClaimTypeMap isn't renaming the claim unexpectedly"],"exampleFix":"// before\noptions.Scope.Add(\"openid\");\n// after\noptions.Scope.Add(\"openid\");\noptions.Scope.Add(\"email\");\noptions.Scope.Add(\"profile\");\noptions.ClaimActions.MapJsonKey(ClaimTypes.Email, \"email\");","handlingStrategy":"validation","validationCode":"var email = principal.FindFirstValue(ClaimTypes.Email) ?? principal.FindFirstValue(\"email\"); if (string.IsNullOrWhiteSpace(email)) throw new InvalidOperationException(\"External provider did not return an email claim.\");","typeGuard":"bool HasEmailClaim(ClaimsPrincipal p) => !string.IsNullOrWhiteSpace(p.FindFirstValue(ClaimTypes.Email) ?? p.FindFirstValue(\"email\"));","tryCatchPattern":"catch (CustomException ex) when (ex.Message.Contains(\"Email claim\")) { return Results.Redirect(\"/collect-email?provider=external\"); }","preventionTips":["Always request 'email' and 'profile' scopes on external providers","Inspect the raw ID token/userinfo payload once per provider during setup","Check OIDC ClaimActions aren't stripping the email claim","Plan a fallback email-entry screen for providers that omit email"],"tags":["oauth","external-auth","claims","oidc"],"backgroundTag":"missing-required-argument","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}