Ruby · Password breach checks
Check passwords against breach data in Ruby
Checking a password from Ruby takes one request to /v1/breaches/password with the password in the JSON body. Finowl hashes it with SHA-1 and searches by a five-character hash prefix using a k-anonymity range search with padding, and the password is processed transiently: never logged, stored or returned. The response tells you whether it was found and how many times through the breached and count fields.
The example reads the password with IO#noecho so it never appears on screen, and its errors describe the problem without repeating the value. In a Rails app using has_secure_password, the same method fits a custom validate method on the user model that adds an error when the count is positive. Keep the call out of loops over existing users and check passwords only when someone sets or changes one.
Steps
- 1
Export FINOWL_API_KEY in the environment where the script or app server runs.
- 2
Save the script as password_check.rb and run it with ruby; the prompt hides what you type.
- 3
Call password_breach_count from your sign-up or password-change flow before you hash and store the password.
- 4
Show a clear message asking for a different password when the count is above zero.
- 5
Decide whether a timeout or server error should accept the password or ask the user to try again.
require "io/console"
require "json"
require "net/http"
PASSWORD_ENDPOINT = URI("https://api.finowl.co/v1/breaches/password")
# Returns how many times the password appears in breach data (0 when never seen).
def password_breach_count(password, attempts: 3)
attempts.times do
response = Net::HTTP.start(PASSWORD_ENDPOINT.host, PASSWORD_ENDPOINT.port, use_ssl: true,
open_timeout: 5, read_timeout: 10) do |http|
request = Net::HTTP::Post.new(PASSWORD_ENDPOINT)
request["Authorization"] = "Bearer #{ENV.fetch("FINOWL_API_KEY")}"
request["Content-Type"] = "application/json"
request.body = JSON.generate(password: password)
http.request(request)
end
case response
when Net::HTTPSuccess
return JSON.parse(response.body).fetch("count")
when Net::HTTPBadRequest
# Never echo the password itself in errors or logs.
raise ArgumentError, "Password is empty or longer than 4096 characters"
when Net::HTTPUnauthorized
raise "FINOWL_API_KEY is missing or invalid"
when Net::HTTPTooManyRequests
code = JSON.parse(response.body).dig("error", "code")
raise "Finowl credits are used up" if code == "quota_exceeded"
sleep [response["Retry-After"].to_i, 1].max
else
raise "Finowl returned HTTP #{response.code}; not retrying"
end
end
raise "Still rate limited after #{attempts} attempts"
end
print "Choose a password: "
password = $stdin.noecho(&:gets).to_s.chomp
puts
count = password_breach_count(password)
if count.positive?
puts "That password has been seen #{count} times in breach data. Pick another."
else
puts "Not found in known breach data."
endIN PRODUCTION
Production tips
Keep password in Rails.application.config.filter_parameters, which new Rails apps include by default, so request logs never contain it, and never interpolate the value into exception messages.
Check the plain password only when it is created or changed. Stored bcrypt digests cannot be checked later, so there is no backfill for existing accounts beyond prompting a password change.
Pair the breach check with a minimum length rule. NIST SP 800-63B recommends both, and the length check costs nothing, so reject short passwords before you spend a credit on them.
Each lookup costs 1 credit, with 100 free credits every month. See pricing and error codes.
A FEW GOOD QUESTIONS
Common questions
Related guides
- Check email breaches in Ruby with net/http
- Predict gender from first names in Ruby
- Check breached passwords in Python at signup
- Screen breached passwords in Node.js
- Check breached passwords in TypeScript
- Check breached passwords in Go before storing them
- Breached password checks for fintech accounts
- Breached password screening for e-commerce accounts
- Breached password checks for SaaS signup and resets
Check passwords against breach data in Ruby
Grab an API key, paste the example and run it. 100 free credits every month.