Authorization

Pre-Work

Make sure you have read about Authentication, Authorization, and Sessions before you start this lesson!

Warmup

In small groups, discuss the following:

  • What is the difference between Authentication and Authorization?
  • Describe the user flow that you experienced when signing up for an API key with pexels.com
    • How do you retrieve your API key?
    • How does pexels know which API key to show you?

In your homework, you learned the difference between Authentication and Authorization. In this lesson, we will implement both in a Rails API so that we can control who has access to specific endpoints.

Learning Goals

  • Implement token-based authentication in an API
  • Deepen our understanding of authentication and authorization

User Registration

In order to control what actions are allowed in our API, we will need to have some way of identifying who is trying to access our application, and what information or actions they should have access to.

We will be using user-specific authentication tokens to manage this process; which means, we will need users in our application!

Code Exploration in Small Groups

Checkout the auth-starter branch from the SetListAPI repo, and explore the code by answering the following questions:

  • What new tables do you see in our scheme?
  • How does a user get an authentication token?
  • How do we ensure that a user is never created without an authentication token?
  • Do we store user’s passwords in our database?
    • for this question, you might want to explore the bcrypt docs!

Common API Authentication Strategies

In the past, you have used keys in your API requests to places like pexels.com. Now, we are going to update our API to require a key to gain access to our data.

We are using ruby’s SecureRandom module to generate individual keys that are tied to specific users. This is not the only way to implement authorization with our APIs. Can you imagine a few other ways?

Token vs. Key

These terms are often used interchangeably, which is okay! Both are generally unique, hard-to-guess (or maybe encrypted) codes that can be used to authenticate users and authorize certain operations. The main difference is around time - while tokens are generally short-lived and could expire after a short period of time or a logout, keys often are long-lived unless the server regenerates or revokes them.

User Roles

Let’s use TDD to drive the implementation of authenticating api requests, and controlling for specific user-roles. By the end of this lesson, our application will adhere to the following user-stories:

As an API consumer
When I make an api request without an API key in the header,
Then I am sent an error code and message, 
And I am directed to register as a user
As a registered API consumper
When I make an api request with an API key in the header to
  GET "/api/v1/songs"
Then I see a json response will all songs
As a registered API consumer
When I make an API request with an API key in the header to
  GET "/api/v1/songs/1"
Then I see a json response with 1 song
As a registered API consumer
When I make an api request with an API key in the header to
  POST "/api/v1/songs/"
Then I see a json response with a 405 status
As an admin user
When I make an API request with an API key in the header to
  POST "/api/v1/songs/" {'title':'Happy', 'length':'325', 'play_count':'3'}
Then I see a json response with my created song
  And the song is saved in the database

Update the spec/requests/v1/songs_requests_spec.rb to test for authenticated users; you may copy/paste these tests to replace the existing tests.

Control for Authorized Users

Because we want to control for all of our songs endpoints, we can use a before_action callback to authenticate each request.

# app/controllers/api/v1/songs_controller.rb

class Api::V1::SongsController < ApplicationController
  before_action :authenticate_user

  ...

  private

  def authenticate_user
    token = request.headers['Authorization']
    @current_user = User.find_by(api_token: token)
    
    if @current_user.nil?
      render json: { error: 'Unauthorized, please register as a user' }, status: :unauthorized
    end
  end
end

We will see the @current_user variable come back into play after we get user roles set up!

User Role

We generally would make the user role an integer value so we’re not storing a string over and over, and we can tell Ruby to use a lookup table called an “enum” (short for enumerable) to convert that number to a string later.

How we order these values doesn’t really matter, but it’s important to note that we generally only add to the END of our enumerable list. If we add something in the middle of the list, we might accidentally change other roles, and that can get really confusing.

Rails also has some neat “magic” about using these enum strings to build validation routines that we’ll see in a moment.

Add a new role field

Make a migration to add a role field for a user, which is an integer field:

$ rails g migration AddRoleToUsers role:integer

The migration should look something like this down below. Be sure to set the default to 0, which we will set to be a “default” user, like a regular user that has no special access.

class AddRoleToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :role, :integer, default: 0
  end
end

And let us go ahead and apply this change.

$ rails db:migrate

Now, in our User model, we need to specify our list of enumerable strings for the Roles that we have created.

app/models/user.rb

class User < ApplicationRecord
    has_secure_password

    enum role: %w(default admin)

    before_create :generate_api_token

    validates :email, presence: true, uniqueness: true

    private

    def generate_api_token
        self.api_token = SecureRandom.hex(20)
    end
end

What this does is that it will give us access to some really useful helper methods, like this:

# look up user 1
user = User.find(1)

# is user a default user?
if user.default?
  # default user!
elsif user.admin?
  # user is an admin
else
  # we don't know what kind of user they are?!
end

Remember that earlier, we had made it in our migration that our database would set the role to 0 if we didn’t set the role explicitly on our site, so we REALLY want to make sure that we are using strong params when we are creating users to make sure that we are NOT letting the role property to be transferred in as a form parameter.

Control for Admin User

Now that we have implemented roles for our users, we can control who has access to perform certain actions:

class Api::V1::SongsController < ApplicationController
  before_action :authenticate_user

  ...

  def create
    if @current_user.admin?
      render json: Song.create(song_params) 
    else
      render status: :method_not_allowed
    end
  end

  ...

  private
  ...

end

At this point, we have satisfied the user stories outlined at the beginning of the lesson!

Solidify your learning by implementing the following:

What About Sessions?

In the pre-work for this lesson, you learned about sessions - they help an application maintain state for a user by passing session information back and forth in the request and response headers. This allows us to stay logged in on websites like Amazon, Gmail, etc…

But, for API consumption, the session is largely irrelevant because we expect the consumers of our APIs (whether human or application) to tell us who they are with each request.

Checks for Understanding (aka possible interview questions!)

  • Describe the difference between authentication and authorization.
  • Outline the design of an ebay copycat application:
    • what resources might exist?
    • what routes might exist?
    • How many roles would you need?
    • what endpoints would be useful?
    • how might name-spacing be used to structure the design?
    • How might you implement authorization to prevent unauthorized users from performing certain operations?

Completed code for this lesson can be found on the auth-complete branch of the SetListAPI repo

Lesson Search Results

Showing top 10 results