{"record":{"id":"54a3562fdcee7279","repo":"sahat/hackathon-starter","slug":"email-required","errorCode":"EMAIL_REQUIRED","errorMessage":"EMAIL_REQUIRED","messagePattern":"EMAIL_REQUIRED","errorType":"error_code","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"config/passport.js","lineNumber":123,"sourceCode":"      if (user.profile.pictureSource === 'gravatar') {\n        user.profile.picture = providerProfile.picture;\n        user.profile.pictureSource = providerName;\n      }\n    }\n\n    user.profile.location = user.profile.location || providerProfile.location;\n    user.profile.website = user.profile.website || providerProfile.website;\n    await user.save();\n    return user;\n  }\n  // User is not logged in:\n  const existingUser = await User.findOne({ [providerName]: { $eq: providerProfile.id } });\n  if (existingUser) {\n    return existingUser;\n  }\n  const normalizedEmail = providerProfile.email ? validator.normalizeEmail(providerProfile.email, { gmail_remove_dots: false }) : undefined;\n  if (!normalizedEmail) {\n    throw new Error('EMAIL_REQUIRED');\n  }\n  const existingEmailUser = await User.findOne({\n    email: { $eq: normalizedEmail },\n  });\n  if (existingEmailUser) {\n    throw new Error('EMAIL_COLLISION');\n  }\n  const user = new User();\n  user.email = normalizedEmail;\n  user[providerName] = providerProfile.id;\n  req.user = user;\n  if (oauth2provider) {\n    await saveOAuth2UserTokens(req, accessToken, refreshToken, params.expires_in, refreshTokenExpiration, providerName, tokenConfig);\n  } else {\n    user.tokens.push({ kind: providerName, accessToken, ...(tokenSecret && { tokenSecret }) });\n  }\n  user.profile.name = providerProfile.name;\n  user.profile.gender = providerProfile.gender;","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/sahat/hackathon-starter/blob/c12e339564db59443df0b901818c9bb661331b28/config/passport.js#L105-L141","documentation":"Thrown when a new-user OAuth signup provides no usable email. The strategy normalizes providerProfile.email with validator.normalizeEmail; if the provider returned no email (or it normalizes to false/undefined), the app cannot create a local account because email is required.","triggerScenarios":"First-time sign-in with a provider that does not return an email (some X/Twitter apps, Steam, private GitHub accounts), so providerProfile.email is undefined and normalizedEmail is undefined.","commonSituations":"X (Twitter) app created with 'request email' scope missing, GitHub user with no public email and no user:email scope, or a provider API change that stopped returning the email field.","solutions":["Add the email scope to the passport strategy options (e.g. scope: ['user:email'] for GitHub, include_email:true for X)","Prevent sign-in-with-email-less providers; restrict them to account linking only (sessionAlreadyLoggedIn path)","Fork a custom strategy callback that prompts the user for an email before creating the account"],"exampleFix":"// before\npassport.use(new GitHubStrategy({ clientID, clientSecret, scope: ['profile'] }, ...));\n// after\npassport.use(new GitHubStrategy({ clientID, clientSecret, scope: ['user:email'] }, ...));","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"const hasEmail = (p) => typeof p?.email === 'string' && p.email.includes('@');","tryCatchPattern":"catch (e) { if (e.message === 'EMAIL_REQUIRED') { req.flash('errors', {msg:'Provider did not share an email. Sign up locally, then link this provider.'}); return res.redirect('/signup'); } throw e; }","preventionTips":["Request email scopes for every provider strategy","Restrict email-less providers to account linking only"],"tags":["oauth","email","signup","validation"],"backgroundTag":"oauth-provider-missing-email","analyzedSha":"c12e339564db59443df0b901818c9bb661331b28","analyzedAt":"2026-08-27T11:05:00.872Z","schemaVersion":2},"datasetVersion":"2026-08-27T13:17:12.746Z"}