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
endWe 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: 200payload = {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
...
endThis 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]
...
endskip_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
...
endThis 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
endRestart 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.