Although Lucky is fantastic for building complete applications, I like to build my front-end in Angular so I usually use Lucky as a JSON Api. I prefer it over Rails Api because of the type checking, separation of models from forms and queries, and the way actions and routes are organized.

One feature I usually need in a JSON api is authentication, and today we'll go over setting up JWT authentication with Lucky Api.

Starter App

To start we'll be using the lucky api demo app which has User and Post models defined. Run:



git clone git@github.com:mikeeus/lucky_api_demo.git git checkout jwt-auth-0 bin/setup

You can follow along by switching to the branches shown under the headings for each section. Or look at the finished product by checking out jwt-auth-10-complete

Dependencies

branch: jwt-auth-0

The only dependecy we'll need is a shard for jwt encoding and decoding. We can use the crystal jwt package so lets add the following to our shard.yml and run shards .



dependencies: jwt: github: crystal-community/jwt

Begin with Tests

branch: jwt-auth-01-sign-in-test

How else would we know the app is working? Aight, in spec/blog_api_spec.cr we'll add a describe block for authentication and our first test which will be for signing in.

Note I got AppVisitor from Hannes Kaeufler's blog which is a great Lucky site that I use as reference.



# spec/blog_api_spec.cr require "./spec_helper" describe App do visitor = AppVisitor . new ... describe "auth" do it "signs in valid user" do # create a user # visit sign in endpoint # check response has status: 200 and authorization header with "Bearer" end end end

We'll user Lucky's boxes to make generating test data easy. We'll also use Authentic's generate_encrypted_password method to generate our password.



# spec/support/boxes/user_box.cr class UserBox < LuckyRecord :: Box def initialize name "Mikias" email "hello@mikias.net" encrypted_password Authentic . generate_encrypted_password ( "password" ) end end

Now we can generate a user in our test and make a post request to our sign_in endpoint using its email and password. And we'll check the response for the correct status code and Authorization header.



# spec/blog_api_spec.cr ... it "signs in valid user" do # create a user user = UserBox . new . create # visit sign in endpoint visitor . post ( "/sign_in" , ({ "sign_in:email" => user . email , "sign_in:password" => "password" })) # check response has status: 200 and authorization header with "Bearer" visitor . response . status_code . should eq 200 visitor . response . headers [ "Authorization" ]. should_not be_nil end ...

Now this test will fail because we don't have an action for this route or the forms to handle user creation, so let's build them.

Sign In

branch: jwt-auth-02-sign-in-form

If we generate a normal Lucky app it will come with Authentic already configured and several forms and actions will be generated for us. Currently, Lucky api configures Authentic but doesn't generate these files so we'll need to add them ourselves and update them to fit our use case.

Form

Let's start with the SignInForm which will be used to validate the user credentials, generate a token and return it in the Authorization header of the response. This form will be the same as the one generated by Authentic in new non-api apps, and we'll also need to create the form mixin FindAuthenticable which wasn't generated.



# src/forms/mixins/find_authenticable.cr module FindAuthenticatable private def find_authenticatable email . value . try do | value | UserQuery . new . email ( value ). first? end end end # src/forms/sign_in_form.cr class SignInForm < LuckyRecord :: VirtualForm include Authentic :: FormHelpers include FindAuthenticatable virtual email : String virtual password : String private def validate ( user : User ?) if user unless Authentic . correct_password? ( user , password . value . to_s ) password . add_error "is wrong" end else email . add_error "is not in our system" end end end

Action

branch: jwt-auth-03-complete-sign-in

Following Lucky's conventions we're going to create two actions:



lucky gen.action.api SignIn::Create

These commands will generate classes at src/actions/sign_up/create.cr and src/actions/sign_in/create.cr and two post routes to /sign_up and /sign_in .

Now we'll need a way to generate tokens from our user, we'll put this method in a GenerateToken mixin because we'll use it in several of our actions.



# src/actions/mixins/auth/generate_token.cr require "jwt" module GenerateToken def generate_token ( user ) exp = 14 . days . from_now . epoch data = ({ id: user . id , name: user . name , email: user . email }). to_s payload = { "sub" => user . id , "user" => Base64 . encode ( data ), "exp" => exp } JWT . encode ( payload , Lucky :: Server . settings . secret_key_base , "HS256" ) end end

We also need to make our User PasswordAuthenticatable for it to be used with Authentic . Optionally you can include Carbon::Emailable and the emailable method if you plan to send emails to your users on registration, password reset, etc.



# src/models/user.cr class User < BaseModel include Carbon :: Emailable include Authentic :: PasswordAuthenticatable table :users do column name : String column email : String column encrypted_password : String end def emailable Carbon :: Address . new ( email ) end end

Now we can include GenerateToken in our SignIn action and use our SignInForm to complete the authentication.



# src/actions/auth/sign_in.cr class SignIn::Create < ApiAction include GenerateToken route do SignInForm . new ( params ). submit do | form , user | if user context . response . headers . add "Authorization" , generate_token ( user ) head 200 else head 401 end end end end

Run the specs with crystal spec and voila! It works! :)

Sign Up

branch: jwt-auth-04-sign-up-test

