Creating Auth Endpoints

Creating authentication endpoints for our User models

A Quick Recap

In the previous article we created a User record using the Rails console.

You are now ready for the next step: setting up endpoints for authentication. Feel free to refer to the part 5 branch of the GitHub repository if needed.

This is the 5th installment of my Developing A Cross-Platform iOS & Android Social Media App series.

Overview

We are now going to be setting up the authenthication endpoints for our API. By the end of this article, you will have the following two endpoints:

/users/register: Takes in email, password and password_confirmation as parameters. If all params are valid, then creates a User record and returns the User in the json response.

/users/login: Takes in an existing email and password and if correct, it will return that user's information.

Note: we will never send plain text passwords to this API, even if it is over HTTPS. It is not that much more effort to salt and hash a password. We won't ever need to know the original password so we should not store it anywhere. But for testing these endpoints, it doesn't matter much.

What is a Rails Controller?

In simple terms, a controller is a Ruby file with methods that recieve parameters and outputs a response. In other words, it connects the frontend to the database. It does much more than that, but this is a solid high level perspective of it. We can route urls such as https://localhost:3000/users/register to methods in a controller such as the register method. We will cover more on this soon.

Creating A Controller

Rails provides us a command to create a controller automatically. Lets run $ rails generate controller users

$ rails generate controller users
      create  app/controllers/users_controller.rb
      invoke  test_unit
      create    test/controllers/users_controller_test.rb

This created a User Controller and a User Controller Test. Note: We will cover tests soon

Open the app/controllers/users_controller.rb file:

class UsersController < ApplicationController
end

From this file we can see that we have a UsersController that inherits the ApplicationController from app/controllers/application_controller.rb. We will cover the purpose of ApplicationController in a future post.

Right now, the Users Controller is a blank canvas, lets add a register method stub:

class ApplicationController < ActionController::API
  def register
  end
end

Strong Parameters

A user can pass any amount of parameters to an endpoint. In a perfect world, we can just have User.new(params) which will assign the parameters to the respective attributes. But what if we had an admin attribute and a user adds admin=true to the request params? The user could assign any attribute they want in that scenario. To prevent unwanted parameters to be passed to User.new(), we will be using strong parameters.

The controller has a variable called params that refers to the parameters passed to it. We have two important variables that we can use with the params variable: require and permit. Here is what they do:

require: Throws an error if this key doesn't exist in the params

permit: These are optional keys, it won't throw an error if it doesn't exist but it also will filter out anything that isn't listed in here.

The only attributes we want to pass to User.new() are email, password, and password_confirmation. So lets create a private method called user_params to setup our strong parameters:

class ApplicationController < ActionController::API
  def register
  end

  private
  
  def user_params
    params.require(:user).permit(
      :email,
      :password,
      :password_confirmation,
    )
  end
end

The user_params method can now be called instead of params. We now can be confident that when we call User.new(user_params[:user]) the only attributes that will be assigned are email, password and password_confirmation.

Lets take a look at example payloads that can be passed to a request and what the user_params would be. Here is an example JSON payload:

{
  "user": {
    "email": "[email protected]",
    "password": "123456",
    "password_confirmation": "123456",
    "admin": true
  }
}

Here is what user_params would return:

{
  "user": {
    "email": "[email protected]",
    "password": "123456",
    "password_confirmation": "123456"
  }
}

Notice how the admin key/value is removed. Lets look at this example:

{
  "email": "[email protected]",
  "password": "123456",
  "password_confirmation": "123456"
}

All of the attributes are there but it is not nested under a user key. So this means that require would raise an exception since user does not exist in params.

Creating The Register Endpoint

Now that we have our user_params method, we can now finish our register endpoint. Lets update the register method:

def register
  user = User.new(user_params)
end

Now we need to save the new user. We need to handle a success and error state:

def register
  user = User.new(user_params)
  if user.valid? && user.save
    # render success state
  end
  # render error state
end

In order for a success state to be rendered, the method will attempt to save the user after it verifies that it is valid. If the user is invalid or if the save faiils, it will render an error state.

We need to replace the render comments with actual renders. If registration is successful, we will return the user.

def register
  user = User.create(user_params)
  if user.valid? && user.save
    render json: user,
      status: 201
    return
  end
  # render error state
end

This line does a lot for us, so lets disect it line by line:

render: this method tells the endpoint what we are going to render as the response

json: user,: this sets the response to be a JSON and automatically converts the user model into JSON. In a future part, we will go over serializers which will let us customize the user response.

status: 201: 201 is a HTTP status for a successful creation. You can view more HTTP statuses here.

return: Since the render method does not stop the next lines of code from being executed, we need to use return so that we dont end up with a double render error.

If the user is invalid or the save fails, User.errors will provide error info. Update your register method to render a response when an error exists:

def register
  user = User.create(user_params)
  if user.valid? && user.save
    render json: user,
      status: 201
    return
  end
  render json: user.errors,
    status: 400
