
How we implemented API keys for prefix!

Written by Wolf Vollprecht 2 years ago
Along with private and public
channels, prefix.dev now supports
API Key authentication! In this blog post, we share a few details on how we
implemented API Keys with security in mind.
What are API Keys
API Keys are a widely used means of authenticating API requests. They are in use
at many large developer platforms such as GitHub, Docker or Anaconda.org. Since
we now support uploading packages to public or private channel, we definitely
needed the means to authenticate with the platform from a CLI tool or similar.
The conda / mamba community has two ways of authenticating against an API
(built-in to the tools):
- HTTP Basic auth: this is the
https://user:password@mypage.com/...
authentication where theuser:password
is sent as aAuthorization: Basic <hash>
. This is a proven and reliable way of authenticating against an API but doesn't seem to be very much in fashion anymore - The conda / Anaconda token: this is a very weird way of authenticating
against an API. It injects a token into the URL like
https://anaconda.org/t/<TOKEN>/rest/of/url.tar.bz2
. Unfortunately, it's not really "standard" and sending tokens as part of the URL is not very secure.
We've decided to learn from the best and implement a more standard way of
sending the API Key, namely under the
Authorization: Bearer <TOKEN>
- a so-called "bearer token".How do API Keys work
API Keys do two things: identify who is the owner of the API Key and a
password. For the prefix.dev keys, we've chosen a common prefix (
pfx_
) on all
keys to make them easily identifiable (we hope that this will also help with
security scanning later on!). We then use the first 8 characters of the key as
the ID of the key (to uniquely identify it) and the rest of the key is the
"password".This is what a prefix.dev API key looks like:
Key: pfx_vr2XPfxpByKvGVhzrkjtrjn235nj23n4jn9v
# split up into parts:
Key: pfx_ vr2XPfxpB yKvGVhzrkjtrjn235nj23n4jn9v
^ PREFIX ^ ID of the key ^ The actual password
We use "nanoid" for the key because it is relatively "readable" and more compact
vs. UUID4 or other formats. You can read more about nanoid vs UUID in the
PlanetScale
blogpost.
The second important part of the API key implementation is that they should be
treated like a password. That means they should never be stored in plain
text! This is also the reason why you have to store the API key yourself and can
only see it once in the prefix.dev UI - we literally have no means of retrieving
the information once we sent it to you.
Instead of storing the password-part of the API key we store a hash of the
password. This is a very common technique, and usually people use
bcrypt
for this purpose. Another more
modern alternative, that won some password-hashing competitions, is
argon2
. There is a convenient
argon2
Rust crate for it that
makes it easy to hash and verify the passwords.The code in our backend looks roughly like this:
use argon2::{self, Config};
let password = b"password";
let salt = b"randomsalt";
let config = Config::default();
let hash = argon2::hash_encoded(password, salt, &config).unwrap();
let matches = argon2::verify_encoded(&hash, password).unwrap();
assert!(matches);
What to do with the API key
Now that you know how our API keys roughly work, what can you do with them?
Well, globally authenticate against our API endpoints! If you supply the
Authorization: Bearer <API KEY>
header, you can authenticate against the
GraphQL and REST endpoints on the prefix.dev platform.To read more about our endpoints, you can check out the documentation:
To make an authenticated request with
curl
against our GraphQL API, you could
use the following snippet:curl https://prefix.dev/api/graphql \
-H "Authorization: Bearer pfx_vr2XPfxpByKvGVhzrkjtrjn235nj23n4jn9v" \
--data '{"query": "{ viewer { login }}"}'
Or with
python/requests
, for the same:import requests
token = "pfx_vr2XPfxpByKvGVhzrkjtrjn235nj23n4jn9v"
headers = {"Authorization": f"Bearer {token}"}
response = requests.post("https://prefix.dev/api/graphql", json={"query": "{viewer { login }}"}, headers=headers)
print(response.json())
Future work
We have already integrated support for the "BearerToken" format into
micromamba
- you can login using micromamba auth login https://repo.prefix.dev --bearer pfx_vr2XPfxpByKvGVhzrkjtrjn235nj23n4jn9v
. We
are currently in the process of adding authentication support (for all three
described authentication methods) to rattler
, so that rattler can be used for
authenticated repodata retrieval and package installation easily. For this, we
extended the reqwest
client into an AuthenticatedClient
.Furthermore, we hope that we can also make a conda plugin available that
implements the
Bearer
-Token authentication method. The conda team has done a
great job at making the conda codebase extensible, and I believe that the
request/channel layer is already extensible, so this might already be possible.
Maybe even anaconda.org will switch to a more standard "Bearer"-Token
authentication in the future?We hope that this article illustrated how serious we take security. If you try
out our authenticated API or our newly released private
channels, do let us know. We love
to hear from users on our Discord or on the
repository.