Cookies but better. For Elixir.
Fully Functional But Probably Needs More Testing :)
Requires: libsodium (can usually be easily installed using your favorite package manager)
If you'd like to know all the details about Macaroons, I encourage you to read the research paper!
I'll summarize it up a bit below:
Macaroons are bearer credentials, similar to cookies, API tokens, or JWTs. They're presented upon each of a client's request. Where Macaroons differ from most bearer credentials are the fact that they securely embed caveats (permissions, reasons, capabilities, etc.) inside the credential itself. These caveats are signed using a secret key, so the target service can trust the credential as it is presented along with the client's request. The target service can evaluate the request, and the caveats to see if the operation is allowed.
Caveats are simple statements that define what capabilities, identities, or authority the Macaroon holds.
Here's an example list of caveats a Macaroon may hold pertaining to an imaginary file sharing service:
1. user_id = 1234
2. user_upload_limit = 4MB
3. user_download_limit = 100MB
4. upload_namespace=/users/1234/*
5. timestamp <= 1/10/2021-5:48:47PM
With the examples above, the service should respect the requested operation should it meet the Macaroon's declared and signed caveats.
These caveats can contain any information in any string-based format. It's up to the service author to design the predicate language used.
When operating a service, you can verify a Macaroon "exactly" or "generally".
Exact verification means the data of the caveat must match byte-per-byte.
General verification allows the service author to provide simple callbacks which receive the caveat and can return true
or false
to indicate if it is met.
When you want to have a third-party validate a caveat, you must have them issue you a "discharge" Macaroon that can prove that specific caveat. There are 2 well know ways to do this:
(this is my favorite method of third-party proof!)
- Establish a relationship between the two servers (in this case a public/private RSA key pair)
- Encrypt your third-party predicate using the
add_rsa_third_party_caveat/5
function - Send this Macaroon to the client -- which will read the location and send the caveat id to the third-party server
- The third-party server will use the
decrypt_rsa_third_party_caveat/3
function to take apart the cipher text into the predicate and the root key - The third-party server will create a discharge Macaroon using the root key extracted from the cipher text in step 4 -- bind it to the original Macaroon
- Client will receive the new discharge Macaroon, and send that AND the original Macaroon back to the first-party service for verification
- Generate a nonce, then make some form of remote call out to the third-party service informing it of that random nonce
- The third-party service should return a unique ID, use this unique ID as the caveat ID in the third-party caveat. associate the unique ID with the random nonce that was generated
- Send this Macaroon to the client -- which will read the location and send the caveat id to the third-party server
- The third-party server will use the nonce to look up what needs to be verified
- The third-party server will create a discharge Macaroon using the nonce you sent it as the root key -- bind it to the original Macaroon
- Client will receive the new discharge Macaroon, and send that AND the original Macaroon back to the first-party service for verification
m = Macaroon.create_macaroon("http://my.cool.app", "public_id", "SUPER_SECRET_KEY_DO_NOT_SHARE")
m = Macaroon.create_macaroon("http://my.cool.app", "public_id", "SUPER_SECRET_KEY_DO_NOT_SHARE")
|> Macaroon.add_first_party_caveat("upload_limit = 4MB")
|> Macaroon.add_first_party_caveat("upload_namespace = /users/1234/*")
|> Macaroon.add_third_party_caveat("https://auth.another.app", "PREDICATE_HOPEFULLY_ENCRYPTED", "RANDOM_SECRET_NONCE_KEY")
alias Macaroon.Verification
result = Verification.satisfy_exact("upload_limit = 4MB")
|> Verification.satisfy_exact("upload_namespace = /users/1234/*")
|> Verification.satisfy_exact("time < 2022-01-01T00:00")
|> Verification.verify(macaroon, "SUPER_SECRET_KEY_DO_NOT_SHARE")
# result will be {:ok, macaroon} or {:error, reason_for_failure}
{:ok, json_string} = Macaroon.create_macaroon("http://my.cool.app", "public_id", "SUPER_SECRET_KEY")
|> Macaroon.serialize(:json)
macaroon = Macaroon.deserialize(json_string, :json)
{:ok, url_base64_string} = Macaroon.create_macaroon("http://my.cool.app", "public_id", "SUPER_SECRET_KEY")
|> Macaroon.serialize(:binary)
macaroon = Macaroon.deserialize(url_base64_string, :binary)
While the enacl
dependency is awaiting some PRs to fix the build flags on Apple Silicon machines, you can work around this easily:
BEFORE you run mix deps.compile
do the following
- Install libsodium via Homebrew:
brew install libsodium
- Export the environment variables so Clang can find the library:
export CPATH=/opt/homebrew/include
export LIBRARY_PATH=/opt/homebrew/lib
- Export some extra C, C++ and Linker flags to build a dual-arch library (instead of just an x86_64 one):
export CFLAGS="-arch arm64"
export CXXFLAGS="-arch arm64"
export LDFLAGS="-arch arm64"
- Done! Now run
mix deps.compile
(I really recommend using the Windows Linux Subsystem. It makes installing libsodium and most other things much easier. But if you must run this natively on Windows, follow these tips!)
- Download the latest release of libsodium, compile it using Visual Studio's compiler using x86 ReleaseDLL config.
- Take note of the full path where the
.dll
,.lib
are generated. Also note where theinclude
directory is located. - Rename the generated
.lib
to.dll.a
.
Then using a Developer Command Prompt navigate to your project:
set lib=%lib%;<PATH_TO_FOLDER_THAT_CONTAINS_libsodium.dll.a>
set include=%include%;<PATH_TO_FOLDER_THAT_CONTAINS_sodium.h>
mix deps.get
andmix deps.compile
🍪 Baked with 🐾 by Digit (@doawoo) | https://puppy.surf