DEV Community

loading...
Cover image for localStorage with JWT Web Tokens

localStorage with JWT Web Tokens

matthewpalmer9 profile image Matthew Palmer ・5 min read

Introduction

Security in web applications is a must. Especially when you're scaling up with a gigantic platform. Securing your websites doesn't make them invulnerable, but they certainly do the job of preventing as many disasters down the line as possible. Even tech giants like Facebook, Twitter, Google and Amazon have experienced a security breach at one point or another. So today, let's talk about one of my favorites -- JWT Web Tokens with localStorage. For the sake of this blog, you'll need to be familiar with Ruby on Rails.

What are JWT Web Tokens?

They are JSON Web Tokens. In other words, they are encrypted "keys" to all the doors a user can open with their account. Including the most important -- signing into it! It's basically a handshake between the client and server that says, "Okay, you are who you say you are. You are permitted to perform this action." Sound good? Awesome, let's dive in further.

Ruby on Rails

Let's assume you're a Rubyist! I hope you are, at least. It is a stupid easy-to-use backend API. The learning curve isn't difficult at all. Especially if you're already familiar with MVCs.

To set up JWT Web Tokens, you'll want to add gem 'jwt' to your Rails dependencies and run bundle install.

Now, let's learn how to use this gem!

application_controller.rb:

This is what your application controller should look like. Pay close attention to the notes as I've explained what is happening at each method:

class ApplicationController < ActionController::API
    # Pay close attention... "authorized" is invoked first
    before_action :authorized

    # When this method encode_token is invoked later, 
    # an object is sent as the argument.
    # The method encode provided by the JWT gem will be utilized to generate a token. 
    # JWT.encode takes two arguments, the payload object and a string that represents a “secret”. 
    # The method encode_token will return the token generated by JWT.
    # To better understand how this works, look at the "authorized" method next.
    def encode_token(payload)
        JWT.encode(payload, 's3cr3t')
    end 

    def auth_header
        request.headers['Authorization'] 
        # Checking the "Authorization" key in headers
        # Ideally, a token generated by JWT in format of Bearer <token>
        # auth_header returns this
    end

    # auth_header 
    def decode_token
        if auth_header
            token = auth_header.split(' ')[1]
            # To get just the token, we will use the .split(" ")[1] method on 
            # Bearer <token> . Once the token is grabbed and assigned to the token 
            # variable, the decode method provided by JWT will be utilized.

            begin
                JWT.decode(token, 's3cr3t', true, algorithm: 'HS256')
                # JWT.decode takes in four arguments:
                # the token to decode, the secret string that was assigned when encoding, 
                # a true value, and the algorithm (‘HS256’ here). 
                # Otherwise, if there is an error, it will return nil.
            rescue JWT::DecodeError
                nil
            end
        end 
    end 

    # decoded_token is another method above being called, which calls auth_header
    def logged_in_user
        if decode_token
            user_id = decoded_token[0]['user_id']
            @user = User.find_by(id: user_id)
        end 
    end 

    # Great, now this method is checking another method above... logged_in_user;
    # true or false? (Boolean) ^
    def logged_in?
        !!logged_in_user
    end 

    # This method is invoked first, but is dependent on a chain of other methods above.
    # If a user is not logged in or a request is not sending the necessary credentials, 
    # this method will send back a JSON response, asking them to log in. To determine that 
    # information, the method logged_in? is called. Check that...
    def authorized
        render json: { message: 'Please log in'}, status: :unauthorized unless logged_in?
    end
end
Enter fullscreen mode Exit fullscreen mode

Whew! That's a lot going on in there. Believe me, this is the hardest part. Read it over, code it out a few times and it will all make perfect sense. Save it in a personal repo too! This is going to be the same code snippet in any application you write with Rails for JWT Web Tokens shy of the algorithm -- that part is up to you!

Onward!

How To USE Them!

