In the last section we created an AccessToken singleton class to help DRY up our code. In this section we will be utilizing that AccessToken class.
Feel free to refer to the part 10 branch of the GitHub repository if needed.
This is the 10th installment of my Developing A Cross-Platform iOS & Android Social Media App series.
The problem we are presented with is that we do not want to pass login information in every request. The only time a username and password should be passed in would be for the login
and register
endpoints. To solve this, we will pass in a JSON Web token in every request that isn't login/register. If no user is found, or the JWT is invalid/expired, the endpoint will return a 401
.
We will first need to provide the user with an access_token
when they login. Here is our current login
method:
def login
email = params[:user][:email]
password = params[:user][:password]
user = User.find_by(email: email)
is_valid = user && user.valid_password?(password)
unless is_valid
render json: {
status: 'error',
message: 'Invalid credentials'
}, status: 400 and return
end
return render json: user,
meta: {access_token: '123'},
status: 200
end
We still are hardcoding the access_token
. Fix that by updating the render with:
payload = {user_id: user.id}
access_token = AccessToken.encode(payload)
return render json: user,
meta: {access_token: access_token},
status: 200
payload = {user_id: user.id}
: creates a variable named payload
that contains the user's id from the user
object declared above
access_token = AccessToken.encode(payload)
: We use our AccessToken
singleton class to encode our payload
and assign it to access_token
.
meta: {access_token: access_token},
: replaces our hardcoded access_token
with the correct access token.
Now, when we hit the login
endpoint with curl
, we get the access_token
(save it for later):
$ curl -H "Content-Type: application/json" -X POST -d '{"user":{"email": "[email protected]","password":"123456"}}' http://localhost:3000/v1/users/login | json_pp
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 307 0 256 100 51 1363 271 --:--:-- --:--:-- --:--:-- 1368
{
"user" : {
"id" : "7545d290-962f-45bb-891d-f7e43b4fbf68",
"email" : "[email protected]"
},
"meta" : {
"access_token" : "eyz1...5dX"
}
}
Don't forget that our UserController
inherits ApplicationController
:
class V1::UsersController < ApplicationController
...
end
This means that we can handle the access_token
logic in our ApplicationController
by validating the Authorization
header.
An Authorization
header can be passed in to our request so that APIs will know who is making the request. For JWT, these are usually in the format of Bearer JSON_WEB_TOKEN
. We can append an Authorization
header to our request by adding -H "Authorization: Bearer eyz1...5dX"
to our curl
command.
Rails can now access this by using request.headers['Authorization']
. Lets take a look at how we can verify the auth header. Create the following private method in your app/controllers/application_controller.rb
:
class ApplicationController < ActionController::API
private
def authenticate_request
auth_header = request.headers['Authorization']
regex = /^Bearer /
auth_header = auth_header.gsub(regex, '') if auth_header
@current_user = AccessToken.get_user_from_token(auth_header)
render json: { error: 'Not Authorized' }, status: 401 unless @current_user
end
end
private
: Declares a private method.
def authenticate_request
: The name of our method, we will call this method soon.
auth_header = request.headers['Authorization']
: Pulls the Authorization
header from the headers (request.headers
) and assigns it to auth_header
.
regex = /^Bearer /
: Remember that the Authorization
header is in this format: Bearer AUTH_TOKEN
. We only need the token itself, so we will remove the Bearer
from the string using regex. In Ruby, we can declare regex between /.../
. The caret (^
) signifies that it will match the string if it starts with whatever comes after the ^
. In this case ^Bearer
will match anything that starts with Bearer
. For example /^abc/
will match abcd
but not acd
or aabc
. (Do not forget the space in /^Bearer /
!)
auth_header = auth_header.gsub(regex, '')
: gsub
allows us to substitute all occurrences of a pattern provided (our regex
variable) and substitutes it with a second string (''
is equivalent to removing the occurrences). We then assign the new string to auth_header
. This essentially removes Bearer
from our token.
if auth_header
: Adding an if
or unless
statement to the end of a line is a shorthand for wrapping that line with an if
or unless
block. So ...auth_header.gsub(...)
will only run if auth_header
is not nil
or ''
.
@current_user =
: This creates an instance variable which means that we can use this variable in any method within the class. For example, if we want to return all of the user's posts, we can just write @current_user.posts
in an endpoint.
AccessToken.get_user_from_token(auth_header)
: This called our singleton class to return the user from the token passed in.
render json: { error: 'Not Authorized' }, status: 401
: This is the error message if the authorization fails. We will use a 401
HTTP status.
unless @current_user
: An error response will be returned unless @current_user
exists.
So when does this method run? Rails provides us with a before_action
option that lets us specify what actions should be ran before each API call. In this case we want to use our authenticate_request
method:
class ApplicationController < ActionController::API
before_action :authenticate_request
private
def authenticate_request
...
end
end
We are almost done. The issue we have is that the API will return Not Authorized
for our login
and register
endpoints since it requires an access token. There is no way for a user to have their access_token
without those endpoints. Rails provides us with a skip_before_action
option for controllers. Add it for the login
and register
endpoints in user_controller.rb
:
class V1::UsersController < ApplicationController
skip_before_action :authenticate_request, only: [:login, :register]
...
end
skip_before_action
: This takes in the actions as a parameter and does not run those before our endpoints.
:authenticate_request
: The method we want to skip
only: [:login, :register]
: The only
parameter lets us set which specific endpoints that we do not want to implement JWT
auth.
Create a test endpoint in the user_controller
:
class V1::UsersController < ApplicationController
skip_before_action :authenticate_request, only: [:login, :register]
...
def test
render json: @current_user, status: 200
end
...
end
This endpoint is very simple, it just returns the @current_user
instance variable from our JWT auth method. Update the routes.rb
:
Rails.application.routes.draw do
concern :base_api do
post 'users/register', to: 'users#register'
post 'users/login', to: 'users#login'
get 'users/test', to: 'users#test'
end
namespace :v1 do
concerns :base_api
end
end
Restart your server using ctrl+c
and $ rails c
. We can hit the /v1/test
endpoint using curl
. If you do not have your access_token
, hit the login
endpoint again and copy it.
curl -H "Content-Type: application/json" -X POST -d '{"user":{"email": "[email protected]","password":"123456"}}' http://localhost:3000/v1/users/login | json_pp
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 307 0 256 100 51 1041 207 --:--:-- --:--:-- --:--:-- 1044
{
"meta" : {
"access_token" : "eY23...fD2"
},
"user" : {
"id" : "7545d290-962f-45bb-891d-f7e43b4fbf68",
"email" : "[email protected]"
}
}
Pass the access_token as an Authorization
header via -H "Authorization: Bearer eY23...fD2"
, change POST
to GET
, and change the endpoint to test
:
curl -H "Content-Type: application/json" -H "Authorization: Bearer eY23...fD2" -X GET http://localhost:3000/v1/users/test | json_pp
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 130 0 79 100 51 5923 3824 --:--:-- --:--:-- --:--:-- 6076
{
"user" : {
"email" : "[email protected]",
"id" : "0e6e9b1c-22f5-41a7-91bb-3da9808c7b3e"
}
}
If everything works, you're finally done! At this point we should have a very solid foundation for a Rails API. This is a good stopping point for us to introduce tests, which we will cover in the next section.
Part 11 (Creating Rails Tests) will be released soon. Please check back later.