Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Building Your First Crystal Web App and Authenticating With JWTs, Part 2

DZone's Guide to

Building Your First Crystal Web App and Authenticating With JWTs, Part 2

In the conclusion to our introduction to Crystal, we'll learn how to add authentication via JWTs to the web application we built last time.

· Security Zone ·
Free Resource

Discover how to provide active runtime protection for your web applications from known and unknown vulnerabilities including Remote Code Execution Attacks.

Welcome back! If you missed the first part, you can check it out here

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/login?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 to 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 and 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 authorize 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 ofid_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 exchange this for a usable JWT.

Using the http/client library, we can construct a request that sends a JSON blob containing our client_idclient_secretcode, and redirect_uri to Auth0 for verification. Note that thecode 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 authorize 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 realize 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 to 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 add 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 theJsonWebToken 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 authorized in our app. Happily, Auth0 makes it very simple to do this, by applying a rule. Head into the 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 ourClient 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 authorized 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'sbefore_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 authorization, 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 recognize these routes and the logic as very standard. The final additions to this web app are the routes to render custom templates for authorization 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 authorization, 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!

Find out how Waratek’s award-winning application security platform can improve the security of your new and legacy applications and platforms with no false positives, code changes or slowing your application.

Topics:
security ,authentication ,jwt ,json web token ,web application security

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}