The Rails Guide to Securing an API

In this tutorial we are going down a different track then our last Ruby Post (bad pun intended). Instead diving into building a very simple API that, of course, we will secure with access tokens minted by our very own Okta OAuth server. We’ll make requests to this API via Postman to keep things nice and simple. Now let’s get chugging along. (OK, that’s the last pun for a bit.)

Prerequisites for this blog post include:

Now let’s get started!

Build the API

Open up the terminal and create a brand new Rails application:

rails new okta_protected_api
cd okta_okta_protected_api

Now let’s install the JWT gem. Open your gemfile and add this one line:

gem 'jwt'

Like so:

Now let’s run bundle install in the terminal:

bundle install

Now let’s create a route. For this post, I’ll have an API that returns anime I am watching or I am excited about, so I will call it animes. Add this line to the config/routes.rb.

resources :animes, only: [:index]

And now let’s create a controller.

cd app/controllers
touch anime_controller.rb

Now provide the code for the controller.

class AnimesController < ApplicationController
    def index
      animes = ["Haikyu", "The Great Pretender", "Jujutsu kaisen", "Dr. Stone", "Attack on Titan"]
      render json: { animes: animes }.to_json, status: :ok
    end
  end

Now let’s do an arbitrary test to make sure our API works. Run the Rails app and navigate to http://localhost:3000/animes

rails s

You should see:

Add Security methods

This looks nice! A great list of stuff to watch. Anime aside, let’s secure this API. To do this, we will add some security methods to our application controller. We are going to add a private method that will use the JWT library.

  private
    def valid_token(token)
      unless token
        return false
      end
  
      token.gsub!('Bearer ','')
      begin
            keys = []

        JWT.decode(token, nil, true, { algorithms: ['RS256'], jwks: { keys: keys } })
        return true
      rescue JWT::DecodeError
        render json: { errors: ['Not Authenticated'] }, status: :unauthorized
      end
      false
    end

Note: The above is a function that will expect an OAuth token from an Okta OAuth server. Our code should look something like this in the application controller:

But wait a second. We are still missing a couple of pieces of key logic! For one, when do we call this valid token method? So let us fix that first. We are going to create a public method that will check HTTP headers for a valid JWT. It should look like so:

    def require_jwt
      token = request.headers["HTTP_AUTHORIZATION"]
      if !token
        head :forbidden
      end
      if !valid_token(token)
        head :forbidden
      end
    end

Now let’s add that code to our application controller. It should now look like this:

Now let’s add a before_action to our controller with our new method. Add this code right under ApplicationController:

before_action :require_jwt

Now the finished result of the application controller should look like this as pure code:

class ApplicationController < ActionController::Base
    before_action :require_jwt
    
    def require_jwt
        token = request.headers["HTTP_AUTHORIZATION"]
        if !token
          head :forbidden
        end
        if !valid_token(token)
          head :forbidden
        end
    end
  
    private
    def valid_token(token)
      unless token
        return false
      end
  
      token.gsub!('Bearer ','')
      begin
            keys = []

        JWT.decode(token, nil, true, { algorithms: ['RS256'], jwks: { keys: keys } })
        return true
      rescue JWT::DecodeError
        render json: { errors: ['Not Authenticated'] }, status: :unauthorized
      end
      false
    end
end 

Find and Add Your Keys Endpoint

However, one thing is still missing. The keys value is an empty array when it should reflect our public keys from an Okta OAuth server. We can find our JWKS endpoint if we go https://{yourdomain}.okta.com/oauth2/default/v1/keys.

When you go there you should see something like this:

Copy the JSON key-value for “keys” and paste it into the keys array variable in your code.

For example, my end result looks like this:

Test your API

Now it is time to test if your API is protected. Start up the server.

rails s

Now let’s open http://localhost:3000/animes.

You should now see:

Sweet! We have secured our API, but to be sure let’s test with Postman! We are going to create a new request for our anime API:

Click on Send. You should still get the unauthorized access denied screen.

To fix this, we will need to get an access token from Okta and send it to our API. Create a new request and point at http://localhost:3000/animes as a GET request. Then select Auth, and select Authorization code from the dropdown for Grant Type, and select Send client credentials in the body for Client Authentication. For the header prefix, write Bearer. Replace the authorization URL with the authorization URL and token URL from your Okta Developer Account.

The format of the URLs should be something like below:

Authorization URL: https://.okta.com/oauth2/default/v1/authorize Token URL: https://.okta.com/oauth2/default/v1/token

In Postman, it should all look like the following. (If yours looks different, try to make it look like below with your own values.)

Now let’s get our Client ID and Secret from Okta. Go to Okta Applications and create a new web app. Feel free to name it whatever you want. Make sure you add the Postman URL to the Base URI and the redirect URI. The Base URL should be https://oauth.pstmn.io and the redirect URI should be https://oauth.pstmn.io/v1/callback. It should look something like this:

Click Done.

On the next page, you should see the Client information like so:

Now take your Client ID and Client secret and copy it into Postman. In Postman, click Get New Access Token:

It should redirect you to an Okta login page:

Just sign in as a user and you should see something like this:

Note: Make sure you allow popups from Postman. Otherwise, you might get stuck on a loading screen in Postman.

If everything works according to plan, you should see this dialog:

All that’s left is to send our token to our API:

Try clicking Send, and you should see our data come back:

Oh yeah, it’s working! Now, what if we want to add some extra validation, like only tokens with a certain scope can hit our API? Let’s modify our code on the application controller. We are going to replace some code with this snippet that checks for the profile scope in the token and returns a boolean if it is present in the token or not.

 token_payload = JWT.decode(token, nil, true, { algorithms: ['RS256'], jwks: { keys: keys } })
        scopes = token_payload[0]["scp"]
        return scopes.include? 'profile'

The application controller will now look like this:

class ApplicationController < ActionController::Base
    before_action :require_jwt

    def require_jwt
        token = request.headers["HTTP_AUTHORIZATION"]
        if !token
          head :forbidden
        end
        if !valid_token(token)
          head :forbidden
        end
    end
  
    private
    def valid_token(token)
      unless token
        return false
      end
  
      token.gsub!('Bearer ','')
      begin
        keys = []

        token_payload = JWT.decode(token, nil, true, { algorithms: ['RS256'], jwks: { keys: keys } })
        scopes = token_payload[0]["scp"]
        return scopes.include? 'profile'
      rescue JWT::DecodeError
        render json: { errors: ['Not Authenticated'] }, status: :unauthorized
      end
      false
    end

end

For reference here is also a screenshot of my code:

Now restart your server.

Get a new token with just OpenID and email. Postman reference request below:

You should now get an unauthorized message when you hit Send again:

Now let’s get a new access token with the profile scope again:

Your request should now be successful:

If you just want want to clone and try it, just use the GitHub repo.

Happy coding!

Learn More About Ruby on Rails and OAuth

For more Ruby on Rails and Okta articles, check out these posts:

Make sure to follow us on Twitter, subscribe to our YouTube Channel and check out our Twitch channel so that you never miss any awesome content!