Get a load of this (totally normalized) BS.
Visit our website. Create an account. Verify your email. Create a project. Add your credit card. Go to settings. Create an API key. Add it to your password manager. Drop it in your .env file. Download our SDK. Import it. Pass your env var in. Never share your API key. Make sure you never commit it to source control.
On the client, we have a React SDK. Make sure you use your publishable key for that. For the server, download our admin SDK. Use your secret key. Never mix the two up.”
It’s truly wild to me what some of y’all will tolerate.
Making your own API key
Let me show you something… Did you know generating a JWK is stupidly easy?
import { generateKeyPair, exportJWK } from 'jose'
const keyPair = await generateKeyPair('ES256', {
extractable: true,
})
const publicKeyJWK = await exportJWK(keyPair.publicKey)
const privateKeyJWK = await exportJWK(keyPair.privateKey)
That’s it. Your JWK keypair is now effectively your own self-issued API key.
No need to visit a website, make an account, verify your email, create a project, go to settings, create an API key, or copy it and use it. You just generated your own.
How do we use this?
Let’s see how we can get rid of secret versus publishable keys and separate client and admin SDKs.
Who cares whether you’re on the client or server? If your app wants to authorize a privileged action, it should be able to do so without separate keys or SDKs.
Here’s how to implement this:
- Store your app’s private JWK on the server
- Using whatever auth scheme you have, implement a function that returns whether to allow a given action
- Express privileged actions as claims in your JWT—if a privileged action is allowed, include the claim in the payload and sign the JWT with your private key
- Give your client SDK a function that reverse-proxies to your API, adding a signed JWT to the request’s Authorization header with any privileged claims you want to include
Here’s what the client-side JWT generation looks like:
import { SignJWT } from 'jose'
const jwt = await new SignJWT({
// Clients shouldn't be able to destroy the database
// unless the action is allowed, so this is a claim
destroyDatabase: true,
})
// Include the public key in the JWT header so the server
// can verify the signature and associate the request
// with your account
.setProtectedHeader({ alg: 'ES256', jwk: publicKeyJWK })
.sign(privateKeyJWK);
Charging for your API
But what if you want to charge for your API?
Simple: make your API return a payment URL when a request is made with a public key that isn’t associated with a paid account.
Your client SDK can present this to the developer (for example, by logging it to the console).
After the developer pays, associate their public key with the paid account in your database and stop returning payment URLs for future requests from that key.
B2B2C
What about situations where your customers are developers who want to give their end users API keys?
This is where you need to deviate from JOSE standards.
One approach is hierarchical JWK derivation. Here’s the idea:
- Your developer customers create a master JWK
- They derive child JWKs for each of their end users from that master key
- You modify the JWT scheme to include a zero-knowledge proof that the end user’s key is derived from the developer’s master public key
This way, you can verify that an end user is authorized by a specific developer without the developer having to manage API keys for their users.
I don’t have a ready-made example of this, but the concept is solid. If you want to explore this further, hit me up on Twitter and I’ll help you out.