end

We used a 400 status response because invalid params falls under the general bad request category. We don't need a return as it is already at the end of the method and no code will be executed after this render. Your completed users_controller.rb should now look like:

class ApplicationController < ActionController::API
  def register
    user = User.create(user_params)
    if user.valid? && user.save
      render json: user,
        status: 201
      return
    end
    render json: user.errors,
      status: 401
  end

  private

  def user_params
    params.require(:user).permit(
      :email,
      :password,
      :password_confirmation,
    )
  end
end

Creating Our Route

We have one final step for our register endpoint to be ready: configuring the routes file. Open config/routes.rb:

Rails.application.routes.draw do
  devise_for :users
end

We will be creating our own API-only auth routes, so delete devise_for :users. For the registration route, we want to use /user/register. Since the user will be sending data and expecting a user to be created, we will use a POST request.

Rails.application.routes.draw do
  post 'users/register', to: 'users#register'
end

Here is a breakdown of what is going on:

post: This tells us what HTTP request method needs to be made. post is usually for creating data, get is for retrieving data, and delete for deleting data. Mozilla has documentation on HTTP request methods.

'users/register',: this tells Rails what the url should be. So this line would create the route: http://localhost:3000/users/register

to:: This tells Rails that when a user sends a request to the users/register url, call the action after to:

'users#register': This is the controller and action that the endpoint would direct to. So users would look for users_controller and register looks for the register method inside users_controller

Note: there is a much cleaner approach for basic CRUD endpoints, we will cover this in the future

Once you save the file, verify that the route has been created by running $ rake routes

$ rake routes
        Prefix Verb   URI Pattern                Controller#Action
users_register POST   /users/register(.:format)  users#register

You'll see a lot more routes, but this is the only one we care about. If you see this, you're ready to test the endpoint!

Testing The Register Endpoint

For this tutorial, we will use curl to hit our endpoints. But it is highly suggested to take a look at a REST client such as Insominia

$ curl -H "Content-Type: application/json" -X POST -d '{"user":{"email": "[email protected]","password":"123456","password_confirmation": "123456"}}' http://localhost:3000/users/register
{"id":"666b9e2b-da4a-4dbb-8fcc-aecc760c2f6f","email":"[email protected]","created_at":"2020-02-09T17:19:33.376Z","updated_at":"2020-02-09T17:19:33.376Z"}%

A little hard to read, but you can pipe the JSON response and pretty print it using json_pp by appending | json_pp to the end of it:

$ curl -H "Content-Type: application/json" -X POST -d '{"user":{"email": "[email protected]","password":"123456","password_confirmation": "123456"}}' http://localhost:3000/users/register | json_pp
% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   236    0   148  100    88    743    442 --:--:-- --:--:-- --:--:--   747
{
   "id" : "4edc9a20-1e61-4a82-bb97-ee6647ff8212",
   "updated_at" : "2020-02-09T17:24:53.319Z",
   "created_at" : "2020-02-09T17:24:53.319Z",
   "email" : "[email protected]"
}

Here is a breakdown of what is going on:

curl: This is the command that we will be using to send data to our server and recieve a response. You can learn more about curl by running man curl on your terminal.

-H: This is the option for the curl command to set the Header of the request

"Content-Type: application/json": This is a header that tells the API we are serving JSON data to it. We are using the application/json MIME type.

-X: is used to set the request method that we are about to call.

POST: This is the request method we are setting for our request.

-d: This is an option for us to send data to the curl command so that it can include it in the request

'{"user": ...}': this is the data that is being served

http://localhost:3000/users/register: This is the url that we are sending the request to. We are running the server locally and it runs on port 3000 on default.

| json_pp: We pipe the result of the curl command to json_pp so that it can pretty print the json response.

Awesome! Our endpoint works and we can create new users. You can verify this in the Rails console:

$ rails c
Loading development environment (Rails 6.0.2.1)
2.6.5 :001 > User.find_by(email: "[email protected]")
  User Load (0.9ms)  SELECT "users".* FROM "users" WHERE "users"."email" = $1 LIMIT $2  [["email", "[email protected]"], ["LIMIT", 1]]
 => #<User id: "4edc9a20-1e61-4a82-bb97-ee6647ff8212", email: "[email protected]", created_at: "2020-02-09 17:24:53", updated_at: "2020-02-09 17:24:53">

We have one more endpoint left: the login endpoint.

Creating The Login Endpoint

For the login endpoint, we will take email and password as parameters and return the user if the credentials are correct. We will make this action return a JWT token in the future, but this is suffice for now.

Remember that we can find a user using the find_by command:

2.6.5 :001 > user = User.find_by(email: "[email protected]")
  User Load (0.9ms)  SELECT "users".* FROM "users" WHERE "users"."email" = $1 LIMIT $2  [["email", "[email protected]"], ["LIMIT", 1]]
 => #<User id: "4edc9a20-1e61-4a82-bb97-ee6647ff8212", email: "[email protected]", created_at: "2020-02-09 17:24:53", updated_at: "2020-02-09 17:24:53">

