The Importance of Safe Defaults: How I Learned the Hard Way
April 24, 2025 · by Nils Caspar · 3 min read

Many years ago, when I was working at a company transitioning from a small startup into a not-so-small startup, I learned a crucial lesson about safe defaults and API design. We were using a JWT (JSON Web Token) to pass data between two services. For those who might not be familiar, JWT allows a server to create a token containing arbitrary data that a user passes between services. The token is cryptographically signed with a shared secret, which allows the receiving service to verify that the data has not been tampered with, preventing the user from altering this information.
The Code
Our implementation on the receiving service looked roughly like this (note: exact API details may vary slightly as this is from memory):
decoder = JWTDecoder(key: ENV['JWT_SECRET'])
token = request.get('token')
data = decoder.load(token)
user = load_user(data['user_id'])
...
At first glance, this code looks perfectly fine; it initializes the JWT decoder with our secret key, retrieves the token from the request, decodes the data, and then uses it to load user information. However, we discovered later that this was not working as intended. Specifically, it turned out that our implementation allowed any JWT token, even ones not correctly cryptographically signed with our shared key.
The Problem
How was that possible?
Well, we found out the hard way that the JWT decoder’s load
function had an
optional parameter verify
which, shockingly, defaulted to false
. This meant
that, by default, the library did not check if the token was properly signed
and just blindly trusted whatever data was provided.
To actually ensure that the JWT token is correctly signed with our shared secret, the code needed to explicitly set this parameter:
data = decoder.load(token, verify: true)
Why This Matters
This is a classic example of unsafe default behavior. A well-designed API
should always have secure defaults. In our case, the verify
parameter
absolutely should have defaulted to true
. Had that been the case, our initial
code would have been secure right from the start, rather than vulnerable.
Without explicit documentation or previous knowledge of the library’s behavior,
a developer would naturally assume that verification would be performed by
default. After all, if you’re initializing a decoder with a cryptographic
secret, why wouldn’t it automatically verify?
The core lesson here: Always design APIs with safe defaults. Secure behavior should never require explicit action, it should be the implicit standard. Unsafe behaviors should only be enabled through explicit, intentional opt-ins. This experience made a lasting impression on me and now influences every API decision I make.
Hopefully, you won’t have to learn this the hard way, as I did.