{"record":{"id":"d3c4b4c5cbe56e1f","repo":"Freika/dawarich","slug":"user-not-found","errorCode":null,"errorMessage":"user not found","messagePattern":"user not found","errorType":"http","errorClass":"Auth::VerifyOtpChallengeToken::InvalidToken","httpStatus":401,"severity":"error","filePath":"app/services/auth/verify_otp_challenge_token.rb","lineNumber":29,"sourceCode":"      @token = token\n    end\n\n    def call\n      raise InvalidToken, 'blank token' if @token.blank?\n\n      decoded, = JWT.decode(@token, Auth::InternalTokenSecret.call, true, algorithm: 'HS256')\n      raise InvalidToken, 'wrong purpose' unless decoded['purpose'] == 'otp_challenge'\n      raise InvalidToken, 'missing jti' if decoded['jti'].blank?\n\n      if decoded['iat'].present? &&\n         (Time.now.to_i - decoded['iat'].to_i) > Auth::IssueOtpChallengeToken::TTL.to_i\n        raise InvalidToken, 'token too old'\n      end\n\n      raise TokenReplayed, 'token already consumed' if token_consumed?(decoded['jti'])\n\n      user = User.find_by(id: decoded['user_id'])\n      raise InvalidToken, 'user not found' unless user\n\n      @jti = decoded['jti']\n      user\n    rescue JWT::DecodeError => e\n      raise InvalidToken, e.message\n    end\n\n    def mark_consumed!\n      return false if @jti.blank?\n\n      Rails.cache.write(\n        \"#{CONSUMED_KEY_PREFIX}#{@jti}\",\n        true,\n        expires_in: Auth::IssueOtpChallengeToken::TTL,\n        unless_exist: true\n      )\n    end\n","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/Freika/dawarich/blob/97fad417c5a11b0eb11157890635e015723a2e97/app/services/auth/verify_otp_challenge_token.rb#L11-L47","documentation":"Raised by Auth::VerifyOtpChallengeToken#call when the JWT is fully valid (signature, purpose, jti, freshness) but User.find_by(id: decoded['user_id']) returns nil. The token references a user that no longer exists in the database — typically a deleted account or a token minted against a different database/environment.","triggerScenarios":"User requested an OTP, then their account was deleted (GDPR erasure, admin cleanup) before submitting the code; a token from a staging environment replayed against production whose user IDs differ; test fixtures that sign tokens for user IDs that were never created.","commonSituations":"Account deletion mid-flow, database resets/restores while challenges are in flight, shared secrets across environments (Auth::InternalTokenSecret identical in staging and prod) letting cross-environment tokens verify, seeds changed between issue and verify.","solutions":["Treat it as terminal for that token: clear the client-side challenge and ask the user to restart authentication (account no longer exists).","If it happens after a database restore/reset, note that in-flight tokens are invalidated by design — let them age out.","If it appears in production for real users, check whether an account-deletion job ran and whether other flows reference deleted users.","Ensure each environment has its own Auth::InternalTokenSecret so tokens cannot cross environments."],"exampleFix":"# before\nrescue Auth::VerifyOtpChallengeToken::InvalidToken => e\n  render json: { error: e.message }, status: :unauthorized # opaque 'user not found'\n\n# after: translate to user-facing restart\nrescue Auth::VerifyOtpChallengeToken::InvalidToken => e\n  if e.message == 'user not found'\n    render json: { error: 'Account not found. Please sign up again.' }, status: :not_found\n  else\n    render json: { error: e.message }, status: :unauthorized\n  end","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"begin\n  user = Auth::VerifyOtpChallengeToken.new(token).call\nrescue Auth::VerifyOtpChallengeToken::InvalidToken => e\n  if e.message == 'user not found'\n    render json: { error: 'Account no longer exists' }, status: :not_found\n  else\n    render json: { error: e.message }, status: :unauthorized\n  end\nend","preventionTips":["Run account deletion with awareness of in-flight auth flows (or accept these errors as expected noise right after deletions).","Give each environment its own Auth::InternalTokenSecret so tokens cannot verify across databases.","Alert if this error spikes without corresponding deletions — it can indicate env mismatch."],"tags":["authentication","user-lookup","jwt","otp","ruby"],"backgroundTag":"user-not-found","analyzedSha":"97fad417c5a11b0eb11157890635e015723a2e97","analyzedAt":"2026-08-21T17:04:17.778Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}