I don't allow sign ups on my blog so I return head 401 for my SignIn action but of course you may want to implement it in yours. It's going to be very similar to the SignIn feature with some slight differences. Let's get to it.

Test

Let's begin by writing a test to create a user, making sure it returns the Authorization header and that we can query our new user from the database.



# spec/blog_api_spec.cr describe App do ... describe "auth" do ... it "creates user on sign up" do visitor . post ( "/sign_up" , ({ "sign_up:name" => "New User" , "sign_up:email" => "test@email.com" , "sign_up:password" => "password" , "sign_up:password_confirmation" => "password" })) visitor . response . status_code . should eq 200 visitor . response . headers [ "Authorization" ]. should_not be_nil UserQuery . new . email ( "test@email.com" ). first . should_not be_nil end ... end

Form

branch: jwt-auth-05-sign-up-form

Now our SignUpForm will need a PasswordValidations module to check the passwords, we'll create that first.



# src/forms/mixins/password_validations.cr module PasswordValidations private def run_password_validations validate_required password , password_confirmation validate_confirmation_of password , with: password_confirmation validate_size_of password , min: 6 end end

With that we can build our sign up form.



# src/forms/sign_up_form.cr class SignUpForm < User :: BaseForm include PasswordValidations fillable name , email virtual password : String virtual password_confirmation : String def prepare validate_uniqueness_of email run_password_validations Authentic . copy_and_encrypt password , to: encrypted_password end end

Action

branch: jwt-auth-06-complete-sign-up

With those two things done our we can create our SignUp::Create action which will look exactly the same as our SignIn::Create action. Run lucky gen.action.api SignUp::Create and fill it in:



# src/actions/sign_up/create.cr class SignUp::Create < ApiAction include GenerateToken route do SignUpForm . create ( params ) do | form , user | if user context . response . headers . add "Authorization" , generate_token ( user ) head 200 else head 401 end end end end

Now we can run our tests and watch them pass!

Protecting Routes

Great we can sign in and sign out, but what good does that do us if we can't protect our resources based on it? Since every action in lucky inerits from ApiAction or BrowserAction , it's very straight forward to build our own AuthenticatedAction that handles getting the current user from the Authorization header and returning head 401 if it's not valid.

Test

branch: jwt-auth-07-authenticated-action-test

First let's write test to make sure our feature works as expected. Since we are creating posts with this api, lets make sure that the endpoint is protected. We'll create two specs and update an older one that will be effected by our changes.

Make sure to include the GenerateToken module in our specs so we can mock an authenticated request.



# spec/blog_api_spec.cr describe App do include GenerateToken ... describe "/posts" do ... it "creates post" do user = UserBox . create visitor . post ( "/posts" , new_post_data , { "Authorization" => generate_token ( user ) }) visitor . response_body [ "title" ]. should eq "New Post" end end ... describe "auth" do ... it "allows authenticated users to create posts" do user = UserBox . create visitor . post ( "/posts" , new_post_data , { "Authorization" => generate_token ( user ) }) visitor . response_body [ "title" ]. should eq new_post_data [ "post:title" ] end it "rejects unauthenticated requests to protected actions" do visitor . post ( "/posts" , new_post_data ) visitor . response . status_code . should eq 401 end end end

Now our tests will definitely be failing so lets build our AuthenticatedAction to make them pass.

AuthenticatedAction

In order to do so we'll need a way to get the user from the token, so lets create a mixin called UserFromToken to do just that.

Note I chose to use mixins for generating and parsing tokens but you can also include these methods directly in the user model.



# src/actions/mixins/user_from_token.cr module UserFromToken def user_from_token ( token : String ) payload , _header = JWT . decode ( token , Lucky :: Server . settings . secret_key_base , "HS256" ) UserQuery . new . find ( payload [ "sub" ]. to_s ) end end

Now we can use that in our AuthenticatedAction class.



# src/actions/authenticated_action.cr abstract class AuthenticatedAction < ApiAction include UserFromToken before require_current_user getter current_user : User ? = nil private def require_current_user token = context . request . headers [ "Authorization" ]? if token . nil? head 401 else @current_user = user_from_token ( token ) end if @current_user . nil? head 401 else continue end rescue JWT :: ExpiredSignatureError head 401 end def current_user @current_user . not_nil! end end

So what's happening here? We use a callback before to run require_current_user before the action is called. In that method we get the user from the Authorization token and set it to the current_user getter. If there is no token, if the user doesn't exist or if the token is expired (raises JWT::ExpiredSignatureError ) we return 401 .

We also add a current_user method to alias our nilable getter for convenience in our actions.

Protect Actions

branch: jwt-auth-09-complete-authenticated-action

Now we can use it in our Posts::Create action.



class Posts::Create < AuthenticatedAction # changed this route do post = PostForm . create! ( params , author: current_user ) # and this json Posts :: ShowSerializer . new ( post ), Status :: Created end end

Now we can run our specs... and BOOM! Protected.

That's whats up.

Join Us

I hope you enjoyed this tutorial and found it useful. Join us on the Lucky gitter channel to stay up to date on the framework or checkout the docs for more information on how to bring your app idea to life with Lucky.