We can also validate the password using valid_password?

2.6.5 :005 > user.valid_password?("123456")
 => true

With this information, we can create the login endpoint in users_controller.rb:

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,
    status: 200
end

We extract the email and password into variables by pulling them from the user object that is passed in through the JSON request body. params[:user] returns the user object and [:email] pulls the value of the email key inside the user object. So our request body would look like:

{
  "user": {
    "email": "[email protected]",
    "password": "123123"
  }
}

Alternatively we could just do:

{
  "email": "[email protected]",
  "password": "123123"
}

But I like to keep my code as consistent as possible. It helps with readability and maintainability, not only for you, but for other developers as well. You should always write clean code that others can pickup and understand.

However, we won't get an error if we do go that route above. We are not using the user_params method, so .requre(:user) won't be ran. We won't need to filter out any params via .require and .permit as we are directly extracting the email and password keys from the params hash.

User.find_by(email: email) we find the user with the associated email, keep in mind that if nothing is found, it will return nil which is falsey.

user && user.valid_password?(password) is equal to true if user exists (not nil) and if the valid_password? password method returns true. Note: valid_password? will not run if user does not exist so we won't get an error calling nil.valid_password?. This is because Ruby knows that it won't need to run the whole conditional statement if the first conditional is false.

unless is_valid is the same as if !is_valid. Ruby focuses on being easy to read, so according to the Rubocop Style Guide, we should use unless. So unless is_valid is true, run the nested render method.

We handle both an invalid email and invalid password error under a single Invalid credentials' error becuase it will allow someone to bruteforce a list of emails and passwords easier. A hacker could send requests through proxies and log any email that returns an Invalid Email response. They could also figure out which emails exist and bruteforce a specific account as long as it keeps getting an Invalid password error. You may or may not ever come into this issue but it is a simple adjustment and protects your endpoints.follo

Your entire app/controllers/users_controller.rb file should now look like:

class UsersController < ApplicationController
  def register
    user = User.create(user_params)
    if user.valid? && user.save
      render json: user,
        adapter: :json ,
        status: 201 and return
    end
    render json: user.errors, status: 400
  end

  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,
      status: 200
  end

  private

  def user_params
    params.require(:user).permit(
      :email,
      :password,
      :password_confirmation,
    )
  end
end

Updating The Routes

We need to add the new login route to our config/routes.rb file:

Rails.application.routes.draw do
  post 'users/register', to: 'users#register'
  post 'users/login', to: 'users#login'
end

We are using a post for this as well. If you chose to not have a user object for the login, I still recommend a post instead of a get request. The get request will show the email and password params in the url itself: users/login/?email="[email protected]"&password="123456".

It is a bad habit to include any sensitive data in a get request because the whole url may end up in logs, or history. A post isn't any more secure than a get request other than the sensitive data is moved to the request body. When dealing with sensitive data, it should always go through HTTPS. The request method does not secure it, but SSL does.

Run rake routes to verify that the route has been created.

$ rake routes
        Prefix Verb   URI Pattern                Controller#Action
users_register POST   /users/register(.:format)  users#register
users_login    POST   /users/login(.:format)     users#login

Testing The Login Endpoint

Make sure your server is running using $ rails s and enter the following curl command to test if our endpoint works. Be sure to update the login credentials to the right credentials.

curl -H "Content-Type: application/json" -X POST -d '{"user":{"email": "[email protected]","password":"123456"}}' http://localhost:3000/users/login | json_pp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   202    0   148  100    54    135     49  0:00:01  0:00:01 --:--:--   135
{
   "created_at" : "2020-02-09T17:24:53.319Z",
   "email" : "[email protected]",
   "updated_at" : "2020-02-09T17:24:53.319Z",
   "id" : "4edc9a20-1e61-4a82-bb97-ee6647ff8212"
}

It works! Now that we got our endpoints in order, check out the Rails server terminal to see what it outputted once we ran that login command:

Started POST "/users/login" for ::1 at 2020-02-09 10:45:24 -0800
Processing by UsersController#login as */*
  Parameters: {"user"=>{"email"=>"[email protected]", "password"=>"[FILTERED]"}}
  User Load (1.0ms)  SELECT "users".* FROM "users" WHERE "users"."email" = $1 LIMIT $2  [["email", "[email protected]"], ["LIMIT", 1]]
  ↳ app/controllers/users_controller.rb:15:in `login'
Completed 200 OK in 159ms (Views: 8.6ms | ActiveRecord: 1.9ms | Allocations: 3940)

This is where you will see realtime errors and logs. From this we can tell a POST request was made to users/login and follow along to see which controller it called, parameters that were passed, SQL queries that were made, the line where the response was rendered, the status code, and more. This is very useful information that we will come across in the future.

What's Next?

You have now made your first Rails endpoints! We will be improving these endpoints in the next part by implementing API Versions.