DEV Community

Milan Pevec
Milan Pevec

Posted on

Elixir Plug and JWT

Coming myself from the java world (e.g. Microprofile spec) and knowing JWT implementation there, I would like to see it in action with Elixir, hoping to get all that jazz, that Elixir is giving us. And importantly, without using the Phoenix framework.

You can find the Elixir source code of this project on Github.

Not going into too many details about JWT, what is it and how it works (you can get more information here: jwt.io), I would just like to write down some points that are gonna make our lives easier.

What is JWT

JWT or JSON Web Token is a token - string mainly used for authorization of a client. After the client successfully logs into the server it receives JWT in the response and stores it in browser storage. Every subsequent request will contain this token in its header, which will be then used on the server-side to authorize (or not) the request. The client does not even need to decode it.

Quest for the three strings

In our search for JWT, we are actually doing a quest for three strings which together, separated with a dot, make a JWT. These three string are named by their nature so we can define JWT as:

"Header"."Payload"."Signature"

Header and Payload are nothing more than JSONs which are Base64Url encoded. That's all! That means if someone manages to hijack the token, then they can easily decode the Header and Payload. So that means you should never store sensitive information in the JWT. For example, we could use online encoder and encode or decode JSONs ourselves if only we could hijack the token.

Show me JSONs

Let's have a look at how these JSONs look like. The header contains the information about the algorithm used for signature, e.g.:

{"alg":"HS256","typ":"JWT"}

and its Base64Url representation is

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

The payload contains so-called claims. Without going into details lets just say that claims contain information about the user, like his name, ID, the expiration time of the token and in the case of Java Microprofile we usually store also a list of groups (which are then used for authorization). The example of the payload is:

{
"sub": "John Doe",
"user": "johndoe@hey.com",
"iss": "Server ms",
"exp": 1516239022,
"groups": ["guest"]
}

and its Base64Url represantation is:

eyJzdWIiOiJKb2huIERvZSIsInVzZXIiOiJqb2huZG9lQGhleS5jb20iLCJpc3MiOiJTZXJ2ZXIgbXMiLCJleHAiOjE1MTYyMzkwMjIsImdyb3VwcyI6WyJndWVzdCJdfQ

Can I trust you?

The last missing piece of the token is the signature. What does it mean signed JWT? What does the signature present inside of the token?

Verifying signature is considered to be a part of the validation of the token (other parts of validations is checking the expiration time etc). If JWT is valid, that means the JWT payload can be trusted, in other words, no one has changed the payload ie. claims during the transport of the token and the server can be trusted.

An example of maliciously changed payload is when attacker would change the "groups" claim to contains group "admin" etc in order to force server to allow admin only allowed request.

What is important here is to distinguish between signature and encryption. As we already know, header and payload are just base64url encoded.

The signature does not provide secrecy! 

JWTs by default are not encrypted. Encryption is covered by JSON Web Encryption (JWE) and we are not going to cover it. Signature is covered by JSON Web Signatures (JWS) and we will show its usage with Elixir.

There are several types of signing algorithms available according to the JWS. In our example, we will use HMAC using SHA-256 or shorter HS256. HMAC algorithms combine payload with a secret using a cryptographic hash function (e.g. SHA-256) or with simple words, we need a secret in order to sign a token.

That's a secret I'll never tell

Where to store secrets in Elixir? In my research, people store secrets in many ways but basic principles stay the same:

  • Secrets depend on the environment.

  • Secrets should not be stored in the source control.

  • Access to the secrets must be managed.

Me personally I'm using the approach of one file per secret, where the secret file is stored in the filesystem and is loaded by "config.exs". On the downside, that means secrets are loaded at build (compile) time, but on the upside, you can have good access management on the filesystem, while still avoiding storing it by mistake in the source control. This approach is also used by https://github.com/thechangelog/changelog.com

We could also use "releases.exs" per environment - in case you use releases in general (e.g.: MIX_ENV=prod mix release). In this way, the configuration is read when the system starts.

We could also use environment variables like the twelve-factor app is suggesting https://12factor.net/config, but then again the access management can be more tricky. Once again, choose whatever suits you best.

Show me the code already

Ok, its time for the code (Github). Let's emphasize important parts.

As you can see we are only using the following dependencies:

{:plug_cowboy, "~> 2.0"},
{:jason, "~> 1.2"},
{:jose, "~> 1.10.1"}

So for JWT operations, I've chosen JOSE library which for my taste offers the perfect taste of information compression - its nor too high-level nor too low-level code.

One plug pipeline per Application

One very important thing is, that by using "Plug.Router" you can have only one pipeline defined per application. There is a workaround explained here:

https://groups.google.com/g/elixir-lang-talk/c/U_o81N1VPfg

but I did not take the above solution.

On top of this limitation, there is another catch, every module that uses "Plug.Router" needs to have at least two plugs (match, dispatch) defined.

Combining those two important pieces of information, my code looks like the following. In my main application router I have defined my pipeline and forwards:

forward("/greetings", to: Greetings)
forward("/login", to: Login)

plug(Plug.Logger, log: :debug)
plug(:match)
plug(JwtExample.Plug.Auth, public_path: "/login")
plug(Plug.Parsers,
  parsers: [:json],
  pass: ["application/json"],
  json_decoder: Jason)
plug(:dispatch)

As you can see, we are using custom plug JwtExample.Plug.Auth for checking and verifying tokens with an option "public_path". This sets the exception for which this plug will skip the JWT check, ie. the login page should not have a JWT check off course. For other resources, plug will take the JWT token from the header and verify it.

Creating and verifying JWT is simple using JOSE library, we just need to provide:

  • JWK - JSON Web Keys which contains application secret for signing.

  • JWS - JSON Web Signature which contains defined algorithm.

  • JWT claims of our application.

Code for creating then looks like this:

# JSON Web Keys
jwk = %{
  "kty" => "oct",
  "k" => encode_secret()
}

# JSON Web Signature (JWS)*
jws = %{
  "alg" => "HS256"
}

# JSON Web Token (JWT)*
jwt = %{
  "iss" => Application.get_env(:jwt_example, :jwt_issuer),
  "sub" => id,
  "exp" => DateTime.utc_now() |> DateTime.add(expired_in * 60, :second) |> DateTime.to_unix(),
  "groups" => groups,
  "email" => email
}

{_, token} = JOSE.JWT.sign(jwk, jws, jwt) |> JOSE.JWS.compact()

The code for JWT verification looks like this:

jwk = %{
  "kty" => "oct",
  "k" => encode_secret()
}

case JOSE.JWT.verify(jwk, token) do
  {true, claims, *_*} -> {:ok, claims}
  _ -> {:error, "Token signature verification failed!"}
end

where the token is taken from the request header and as results we have claims.

More details on how plug is grabbing token etc. you can find in the sources (Github). Enjoy coding!

Top comments (2)

Collapse
 
mack_akdas profile image
Mack Akdas

Thanks for the post Milan !

Collapse
 
combinedcognitions profile image
CombinedCognitions

amazing thanks