We learned how to implement serializers in the last part. To help ease the implementation of JWT and keep the controllers skinny, we will create a singleton class that helps encode and decode our JSON Web Tokens.
Feel free to refer to the part 9 branch of the GitHub repository if needed.
This is the 9th installment of my Developing A Cross-Platform iOS & Android Social Media App series.
Singleton classes are classes that are instantiated only once. This means that there can only be one instance of the class. It is encouraged to research if singleton classes are ideal for you. While it is argued to be a good way to DRY up your code, it is also argued to be an antipattern. For this app, we will use a singleton class.
Take a look at this example class:
class AccessToken
def encode
1
end
end
This is our mock AccessToken
class. It has the class name, and a method called encode
that returns 1
when called. In order to call the method, we need to create an instance of the class and then call the encode method.
2.6.5 :001 > token = AccessToken.new
=> #<AccessToken:0x00007fe5a98d9fc8>
2.6.5 :002 > token.encode
=> 1
token = AccessToken.new
: Creates a new instance of the AccessToken
class.
=> #<AccessToken:0x00007fe5a98d9fc8>
: This is the AccessToken
that was just instantiated. Each time we create a new AccessToken, it will have a new id.
token.encode
: This calls the encode
method. This can only be called on an instantiated AccessToken
otherwise it will return a NoMethodError
Here is an example of testing the ids for different instances of AccessToken:
2.6.5 :003 > token2 = AccessToken.new
=> #<AccessToken:0x00007fe5a83967d8>
2.6.5 :004 > token2.object_id
=> 7...6380
2.6.5 :005 > token.object_id
=> 7...4660
We can convert this to a singleton class by wrapping the methods under class << self
:
class AccessToken
class << self
def encode
1
end
end
end
Now we don't need to manually instantiate an AccessToken class in order to call encode
:
2.6.5 :001 > AccessToken.encode
=> 1
With some configuration, the AccessToken
singleton class will be accessible globally so that we can use it in our controllers.
First, create the access_token.rb
file under lib/
:
$ touch lib/access_token.rb
Input the following class and stub methods:
class AccessToken
class << self
def encode
1
end
def decode
2
end
end
end
After saving the file, we will need to autoload everything under lib/
. In Ruby you would need to require
every module, but Rails does this automatically for everything under app/
. Fortunately Rails gives us the option to configure which directories get autoloaded through the config.autoload_paths
option in config/application.rb
. Add the config.autoload_paths
line to your application config:
...
module SocialMediaBlogApi
class Application < Rails::Application
config.load_defaults 6.0
...
config.autoload_paths += Dir["#{config.root}/lib/**/"]
...
config.api_only = true
end
end
config.autoload_paths +=
: This is telling Rails we want to append more directories to our autoloaded paths.
Dir[...]
: This is Ruby's Dir
class that will output an array of directories of whatever is passed to it.
Dir["#{config.root}/lib/**/"]
: This returns an array of directories under /lib/
. If this was ran in the Rails console, you will get ["lib/tasks", "lib/access_token.rb"]
Now Rails will autoload our lib
modules whenever we restart the server or console. (You may need to restart the console every time you change a module in lib
)
Before we continue to work on the AccessToken
class, we should test it out using the rails console:
2.6.5 :001 > AccessToken
=> AccessToken
2.6.5 :002 > AccessToken.encode
=> 1
2.6.5 :003 > AccessToken.decode
=> 2
Awesome, it works! We can finally start the encode
method.
In part 7, we encoded a payload using JWT. We can reuse that code in this class. For reference, here is the code we used:
payload = {"user_id": 123}
exp = 1.days.from_now
payload[:exp] = exp.to_i
key = Rails.application.secrets.secret_key_base
encoded_payload = JWT.encode(payload, key)
decoded_jwt = JWT.decode(encoded_payload, key)
For the encode
method, we will only focus on the first 5 lines. We need to take payload
as a parameter and return the response from JWT.encode
:
class AccessToken
class << self
def encode(payload)
exp = 1.days.from_now
payload[:exp] = exp.to_i
key = Rails.application.secrets.secret_key_base
JWT.encode(payload, key)
end
def decode
2
end
end
end
We can also complete the decode method:
class AccessToken
class << self
def encode(payload)
exp = 1.days.from_now
payload[:exp] = exp.to_i
key = Rails.application.secrets.secret_key_base
JWT.encode(payload, key)
end
def decode(token)
key = Rails.application.secrets.secret_key_base
JWT.decode(token, key)
end
end
end
Start a new console using $ rails c
and try calling the AccessToken
methods:
2.6.5 :001 > payload = {user_id: 123}
=> {:user_id=>123}
2.6.5 :002 > token = AccessToken.encode(payload)
=> "e32...dds3"
2.6.5 :003 > AccessToken.decode(token)
=> [{"user_id"=>123, "exp"=>1582179883}, {"alg"=>"HS256"}]
Looks like these methods work, but we should create a method that returns a user using a token. Here is how the method would look:
def get_user_from_token(token)
begin
response = self.decode(token)
rescue JWT::VerificationError
return nil
end
payload = response[0]
user_id = payload['user_id']
User.find_by(id: user_id)
end
def get_user_from_token(token)
: This is a self-documenting method name for fetching the user from a token passed in.
begin...rescue...end
: This will capture exceptions for us. rescue JWT::VerificationError
catches all VerificationError
exceptions from JWT.decode
. Since AccessToken.decode
does not catch exceptions, it goes up into the get_user_from_token
method. Within rescue
we return nil
so that the method returns nothing since a user could not be found without a token.
response = self.decode(token)
: This assigns the response from the decode
method to response
. Methods can call other methods within the same class using self
.
payload = response[0]
: The decode
method returns an array that contains the payload
hash and the algorithim
hash. We would only need the first one, which has the user_id
in it.
user_id = payload['user_id']
: since this is a hash, we can directly pull the user_id
from the hash using the user_id
key.
User.find_by(id: user_id)
: This will hit the users
table in the database to fetch the user that has the id that is equal to user_id
.
We can test this out in the Rails console:
2.6.5 :001 > user_id = User.first.id
User Load (0.4ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1 [["LIMIT", 1]]
=> "0e6e9b1c-22f5-41a7-91bb-3da9808c7b3e"
2.6.5 :002 > payload = {user_id: user_id}
=> {:user_id=>"0e6e9b1c-22f5-41a7-91bb-3da9808c7b3e"}
2.6.5 :003 > token = AccessToken.encode(payload)
=> "e32...dds3"
2.6.5 :004 > AccessToken.get_user_from_token(token)
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", "0e6e9b1c-22f5-41a7-91bb-3da9808c7b3e"], ["LIMIT", 1]]
=> #<User id: "0e6e9b1c-22f5-41a7-91bb-3da9808c7b3e", email: "[email protected]", created_at: "2020-02-09 03:07:54", updated_at: "2020-02-09 03:07:54">
If everything goes well, AccessToken.get_user_from_token(token)
should return a User
. This will clean up a lot of our code in our controllers in the next section.