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.

Getting Started

First things first, we need to include the Joken package in our application’s dependencies:

Run 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 MyAppWeb.JwtAuthToken module:

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 call function.

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 401 status.

And that’s it!

Conclusion

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!


Footer top

To learn more about Flatiron School, visit the website, follow us on Facebook and Twitter, and visit us at upcoming events near you.

Flatiron School is a proud member of the WeWork family. Check out our sister technology blogs WeWork Technology and Making Meetup.

Footer bottom