Cover image for How we implemented API keys for prefix!

How we implemented API keys for prefix!

Wolf Vollprecht
Written by Wolf Vollprecht 10 months 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 the user:password is sent as a Authorization: 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.