TL;DR: In this article we will cover building your first web application in Crystal. The application will utilise JSON Web Tokens (JWTs) to authenticate our users around the restricted resources of our app. The completed sample application can be found in this GitHub repo. I'd encourage you to check out the repo to follow, aside this tutorial.
Building your first Crystal Web App & Authenticating with JWTs
I wrote a technical article back in June titled "The Highs & Lows of Crystal". It was an introduction to the Crystal Language and an overview of my perspective on it, trying it out for the first time. I enjoyed trying out Crystal very much and the good news is that Crystal is still on the up, and more than ever people are trying it out, and finding that they really rather like it (just as I do!)
Today I'd like to share a guide on building your first Web App with Crystal, and using JSON Web Tokens (JWTs) to authenticate users in said application. Before we dive into the technical tutorial, for those who haven't read the previous article, and perhaps don't know about Crystal Language at all, I'd like to quickly introduce it.
“Crystal is a statically-typed, garbage collected language, boasting the speed, efficiency and type safety of a compiled language.”
Tweet This
Crystal was introduced by its creator, Ary Borenszweig, back in mid-2014 as a general purpose, object-oriented language with the aim of giving speeds close to C/C++ and being heavily syntactically influenced by Ruby. Crystal is a statically-typed, garbage collected language, boasting the speed, efficiency and type safety of a compiled language.
Crystal runs on the LLVM, and although the feel of the language is rather similar to Ruby, Crystal compiles down to much more efficient code, with much greater processing speeds. It is also self-hosted with the Crystal compiler actually being written in Crystal itself. As I mentioned in my previous Crystal introduction, the language is built to support concurrency primitives right out of the box, and uses similar concurrency concepts (including lightweight threading) to languages like Golang and Elixir.
“Two of the things I like most about Crystal are the excellent built-in tooling available, and the ease with which you can bind C libraries.”
Tweet This
Two of the things I like most about Crystal are the excellent built-in tooling available, and the ease with which you can bind C libraries. When I look at new languages, especially relatively immature languages, it's always very reassuring when the language has extensive built-in tooling available to help developers stay productive and happy! In Crystal, there are a bunch of tools that make hacking around in the language super fun, but also help us to stay on the right track with semantics and more. I touched on those topics in a bit more detail in my previous article.
So that's Crystal in a nutshell! And now we have that covered, we can get on with building our first Crystal web application, and authenticating our app users with JWTs. If you would like to see the completed sample application to follow along with this article, it can be found in this GitHub repo.
Web App concept TL;DR
The idea behind the web app that we're going to build is a community driven Coding Challenge app in which members of the Open Source community can set coding challenges, and other members of the community can take the challenge and provide the solution. The joy of this app would be that coding challenges would have a whole array of solutions, in many different languages. They could act as great learning resources to others in the developer community, alongside being a fantastic learning experience in solving the challenge itself. (An idea I'd love if someone were to build and launch in the real world!)
In our app members of the community will have to be authorised to post and access the coding challenges, and therefore we're going to need a means of authentication. For that, we're going to use JSON Web Tokens. We also need to store the challenges posted, for that we'll use standard MySQL as our database.
Installing Crystal Lang
If you run on a Mac, you can install Crystal through Homebrew. Simply run:
brew update brew install crystal-lang
If you run on Debian or Ubuntu, you can install through apt-get:
sudo apt-get install crystal
One thing worth noting is that if you are going to install onto Ubuntu or Debian, you must first add the repository and signing key to your APT configuration. It's super simple to do, and instructions can be found here.
Alternatively, you can build from source with a tar.gz file. The latest files can be found on the releases page on GitHub: https://github.com/crystal-lang/crystal/releases.
Crystal does not yet have a direct Windows port but that being said, if you have Windows 10 onward you can install and run Crystal as normal through the Bash / Ubuntu for Windows Subsystem. Details for that can be found here.
To test your installation has worked successfully, simply type
crystal
into your terminal, and you will have returned the Crystal CLI default menu. Now we have Crystal installed and working on our machines, we can get on with the fun part—building our app!Creating our Project
For building our web app, we shall use the popular Crystal web micro-framework Kemal. Kemal is heavily influenced by Sinatra for Rubyists, and works in a very similar way. I can confirm it is a joy to use—being both simple and functional. Handily, we can scaffold our new project using Crystal's built-in project tooling. Switch into your development working directory, and run the command:
crystal init app challenger
With
challenger
being the name I gave my application. Feel free to name your project whatever you wish! Open up the project in your chosen editor, and head over to the file shard.yml
in the project root. Much like having a Ruby Gems file, this YML file contains all of the dependencies for our project. Open up shard.yml
and add the following:dependencies: kemal: github: kemalcr/kemal branch: master
Once that's in, head back into your terminal and run
shards install
. Doing this will pull down Kemal and its dependencies for us to utilise.The main file in your project is located in the
/src
directory and will be named with whatever you called your project, so for me it's: /src/challenger.cr
. This file will contain the routes for our Kemal app alongside the controller logic, some config for our Kemal app, and not much else. To get our app up and running immediately, open up that file and add in the following:require "./challenge/*" require "kemal" module Challenger get "/" do render "src/challenge/views/home.ecr", "src/challenge/views/layouts/main.ecr" end Kemal.config.port = 6969 Kemal.run end
The require statements at the beginning of the file are telling our app to include Kemal to serve pages, and to include the
src/challenge
folder, which will contain files and folders necessary to our project such as Config, Models, Views, etc. Next, we have defined a route and instructed it to render two ECR files. This is because Kemal allows us to create layout templates, alongside file-specific views.To enforce those views, create the layout file
src/challenge/views/layouts/main.ecr
:<html> <head> <title>Challenger // Coding Challenges</title> <link rel="stylesheet" href="/css/main.css"/> </head> <body> <section id="main"> <div class="container"> <%= content %> </div> </section> <script src="/js/main.js"></script> </body> </html>
Now create the home view file
src/challenge/views/home.ecr
, and enter in whatever you like. I simply included an <h1> tag
saying hello.You can now test your application by running
crystal run src/challenger.cr
from your terminal. Your app should now be running on whichever port you specified—in this case ":6969".Setting up User Login with Auth0 Hosted Pages
Now that we've got the base of our app setup, we can move straight onto the good stuff, and the main reason we're all here—authenticating our users with JWTs. With Crystal being a young and relatively immature language, it's no huge surprise that there aren't really any existing user authentication frameworks that we could simply drop into our project, much like we would with Devise or Omniauth for Ruby / Rails. Happily however, we don't need one.
We're going to store our users with Auth0 meaning we don't even need a
Users
table in our database. Auth0 has quickstart guides to pretty much all major languages, but again with Crystal being a fairly new language, it's up to us to build from scratch. However, Auth0 make this easy for us with good technology, and Crystal makes it a genuine pleasure to write. The first thing to do is open up your Auth0 Dashboard and click on the Create Client button. For this app, we can create a Regular Web Application client, and name it Challenge or whatever you have titled your application.
Then, back in your chosen editor, we need to create some routes for our app. Open up
/src/challenger.cr
and add the following routes:get "/auth/login" do |env| env.redirect "https://[YOUR_URL].auth0.com/authorize?client=[CLIENT_ID]" end get "/auth/callback" do |env| # callback logic end get "/success" do |env| # for forwarding people to end get "/auth/logout" do |env| # log people out end
These four routes are the basis for our authentication flow with Auth0. Since there are, as I stated earlier, no available authentication libraries for our users to log in—we will use Auth0's hosted pages. Note in the
/auth/login
route, we have created a redirect. This redirect will send users straight to our Auth0 hosted login page whenever they access the /auth/login route.Once you've added these routes, open up your Auth0 app settings again, scroll down on the settings tab for your app and add into the
Allowed Callback URL
section:http://localhost:6969/auth/callback
We haven't created the callback logic in our app just yet, but this is the route we will assign to it, so we can fill that in already.
Create Login Callback & Get JWT
If you were to boot your application and run through the login process now, you would see that once successfully logged in, you would simply be presented with the
/auth/callback
page—entirely blank, but with a URL param titled ?code=
. The way in which the process works can be found in the Access Token Documentation, that code actually being our Auth0 Access Token. To turn this Access Code into a usable JWT that we can pass around our application to authorise our user, we need to silently POST that code, alongside our App ID and Secret back to Auth0 to verify and exchange for a JWT. To do this, we shall create a module named Auth that will handle this in the background, on our /auth/callback
.Create a file in the
/src
directory of your application named auth.cr
, and add in the following:require "http/client" require "json" module Auth extend self def get_auth_token(id_code) id = id_code uri = URI.parse("https://[YOUR_DOMAIN].auth0.com/oauth/token") request = HTTP::Client.post(uri, headers: HTTP::Headers{"content-type" => "application/json"}, body: "{\"grant_type\":\"authorization_code\",\"client_id\": \"[CLIENT_ID]\",\"client_secret\": \"[CLIENT_SECRET]\",\"code\": \"#{id}\",\"redirect_uri\": \"http://localhost:6969/auth/callback\"}") response = request.body res = get_jwt(response) return res end end
In the above code, we are creating a module titled Auth that we can call in the background from our application. We have defined a method called
get_auth_token
which takes the argument of id_code
sent from the successful login of our hosted login. We then need to send a request to [YOUR_domain].auth0.com/oauth/token
to exhange this for a usable JWT. Using the
http/client
library, we can construct a request that sends a JSON blob containing our client_id
, client_secret
, code
, and redirect_uri
to Auth0 for verification. Note that the code
value here is a string-interpolation of the Access Code
we received back as a URL param from the Auth0 login.Also at the end of this method, we are calling another method titled
get_jwt()
that takes the returned JSON blob as its argument. Let's create that method now, in the same Auth module:class Token JSON.mapping( access_token: String, id_token: String, expires_in: Int32, token_type: String, ) end def get_jwt(auth_code) auth = auth_code value = Token.from_json(%(#{auth})) jwt = value.id_token return jwt end
To extract the usable JWT from the JSON blob that was returned in the last method call, we can use Crystal's super-handy
JSON.mapping
functionality. We have defined a class titled Token
that we can push the JSON blob into, to create usable values. In defining the get_jwt()
method, we are doing that value push into the Token
object, and returning just the id_token
value. This id_token
is our actual, usable JWT containing our encoded user's details that we will use to authorise them around our application.Now that we have our Auth module created, we can head back to our main
src/challenger.cr
file, require the module, and call it from our /auth/callback route:require "./auth" get "/auth/callback" do |env| code = env.params.query["code"] jwt = Auth.get_auth_token(code) env.response.headers["Authorization"] = "Bearer #{jwt}" end
So when a user successfully logs in, their Access Token will be sent to our
/auth/callback
route, extracted from the URL params and used to call our Auth module which will return the usable JWT. We can then set this JWT in our HTTP Authorization Header and we're done... Or are we?If you run your application now, you will realise that after successful login and setting of the JWT in the Authorization header, if you navigate to another page the header disappears entirely. This is because we need to actually store this JWT in LocalStorage either in a session or in a cookie. In this case, we are going to use a session, and happily there is a library built for Kemal to handle the creation of sessions.
Add the following into your
shard.yml
file, and run shards install
:kemal-session: github: kemalcr/kemal-session
Setting the JWT in a User Session
We now need to set up
kemal-session
to carry our JWT Authorization header. In your main src/challenger.cr
file, remember to require kemal-session, and add in the following before your route definitions:Kemal::Session.config do |config| config.cookie_name = "sess_id" config.secret = "[SOME_SECRET]" config.gc_interval = 1.minutes end class UserStorableObject JSON.mapping({ id_token: String }) include Kemal::Session::StorableObject def initialize(@id_token : String); end end
To generate a strong
config.secret
, you can run the following and copy / paste the result:crystal eval 'require "secure_random"; puts SecureRandom.hex(64)'
Once again here, we are going to create a class to which we can
JSON_map
values. The only value we need is the id_token
here, as this will be our encoded JWT value. Back to our auth/callback
route, we can update it to the following:get "/auth/callback" do |env| code = env.params.query["code"] jwt = Auth.get_auth_token(code) env.response.headers["Authorization"] = "Bearer #{jwt}" # Set the Auth header with JWT. user = UserStorableObject.new(jwt) env.session.object("user", user) env.redirect "/success" end
Note that we are creating a
user
object from the jwt
value returned from our Auth module. We are then using that user
object to set as a session
, storing the encoded JWT which will now remain when the visitor navigates to another page. It's now worth updating our /auth/success
and /auth/logout
routes too:get "/success" do |env| user = env.session.object("user").as(UserStorableObject) env.response.headers["Authorization"] = "Bearer #{user.id_token}" render "src/challenge/views/success.ecr", "src/challenge/views/layouts/main.ecr" end get "/auth/logout" do |env| env.session.destroy render "src/challenge/views/logout.ecr", "src/challenge/views/layouts/main.ecr" end
Great! We've got our encoded JWT being passed round in our Authorization HTTP Header, we have a successful login redirect, and even a way for the user to log out. We're not quite finished yet, though.
Decoding the JWT and Verifying Access
The whole reason we want to pass around a JWT in our HTTP Authorization header is to be able to decode and verify this token to check whether our users have access to restricted parts of our application. So what we need to do now is create another module that will decode our JWT, verify it and check that it contains an arbitrary attribute stating access levels for our application.
Before we build this module, head over to your Application Settings in the Auth0 Dashboard and change a couple of settings. Firstly, scroll down to the Advanced Settings of your application and click on the OAuth tab. The first setting we need to change here is the
JsonWebToken Signature Algorithm
setting. Ensure that this is set to HS256 and not RS256. The next setting is
OIDC Conformant
. For now, just turn this off as it's not something we need to worry about, and may stop us from allowing login with username/password when using the hosted login. More information on that can be found in the OIDC Documentation—but for now, don't worry too much about it, just switch it off. Ensure you save these settings before moving on.In this sample application we are not including an app-specific way for users to register, they can only do so through the hosted login. In doing so, we are not sending any custom attributes for the user, that they will need to be authorised in our app. Happily, Auth0 makes it very simple to do this, by applying a rule. Head into the Rules section directly below the Users section in the navigation. Click on the Create Rule button and select a blank rule. Name it
add-member-meta
and have it reflect the following:function(user, context, callback){ user.app_metadata = user.app_metadata || {}; // update the app_metadata that will be part of the response user.app_metadata.roles = user.app_metadata.roles || []; user.app_metadata.roles.push('member'); // persist the app_metadata update auth0.users.updateAppMetadata(user.user_id, user.app_metadata) .then(function(){ callback(null, user, context); }) .catch(function(err){ callback(err); }); }
Once we've done this, we need to tell Auth0 to return this value in the JWT. Create another rule, name it
include-meta
and add the following:function (user, context, callback) { if (context.idToken && user.user_metadata) { context.idToken.user_metadata = user.user_metadata; } if (context.idToken && user.app_metadata) { context.idToken.app_metadata = user.app_metadata; } callback(null, user, context); }
By setting this rule, Auth0 will now return the
user_metadata
field in our JWT. This field contains our user's access level that we can verify before allowing them access to resources.Moving on, head back into your project, add the Crystal jwt module to your
shards.yml
file and run shards install
:jwt: github: crystal-community/jwt
Now, create a file named
user.cr
in the base /src directory where we created the Auth module earlier. Have it reflect the following:require "jwt" require "json" module User extend self @@verify_key = "[CLIENT_SECRET]" def authorised?(user) token = user payload, header = JWT.decode(token, @@verify_key, "HS256") roles = payload["app_metadata"].to_s rs = roles.includes?("member") puts rs return end end
The first thing to note here is the
@@verify_key
class variable. To decode our JWT, it needs a secret key. This secret key is actually the Client_Secret
defined in our Auth0 app management console. (The same one we used earlier when calling for our Auth Token).Next, we have defined a method called
authorised?()
that will take in the encoded JWT of our User as an argument. Then, we use the Crystal JWT library to decode the JWT using our Client Secret
as the secret key, and denoting the hashing algorithm to use as HS256
as we defined earlier in our application Advanced Settings / Oauth. Once decoded, we can check to confirm that the payload of the JWT contains our app_metadata
attribute of roles, and that it does reflect the user being a member
. We can then return the True / False value.We know that the only parts of our application we want our users to be authorised to view are the coding challenges. Open up your main
src/challenger.cr
file and add the following route definition, ensuring you include require "./user"
:before_get "/challenges" do |env| user = env.session.object("user").as(UserStorableObject) auth = User.authorised?(user.id_token) raise "Unauthorized" unless auth = true end
Here we are using Kemal's
before_get
directive to ensure this method is called before a user can call a resource. We are setting a user
object from the stored user JWT in the session, and calling our User.authorised?()
method on that user / JWT. Depending on the returned boolean, we are either raising a 401 Unauthorized, or allowing access.Realistically, this is the crucial part of our application as it is the logic defining resource access from our user's JWT.
Extra: Adding the Coding Challenges
As this article is focusing on the use of JWTs for authorisation, I won't go into too much detail on the actual coding challenge objects themselves. For these coding challenges, and to make a slightly more complete app, I added in a SQL database and used the
Granite-ORM
library to map the database data to objects. I created a model titled src/models/challenge.cr
which contained the following:require "granite_orm/adapter/mysql" class Challenge < Granite::ORM::Base adapter mysql field title : String field details : String field posted_by : String timestamps end
I then added some samples to the database, and set up the following routes:
get "/challenges" do |env| challenges = Challenge.all("ORDER BY id DESC") render "src/challenge/views/challenges/index.ecr", "src/challenge/views/layouts/main.ecr" end get "/challenges/:id" do |env| if challenge = Challenge.find env.params.url["id"] render("src/challenge/views/challenges/details.ecr", "src/challenge/views/layouts/main.ecr") else "Challenge with ID #{env.params.url["id"]} Not Found" env.redirect "/challenges" end end get "/challenges/new" do |env| render "src/challenge/views/challenges/new.ecr", "src/challenge/views/layouts/main.ecr" end
Anyone coming from a traditional web framework that includes an ORM will recognise these routes and the logic as very standard. The final additions to this web app are the routes to render custom templates for authorisation and internal errors:
error 500 do render "src/challenge/views/error/srv.ecr", "src/challenge/views/layouts/main.ecr" end error 401 do render "src/challenge/views/error/auth.ecr", "src/challenge/views/layouts/main.ecr" end
Conclusion
The GitHub repo for this completed application can be found here, in the Challenge sample app
Coming from a lower-level programming background, whenever I build a web application, I always take the easiest route, i.e. using a complete framework. With Crystal being such a young language it is to be expected that the equivalent libraries from other languages do not yet exist. When I build a web application, I generally use Rails. Alongside Rails, I would use Devise / Omniauth for authentication and something like Rolify or CanCan for user role management. But with these not yet existing for Crystal, we have had to roll our own.
The above sample app is a basic, yet functional example of a Crystal-based web app that takes a user through authorisation, and role / resource management using JWTs. Dropping in Auth0's hosted login on the frontend allowed us to entirely bypass having to build a User and Password system, or having to keep a users table altogether.
I am incredibly excited to see how the Crystal language and community will continue to grow, and I aim to continue being a part of that community too. Maybe one day soon I'll get the time to write and maintain a modular JWT / Authentication library for Crystal that you can drop into your applications for instant use.
Before I finish, I would just like to quickly send a thank you to Serdar Dogruyol, the creator of the Kemal framework, for his advice on using the Kemal-Session library.
Thanks for sticking with me on what was a long tutorial—I greatly appreciate it!