{"record":{"id":"7d190036b2e9d2c1","repo":"toeverything/AFFiNE","slug":"wrong-sign-in-credentials-7d1900","errorCode":"wrong_sign_in_credentials","errorMessage":"Wrong user email or password: ${email}","messagePattern":"Wrong user email or password: (.+?)","errorType":"exception","errorClass":"WrongSignInCredentials","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/core/auth/magic-link.ts","lineNumber":62,"sourceCode":"\n    const callbackUrlObj = this.url.url(callbackUrl);\n    const redirectUriInCallback =\n      callbackUrlObj.searchParams.get('redirect_uri');\n    if (\n      redirectUriInCallback &&\n      !this.url.isAllowedRedirectUri(redirectUriInCallback)\n    ) {\n      throw new ActionForbidden();\n    }\n\n    const user = await this.models.user.getUserByEmail(email, {\n      withDisabled: true,\n    });\n\n    if (!user) {\n      await this.assertSignupAllowed(email);\n    } else if (user.disabled) {\n      throw new WrongSignInCredentials({ email });\n    }\n\n    const ttlInSec = 30 * 60;\n    const { token, expiresAt: tokenExpiresAt } =\n      await this.models.verificationToken.createWithExpiresAt(\n        TokenType.SignIn,\n        email,\n        ttlInSec\n      );\n\n    const otp = this.crypto.otp();\n    const { expiresAt: otpExpiresAt } = await this.models.magicLinkOtp.upsert(\n      email,\n      otp,\n      token,\n      clientNonce\n    );\n","sourceCodeStart":44,"sourceCodeEnd":80,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/auth/magic-link.ts#L44-L80","documentation":"During magic-link send, a user matching the email is found but has disabled=true in the users table. Rather than reveal the account is disabled, the server throws WrongSignInCredentials with the email — the same shape used for invalid credentials — to avoid account enumeration. Category maps to wrong_sign_in_credentials.","triggerScenarios":"Calling MagicLinkAuthService.send with an email whose user row exists and disabled=true (set by an admin via disableUser or equivalent). The check happens after callback/redirect validation and after confirming the user exists with withDisabled:true.","commonSituations":"An admin suspended the user but they still try to sign in via magic link. A disabled account leftover from a data import. Self-hosted ops flipped the disabled flag manually.","solutions":["Have an admin re-enable the account (clear the disabled flag) if access should be restored.","If the user should not have access, treat this as expected and show a generic 'wrong credentials' message to the user.","Do not attempt to sign up a new account with the same email; the existing disabled record blocks it.","Audit admin actions / the users table to confirm the disabled flag's intended state."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isWrongSignInCredentials(err: unknown): boolean {\n  return (\n    !!err &&\n    typeof err === 'object' &&\n    (err as { code?: string }).code === 'wrong_sign_in_credentials'\n  );\n}","tryCatchPattern":"try {\n  await magicLink.send(email);\n} catch (err) {\n  if (isWrongSignInCredentials(err)) {\n    showGenericError('Wrong email or credentials.');\n    return;\n  }\n  throw err;\n}","preventionTips":["Treat wrong_sign_in_credentials as a generic message; do not reveal the account is disabled.","Surface a clear admin contact path so disabled users can request re-enablement.","Audit disabled-flag changes in admin tooling.","Do not allow re-registration of a disabled account's email."],"tags":["auth","magic-link","account-state","disabled-account","anti-enumeration"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}