{"record":{"id":"eb918acc2bcb7f50","repo":"Freika/dawarich","slug":"blank-token-eb918a","errorCode":null,"errorMessage":"blank token","messagePattern":"blank token","errorType":"exception","errorClass":"Auth::VerifyOtpChallengeToken::InvalidToken","httpStatus":401,"severity":"error","filePath":"app/services/auth/verify_otp_challenge_token.rb","lineNumber":15,"sourceCode":"# frozen_string_literal: true\n\nmodule Auth\n  class VerifyOtpChallengeToken\n    class InvalidToken < StandardError; end\n    class TokenReplayed < InvalidToken; end\n\n    CONSUMED_KEY_PREFIX = 'otp_challenge:consumed:'\n\n    def initialize(token)\n      @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","sourceCodeStart":1,"sourceCodeEnd":33,"githubUrl":"https://github.com/Freika/dawarich/blob/97fad417c5a11b0eb11157890635e015723a2e97/app/services/auth/verify_otp_challenge_token.rb#L1-L33","documentation":"Raised by Auth::VerifyOtpChallengeToken#call when the token argument is nil or empty. This service verifies a short-lived HS256 JWT issued by Auth::IssueOtpChallengeToken; a blank token can never decode, so it fails before touching JWT. Surfaced as Auth::VerifyOtpChallengeToken::InvalidToken.","triggerScenarios":"Calling VerifyOtpChallengeToken.new(token).call with a missing params[:token], an email-OTP flow where the user submits the code page without the challenge token cookie/param, or a client that loses the token between issuing the challenge and verifying it.","commonSituations":"Challenge token stored in a cookie that was cleared or blocked (ITP, third-party cookie rules), form posts to the verify endpoint without a hidden token field, deep links into the OTP screen that never carried the token, test harnesses calling the verifier directly with nil.","solutions":["Check the request that reaches the verify endpoint actually carries the challenge token (hidden input, query param, or cookie) and pass it through.","Re-issue the challenge (restart the OTP request) when the token is missing instead of retrying verification.","Ensure cookies for the challenge token use SameSite/secure attributes appropriate to your flow so browsers retain them across the redirect.","In tests, generate a real token with Auth::IssueOtpChallengeToken and verify that one."],"exampleFix":"# before\nuser = Auth::VerifyOtpChallengeToken.new(params[:token]).call\n\n# after\nverifier = Auth::VerifyOtpChallengeToken.new(params[:token].to_s)\nif params[:token].blank?\n  return redirect_to new_otp_request_path, alert: 'Challenge expired - request a new code'\nend\nuser = verifier.call","handlingStrategy":"validation","validationCode":"if params[:token].blank?\n  return redirect_to new_challenge_path, alert: 'Challenge missing - request a new code'\nend","typeGuard":"def challenge_token?(t) = t.is_a?(String) && t.split('.').length == 3","tryCatchPattern":"begin\n  user = Auth::VerifyOtpChallengeToken.new(token).call\nrescue Auth::VerifyOtpChallengeToken::InvalidToken => e\n  render json: { error: e.message }, status: :unauthorized\nend","preventionTips":["Treat the challenge token as required state: render it into the OTP form (hidden field) or a first-party cookie each time a code is sent.","When it is missing, restart the flow instead of retrying verification.","Use real Auth::IssueOtpChallengeToken output in tests."],"tags":["authentication","otp","jwt","input-validation","ruby"],"backgroundTag":"missing-auth-token","analyzedSha":"97fad417c5a11b0eb11157890635e015723a2e97","analyzedAt":"2026-08-21T17:04:17.778Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}