{"record":{"id":"e37dd967ca29efb7","repo":"passbolt/passbolt_api","slug":"single-sign-on-failed-email-not-provided-by-provider","errorCode":null,"errorMessage":"Single sign-on failed. Email not provided by provider.","messagePattern":"Single sign-on failed\\. Email not provided by provider\\.","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"plugins/PassboltEe/Sso/src/Service/Sso/AbstractSsoService.php","lineNumber":273,"sourceCode":"                $msg .= \"Response: {$exception->getResponseBody()}\";\n            }\n\n            Log::error($msg);\n\n            $msg = __('Single sign-on failed.') . ' ' . __('Provider error: \"{0}\"', $exception->getMessage());\n            throw new BadRequestException($msg, 400, $exception);\n        }\n\n        // Helper for developers working on new providers\n        if (!($resourceOwner instanceof SsoResourceOwnerInterface)) {\n            $msg = 'Provider must return a ResourceOwner that implements ResourceOwnerWithEmailInterface.';\n            throw new InternalErrorException($msg);\n        }\n\n        $email = $resourceOwner->getEmail();\n        if (!isset($email) || !is_string($email) || !EmailValidationRule::check($email)) {\n            $msg = __('Single sign-on failed.') . ' ' . __('Email not provided by provider.');\n            throw new BadRequestException($msg);\n        }\n\n        return $resourceOwner;\n    }\n\n    /**\n     * @param \\Passbolt\\Sso\\Utility\\OpenId\\SsoResourceOwnerInterface $resourceOwner user\n     * @param \\App\\Model\\Entity\\User $user user\n     * @return void\n     * @throws \\Cake\\Http\\Exception\\BadRequestException if the assertion failed\n     */\n    public function assertResourceOwnerAgainstUser(SsoResourceOwnerInterface $resourceOwner, User $user): void\n    {\n        if (mb_strtolower($resourceOwner->getEmail()) !== mb_strtolower($user->username)) {\n            $msg = __('Single sign-on failed.') . ' ' . __('Username mismatch.');\n            throw new BadRequestException($msg);\n        }\n    }","sourceCodeStart":255,"sourceCodeEnd":291,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/plugins/PassboltEe/Sso/src/Service/Sso/AbstractSsoService.php#L255-L291","documentation":"Passbolt's SSO service exchanges the OAuth2 authorization code with the identity provider and fetches the resource owner (user info). Before using it, it validates that the returned resource owner carries a valid email address (isset, string, and passes EmailValidationRule). If the provider response has no usable email, a BadRequestException is thrown because Passbolt identifies users by email and cannot proceed.","triggerScenarios":"The IdP's userinfo/id_token payload lacks an email claim, returns a non-string value, or an email that fails validation (e.g. empty, malformed). Happens after getAccessToken succeeds in getResourceOwner during the SSO callback (retrieve/dry-run flows).","commonSituations":"Azure AD / AD FS app registration does not grant email or User.Read scopes; the user has no email configured in the IdP directory; email claims suppressed by conditional access policies or privacy settings; custom provider integration whose resource-owner mapping omits the email field.","solutions":["Ensure the OIDC scopes requested include email (and for Azure, User.Read or email/profile) in the SSO settings form.","In the IdP admin console, verify the user account has an email and that the email claim is included in the token/userinfo response.","If using a custom provider, make its resource owner class extract and return the email from the token payload correctly.","Check the provider's token response (logged via Log::error on IdentityProviderException) to see which claims are actually returned.","Use the SSO dry-run endpoint to test the settings before saving and confirm the email is returned."],"exampleFix":"// before (IdP app config missing scopes)\nscope: 'openid'\n// after\nscope: 'openid email profile'","handlingStrategy":"validation","validationCode":"$email = $resourceOwner->getEmail();\nif (!is_string($email) || !EmailValidationRule::check($email)) {\n    // abort flow before calling passbolt SSO verify, fix IdP claims/scopes\n}","typeGuard":"function hasValidEmail(object $ro): bool {\n    return $ro instanceof SsoResourceOwnerInterface\n        && is_string($ro->getEmail())\n        && filter_var($ro->getEmail(), FILTER_VALIDATE_EMAIL) !== false;\n}","tryCatchPattern":"try {\n    $resourceOwner = $service->getResourceOwner($code);\n} catch (BadRequestException $e) {\n    if (str_contains($e->getMessage(), 'Email not provided')) {\n        // surface hint: check IdP email claim / scopes\n    }\n    throw $e;\n}","preventionTips":["Always request 'openid email profile' (and Azure User.Read) scopes in SSO settings.","Use the SSO dry-run to confirm the provider returns an email before activating settings.","Verify every user in the IdP directory has an email set.","Check the email claim is not filtered out by conditional access or privacy policies."],"tags":["sso","oauth2","email-claim","validation"],"backgroundTag":"empty-required-field","analyzedSha":"31c1bbc10f32808a607fa9bd81891e898779c0bc","analyzedAt":"2026-09-17T00:04:38.960Z","contentChangedAt":"2026-09-17T00:04:38.960Z","schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}