JWT Auth in Elixir with Joken
By Sophie DeBenedetto | Posted 2018-11-29
Use Joken and JOSE for a light-weight implementation of JWT Auth in your Elixir web application.
JSON Web Tokens, or JWTs, allow us to authenticate requests between the client and the server by encrypting authentication information into a secure, 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:
def deps do
# .. other deps
{:joken, "~> 2.0-rc0"}
end
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 assumes 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:
-----BEGIN PUBLIC KEY-----
blahblahblah
yaddayaddayadda
-----END PUBLIC KEY-----
The Decryption Module
We’ll define a module, JwtAuthToken
that is responsible for decrypting a JWT given the token and the public key.
defmodule MyAppWeb.JwtAuthToken do
def decode(jwt_string, public_key) do
# coming soon!
end
end
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:
defmodule MyAppWeb.JwtAuthToken do
def decode(jwt_string, public_key) do
jwt_string
|> Joken.token
end
end
This will return the Joken.Token
struct in the following format:
%Joken.Token{ claims: %{}, claims_generation: %{},
error: nil,
errors: [],
header: %{},
json_module: Poison,
signer: nil,
token: "blah.blah.blah",
validations: %{}
}
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:
defmodule MyAppWeb.JwtAuthToken do
def decode(jwt_string, public_key) do
jwt_string
|> Joken.token
|> Joken.with_validation("exp", &(&1 > Joken.current_time()))
end
end
Now our token struct looks like this:
%Joken.Token{
claims: %{},
claims_generation: %{},
error: nil,
errors: [],
header: %{},
json_module: Poison,
signer: nil,
token: "blah.blah.blah",
validations: %{"exp" => {#Function<6.99386804/1 in :erl_eval.expr/5>, nil}}
}
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:
%Joken.Token{
claims: %{
"aud" => ["user"],
"email" => "guy@email.com.com",
"exp" => 1540399830,
"iat" => 1540392630,
"nbf" => 1540392630,
"sub" => "ea375e5a-f918-4017-a5ee-1fc8b641ef84"
},
claims_generation: %{},
error: nil,
errors: [],
header: %{},
json_module: Poison,
signer: <coming soon!>,
token: "blah.blah.blah",
validations: %{
"exp" => {#Function<0.91892837/1 in DeployerWeb.JwtAuthToken.decode/2>, nil}
}
}
If it returns false
, Joken will return the token struct without the decoded claims and with an error message:
%Joken.Token{
claims: %{},
claims_generation: %{},
error: "Invalid payload",
errors: ["Invalid payload"],
header: %{},
json_module: Poison,
signer: <coming soon!>,
token: "blah.blah.blah",
validations: %{
"exp" => {#Function<0.91892837/1 in DeployerWeb.JwtAuthToken.decode/2>, nil}
}
}
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 string into a map.
Let’s define a private helper function, signing_key
in our MyAppWeb.JwtAuthToken
module:
defmodule MyAppWeb.JwtAuthToken do
...
defp signing_key(public_key) do
{ _, key_map } = public_key
|> JOSE.JWK.from_pem
|> JOSE.JWK.to_map
key_map
end
end
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:
{% raw %}
{%{kty: :jose_jwk_kty_ec},
%{
"crv" => "P-256",
"kty" => "EC",
"x" => "xxxx",
"y" => "xxxx"
}}
{% endraw %}
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:
defmodule MyAppWeb.JwtAuthToken do
...
defp signer(public_key_string) do
public_key_string
|> signing_key
|> Joken.es256
end
defp signing_key(public_key_string) do
{ _, key_map } = public_key_string
|> JOSE.JWK.from_pem
|> JOSE.JWK.to_map
key_map
end
end
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 Token and the Signer
defmodule MyApp.Web.JwtAuthToken do
def decode(jwt_string, public_key_string) do
jwt_string
|> Joken.token
|> Joken.with_validation("exp", &(&1 > Joken.current_time()))
|> Joken.with_signer(signer(public_key_string))
|> Joken.verify
end
defp signer(public_key_string) do
public_key_string
|> signing_key
|> Joken.es256
end
defp signing_key(public_key_string) do
{ _, key_map } = public_key_string
|> JOSE.JWK.from_pem
|> JOSE.JWK.to_map
key_map
end
end
Now we can easily decrypt JWTs like this:
JwtAuthToken.decode(jwt_string, public_key)
=> {
:success,
%{
token: "blah.blah.blah",
claims: %{sub: "1234", email: "guy@email.com"}
}
}
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:
# router.ex
...
pipeline :api do
plug :accepts, ["json"]
plug MyAppWeb.JwtAuthPlug
end
Our plug is pretty simple, it will:
- Grab the JWT from the request’s cookie
-
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!
Defining the Custom Plug
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.
defmodule MyAppWeb.JwtAuthPlug do
import Plug.Conn
alias MyAppWeb.JwtAuthToken
def init(opts), do: opts
def call(conn, _opts) do
# coming soon!
end
end
Getting the JWT from the Cookie
We’ll define a helper function, jwt_from_cookie
, that will pluck the JWT string from the request cookie:
defmodule MyAppWeb.JwtAuthPlug do
import Plug.Conn
alias MyAppWeb.JwtAuthToken
...
defp jwt_from_cookie(conn) do
conn
|> Plug.Conn.get_req_header("cookie")
|> List.first
|> Plug.Conn.Cookies.decode
|> token_from_map(conn)
end
defp token_from_map(%{"session_jwt" => jwt}, _conn), do: jwt
defp token_from_map(_cookie_map, conn) do
conn
|> forbidden
end
defp forbidden(conn) do
conn
|> put_status(:unauthorized)
|> Phoenix.Controller.render(MyAppWeb.ErrorView, "401.html")
|> halt
end
end
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 string separated by ,
, ,
or ;
) 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
defmodule MyAppWeb.JwtAuthPlug do
import Plug.Conn
alias MyAppWeb.JwtAuthToken
def call(conn, _opts) do
case JwtAuthToken.decode(jwt_from_map, public_key) do
{ :success, %{token: token, claims: claims} } ->
conn |> success(claims)
{ :error, error } ->
conn |> forbidden
end
end
defp public_key do
# your public key string that you read from a PEM file or stored in an env var or fetched from an endpoint
end
defp success(conn, token_payload) do
assign(conn, :claims, token_payload.claims)
|> assign(:jwt, token_payload.token)
end
end
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!