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
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
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!
Top comments (7)
Great article 👌
I would recommend to save the JWT in a cookie with
HttpOnly
set. This will be more secure, sincelocalStorage
is readable by JavaScript.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!
You are right, but there is ways to prevent the cookie from being exposed (SameSite, etc).
localStorage
is always open 😊Ditto, localStorage is convenient, but if any of your js dependencies or third party scripts is compromised, that localStorage is up for grabs.
Great job my man. I've implemented it with Node, and I must say its really fun lol.
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.
Will do