The idea of a secret combination of alpha-numeric characters to act as a lock is much older than the digital world. The first examples date back to Mesopotamia
Much like the first Roman challenge/response passwords which were still used as late as World War II (Thunder!/Flash! was used during the Allied landing on Normandy) and beyond, our digital counterpart has the same flaw. Complexity. The evolution of the password over the last couple of decades was led by engineers' limits placed on the database field that stored the password data. You have no idea how many times I've had to update a field in databases first created in the 90s that had limits like 16 characters that were forced to downcase in clear text (not encrypted).
Passwords Suck
A password is inherently poor security because the human factor is always the weak link. People tend to use passwords that are easy to remember and therefore easy to compromise. People hate entropy and only entropy creates a password that is tougher to crack. Here, lets have xkcd explain.
As the example above clarifies, complexity ensures difficulty for common password-cracking techniques. No matter what, the usability is poor in entering a password, simple or complex, creating a mental load.
Those same engineers who thought 16 downcased un-encrypted characters were enough to store a good password then for some reason decided to encourage the common substitution of numbers and symbols for letters in the same database tables when they all were taught the rules of entropy at college. Then the bad actors upped their game. They started using something called a Beowulf cluster to make light work of their attacks. Then came the Botnet.
Password Attacks
One of the factors that the xkcd comic above pointed out, that we can mitigate is the requests allowed per second. A password string like in the first cell of the comic can be attacked for a couple of days at a rate of 1000s of queries per second.
Bad actors commonly use brute force, dictionary, and password-stuffing attacks on websites using basic authentication.
Brute force attacks are the oldest and most common, bad actors use automated scripts to try out possible passwords until the correct one works. Brute-force attacks are most successful when users have common or weak passwords, which can be “guessed” by tools in seconds. Cracking a strong password might take a few hours or days.
Like brute-force attacks, dictionary attacks are less about quantity and more about quality. In other words, instead of trying every possible combination, bad actors start with the assumption that users are likely to follow certain patterns when they create a password.
With credential stuffing bad actors take advantage of the tendency for users to reuse the same usernames and passwords for multiple accounts. Pairs of compromised usernames and passwords are added to a botnet that
automates the process of trying those credentials on multiple sites at
the same time.
Many other attack types take advantage of simple passwords and unmanaged rate limits. It is not only wise, but a requirement today to rate limit authentication, registration, and password reset endpoints no matter what technologies you're using.
Rails 7.2
Here with rails 7.2 and above you can use the rails rate limiter, it makes use of the rails cache to stop abuse of api endpoints as well as mitigating the attacks mentioned above.
Like everything rails, it just works out of the box.
to: the number of requests before the rule is acted upon
within: the amount of time to gate the request within
only: Just like every other before or after action, limits to what functions you wish
by: (not in example) by: -> { request.subdomain } Option allows you to group requests, this can offer some very powerful filtering. By default, rate limits are by unique IP address.
store: You may assign any ActiveSupport::Cache store as the target to store the rate-limiting data. Combined with rails/solid_cache you can have a highly performant rate limiter all with built-in features of Rails 8 and only adding solid_cache to Rails 7.2.
Before 7.2 you should install and configure rack-attack
Nextjs and other js frameworks
Upstash has written a great rate-limit package that uses key-value stores to keep track of limits. Of course, they want you to use their hosted Redis service, but you don't have to. If you're hosting your Nextjs app on Vercel like many, you can write a quick middleware to rate limit.
Make sure you enable vercel kv and it should be working well. You can sub in anything that provides the redis api. If you're building on other frameworks, this example is a great starting point for the frameworks middleware.
Other Measures
Knowledge The weakest link is PEBKAC. If possible, train your users to spot phishing attacks.
Software Use a password manager that creates and stores strong passwords in a local vault. I highly suggest 1Password, Apple's new iCloud Passwords is promising too. It's missing ssh key managment, however.
Multifactor Add non-sms-based multifactor support to your app. For rails, there is Ruby One Time Password library, and for next and js frameworks there are many options. Too many in my opinion.
Passwordless Slack made magic links popular and gave us a UX for relying on the users' email addresses to provide a login. Unfortunately, this method is vulnerable to password stuffing.
Passkeys Soon, passwords will go away and we will only have passkeys. Of course, that soon is like everything, it will be determined by humans and it will take forever to happen. Passkeys are the future of authentication. A person does not have to make something up. You use your face, fingerprint, or some other biometric. With some systems, the fingerprint is of your device not you. These are stored in secure places on your devices and each device has its passkeys so you don't need to figure out a way to sync them.
Password Policy One of the first things you can do to help your users stay safe is to put a policy in place that helps them create good passwords. Here is a Stimulus controller and a React component that promote the same kind of passwords as shown in the xkcd comic above.
Stimulus Controller
And here is a html.erb partial to include as your password fields. Create app/views/shared/_password_strength.html.erb
The partial is designed to work within a form, expecting a form object to be passed as a local variable. This allows it to use the correct field name and integrate smoothly with Rails form helpers.
The Stimulus controller's data attributes are applied directly to the elements, making it easy for the JavaScript to interact with the DOM.
Tailwind CSS classes are used for styling, maintaining consistency with our previous examples.
The strength meter blocks are generated using a Ruby loop, which is a more Rails-like approach than hard-coding five separate divs.
Here is an example of how to use the partial in a registration form app/views/registration/new.html.erb
React Component
This react component implements the strength evaluation the same way as the ruby.
The component takes a password prop which is the password to be evaluated.
It use the useState and useEffect hooks to manage the state of the evaluation and updates as the password changes.
The scoring and feedback are implemented as functions so it's easy to modify to match your needs.
It uses Tailwind CSS for style. Easy to remove/override/modify.
The evaluator implementation in Typescript , ./PasswordStrength.ts
To use the evaluator, include it in a form element input component like so.
Are these examples the best code in the world? Na. But they are good illustrations of how it's pretty simple, with a tiny amount of code, to help your users find better passwords.
In a future article, I will explore how Rails 7.2 and maybe Rails 8 are improving simple credentials-based authentication.