JWT Auth in Phoenix with Joken
How to Decode ECDSA-Key-Signed JWTs in Elixir
This post was originally published on on Sophie’s blog, The Great Code Adventure.
JSON Web Tokens, or JWTs, allow us to authenticate requests between the client and the server by encrypting authentication information into a compact JSON object that is digitally signed. In this post, we’ll use the Joken library to implement JWT auth in a Phoenix app. We’ll focus on JWTs that are signed using a ECDSA private/public key pair, although you can also sign JWTs using an HMAC algorithm.
First things first, we need to include the Joken package in our application’s dependencies:
mix deps.get and you’re ready to use Joken!
A Note on Encryption
We’ll be decrypting tokens that were generated using an ECDSA private/public key pair. This means that we’ll need access to the public key in order to enact the decryption. Where you store that public key is up to you. You can store it in a
.pem file, accessible to your application; you can serve it from an endpoint; you can store it in an environment variable — to name a few options.
This post will assume that your code has access to the public portion of the ECDSA private/public key pair in the form of a string that looks something like this:
The Decryption Module
We’ll define a module,
JwtAuthToken that is responsible for decrypting a JWT given the token and the public key.
The public API of our module is simple. It exposes a function
decode/2 which takes in the arguments of the JWT string and the ECDSA public key string. It will use the public key to decrypt the JWT.
How Does Joken Decode and Verify?
In order to decode and verify our JWT string, Joken needs two things:
- A `Joken.Token` struct
- A `Joken.Signer` struct
So, we need to use our token _string_ to generate a `Joken.Token` and we need to use our ECDSA public key PEM file to generate a `Joken.Signer` struct. Then, we’ll call `Joken.verify/2` with these two structs as arguments.
Generating the `Joken.Token`
In order to generate this struct, we’ll call `Joken.token/1`. We pass in an argument of the JWT string:
This will return the `Joken.Token` struct in the following format:
Validating Token Expiration
We’re not quite done with our token struct though. Notice that the `:validations` key points to an empty map. The data stored under `:validations` key of the token struct will be used by `Joken.verify/2` to determine the validity of a decoded token’s claims. Our token’s encoded claims will include an *expiration date*, under a key of `”exp”`. We *only* want a decoded token to be considered valid if the `”exp”` in the claims has is not in the past. So, we’ll leverage `Joken.with_validation` to write a validation function that returns true if the token’s claims’ `”exp”` is _not_ in the past:
Now our token struct looks like this:
Such that when we later call `Joken.verify/2`, Joken will execute the function stored under the `”exp”` key of the `:validations` struct with an argument of the value stored under the `”exp”` of the decoded token’s claims.
If this function returns `true`, Joken will expose the decoded token’s claims:
If it returns `false`, Joken will return the token struct _without_ the decoded claims and _with_ an error message:
Now that we have our token struct ready to go, we can generate the `Joken.Signer` struct.
Generating the `Joken.Signer`
In order to generate the signer struct, we need to build our ECDSA public key struct. We can doing this using `JOSE`.
Generating the ECDSA Signing Key with `JOSE`
JOSE stands for JSON Object Signing and Encryption. Its a set of standards developed by the JOSE Working Group. The
JOSE package is a dependency of Joken, so we don’t need to install it ourselves via our application dependencies.
Joken needs our public key in the form of a map in order to use it to decrypt our token. We’ll use the
JOSE.JWK (JWK stands for JSON Web Key) module to turn our public key PEM into such a map.
Let’s define a private helper function,
signing_key in our
The first function call,
JOSE.JWK.from_pem converts our public key PEM binary into a
JOSE.JWK. The second function call,
JOSE.JWK.to_map (you guessed it) converts that
JOSE.JWK into a map. So, we end up with a tuple that looks like this:
Where the second element of the tuple is the ECDSA public key map. Joken will use this map as a key when generating an ECDSA signer.
Generating the Signer
Joken.Signer is the JWK (JSON Web Key) and JWS (JSON Web Signature) configuration of Joken. The signer allows us to generate a token signature or read the token signature during decryption. We want to generate an ECDSA signer with our public key. Then, we can use this signer to decrypt our token.
We’ll define another private helper function,
signer/1, to do this:
Here, we use the
Joken.es256 function, with the argument of our public key map, to generate an ECDSA token signer. The
es256 function wraps a call to
Joken.Signer.es/2 which takes in the algorithm type and the key map and returns the signer.
Now that we have our ECDSA signer, we’re ready to decode our token!
Decoding the Token with the Signer
Now we can easily decrypt JWTs like this:
Let’s use our decoder in a custom plug to prevent anyone without a valid JWT from accessing our app’s endpoints.
The Auth Plug
We’ll build a custom plug,
JwtAuthPlug, that we’ll place in the pipeline of our authenticated routes:
Our plug is pretty simple, it will:
1. Grab the JWT from the request’s cookie
2. Call on our
JwtAuthToken.decode/2 function to decode it
If it can successfully decode the JWT, it will allow the request through. If not, it will return a
401 unauthorized status
Let’s get started!
Getting the JWT from the Request
Defining a custom plug is pretty simple. We need to
import Plug.Conn to get access to some helpful connection-interaction functions. Then, we need an
init function and a
We’ll define a helper function,
jwt_from_cookie, that will pluck the JWT string from the request cookie:
Here, we use a convenient
Plug.Conn function to get value of the Cookie request header:
Plug.Conn.get_req_header. Then, we use another function,
Plug.Conn.Cookies.decode to turn that value (a comma or semicolon separated string), into a map. Lastly, we pattern-match the JWT out of the map.
Now that we have our JWT, let’s decode it!
Decoding the JWTs
We’ll call on our
JwtAuthToken.decode/2 function. If it returns the success tuple, we’ll store the decoded claims in the
conn. Otherwise, we’ll respond with the
And that’s it!
Joken makes it easy to decode JWTs in your Phoenix application. By generating your own ECDSA signer using
JOSE, and building a simple custom plug, you can keep your routes secure. Happy coding!
P.S. Want to work on a mission-driven team that loves stress-free, secure authentication and ice cream? We’re hiring!