Let's go with a basic user_controller.rb.
Take a look at this code:

class UsersController < ApplicationController
    # Invoked if ANY route is accessed in the application,
    # ... but only specific to the auto_login route.
    before_action :authorized, only: [:auto_login]

    # REGISTER
    def create 
        user = User.create(user_params)
        if user.valid?
            token = encode_token({user_id: @user.id})
            render json: {user: user, token: token}
        else 
            render json: {error: "Invalid username or password"}
        end 
    end 

    # LOGGING IN
    def login
        user = User.find_by(username: params[:username])

        if user&.authenticate(params[:password])
            token = encode_token({user_id: @user.id})
            render json: {user: @user, token: token}
        else 
            render json: {error: "Invalid username or password"}
        end 
    end

    # There’s really not much going on here. The big question is where the variable user comes from?
    # Since the method, authorized, will run before auto_login, the chain of methods in the application 
    # controller will also run. One of the methods, logged_in_user, will return a global @user variable 
    # that is accessible.
    def auto_login 
        render json: @user
    end 

    private

    def user_params
        params.require(:user).permit(:username, :password, :age)
    end 
end
Enter fullscreen mode Exit fullscreen mode

As you can see, we have access to the JWT methods as defined and inherited from application_controller.rb. We assign our tokens based on a user's ID. A token is issued and will belong to that user to validate any incoming requests to the API.

localStorage

When you receive the response from a server via fetch or axios (whichever one you use), the JWT Web Token will come with it. locaStorage allows us to store this information accessible only to your machine/browser. Since this information isn't available anywhere else AND it persists all data even when the browser is fully closed, it's a great place to store a user's information to keep them authenticated.

Let's assume you just received your JWT token, and it is accessible via data.jwt. We can store this information like so:

localStorage.setItem('token', data.jwt)

And to access this token, we can use:

localStorage.getItem('token')

How you use this information is dependent on what library/framework you're using on the frontend. I use React, but explaining client storage in React is a whole blog post of its own...

Conclusion

JWT Web Tokens are a great way to make your applications more secure. There are better ways to work with JWT Tokens. In fact, here is an article that you should follow once you've gotten comfortable with how JWT Web Tokens work. localStorage is okay when it comes to React since we are passing data in a slightly different way across the application, but not all tools are created equal! The stronger your security, the better.

Happy coding, y'all!

Discussion (7)

pic
Editor guide
Collapse
torstendittmann profile image
Torsten Dittmann

Great article 👌

I would recommend to save the JWT in a cookie with HttpOnly set. This will be more secure, since localStorage is readable by JavaScript.

Collapse
exanup profile image
Anup Dhakal

I agree, except the part about "more secure". They are just "differently secure/insecure" if that makes sense. While localStorage is vulnerable to XSS attacks, Cookies are not safe from CSRF attacks either. There are ways to strengthen the both. They have other differences as well. Just use whatever is more convenient / suitable for your use-case. Having a soundly secure JWT setup is more important IMHO.

And if you are worried about having some malicious JS (from a compromised library) stealing your tokens, while cookies prevent them from getting the tokens, they cannot prevent the malicious code to act on your behalf anyway!

Collapse
torstendittmann profile image
Torsten Dittmann • Edited

You are right, but there is ways to prevent the cookie from being exposed (SameSite, etc). localStorage is always open 😊

Collapse
abooayoob profile image
Mohammad Ali Khan

Ditto, localStorage is convenient, but if any of your js dependencies or third party scripts is compromised, that localStorage is up for grabs.

Collapse
greedybrain profile image
Naya Willis

Great job my man. I've implemented it with Node, and I must say its really fun lol.

Collapse
matthewpalmer9 profile image
Matthew Palmer Author

Thanks man! I'm interested in how it works with Express too. If you find some time, you should write a blog about it and send me a link to it! 😁 I'd love to read it.

Collapse
greedybrain profile image
Naya Willis

Will do