Skip to main content

Implementing Cryptr authorization with Rails

Cryptr - Rails illustration

Rails Authorization Getting Started: Secure an API

Ruby 2.7.1 | Rails 6.1.3.215min

Learn how to use Cryptr to implement authorization in Rails

In this tutorial, we are going to create an API that will allow you to read the data, i.e. see the list of courses in our example. Security will be enhanced so that only Cryptr users can access the list of courses by checking if the token is valid by following the Cryptr security procedure.

Let's get started! ๐Ÿ˜‰

1. Configurationโ€‹

Create a Rails projectโ€‹

๐Ÿ› ๏ธ๏ธ First, generate a new api Rails app and set up this Rails app environment by flagging it as an API with --api:

rails new cryptr-rails-api-sample --api
cd cryptr-rails-api-sample

2. Application keysโ€‹

๐Ÿ› ๏ธ๏ธ In order to start, you need to create a free Cryptr account if you don't have one yet.

Rendering Rendering

๐Ÿ› ๏ธ๏ธ You can then create your application by following the onboarding steps.

Rendering

Once you've completed all the funnel, after the processing steps, you arrive on the homepage of the back-office where you have access to various guides that will help you. They will depend on the choices of technology you have made in the funnel.

Rendering

๐Ÿ› ๏ธ๏ธ You can get your environment configuration by first clicking on ยซ applications ยป in the left menu, then by clicking on the application you will have created. A modal will appear and this is where you may copy/paste your environment variables.

Rendering Rendering

3. Add your Cryptr credentialsโ€‹

๐Ÿ› ๏ธ๏ธ Add your Cryptr environment variables to your application in config/application.rb. Don't forget to replace YOUR_DOMAIN with your own domain.

config/application.rb
module CryptrRailsApiSample
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 6.1

# Add your Cryptr config:
config.before_configuration do
ENV['CRYPTR_BASE_URL'] = 'https://auth.cryptr.eu'
ENV['TENANT_DOMAIN'] = 'YOUR_DOMAIN'
ENV['CRYPTR_AUDIENCE'] = 'http://localhost:8081'
end
# Configuration for the application, engines, and railties goes here.
#
# These settings can be overridden in specific environments using the files
# in config/environments, which are processed later.
#
# config.time_zone = "Central Time (US & Canada)"
# config.eager_load_paths << Rails.root.join("extras")

# Only loads a smaller set of middleware suitable for API only apps.
# Middleware like session, flash, cookies can be added back manually.
# Skip views, helpers and assets when generating a new resource.
config.api_only = true
end
end
NOTE

If you are from the EU, you must add https://auth.cryptr.eu/ in the CRYPTR_BASE_URL variable, and if you are from the US, you must add https://auth.cryptr.us/ in the same variable.

TIP

This part can be done differently depending on the developerโ€™s preference, the goal here is to have access to Cryptr configuration from ENV

4. Validate Access Tokenโ€‹

Install dependenciesโ€‹

๐Ÿ› ๏ธ๏ธ Open up Gemfile and add this gem:

gem 'jwt'

Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.7.1'
# Add this gem:
gem 'jwt'
# ...

๐Ÿ› ๏ธ๏ธ Open your terminal and type this command:

bundle install

JsonWebTokenโ€‹

๐Ÿ› ๏ธ๏ธ Create JsonWebToken file in the lib folder

touch lib/json_web_token.rb

๐Ÿ› ๏ธ๏ธ Now open up lib/json_web_token.rb and add the following code:

lib/json_web_token.rb
#frozen_string_literal: true
require 'net/http'
require 'uri'

class JsonWebToken
def self.verify(token)
puts jwks_hash.count
token_decode = jwks_hash.map do |kid, key|
begin
verify_token_with_key(token, key, kid)
rescue JWT::VerificationError, JWT::DecodeError => e
Rails.logger.info "failed with kid #{kid}: #{e}"
nil
end
end
token_decode.compact.first
end

def self.verify_token_with_key(token, key, kid)
decoded = JWT.decode(token, key,
true,
algorithms: 'RS256',
verify_iat: true,
verify_jti: true,
iss: issuer,
verify_iss: true,
aud: ENV['CRYPTR_AUDIENCE'],
verify_aud: true)
payload, header = decoded
if header["kid"] == kid
decoded
end
end
def self.jwks_hash
jwks_raw = Net::HTTP.get jwks_uri
jwks_keys = Array(JSON.parse(jwks_raw)['keys'])
Hash[
jwks_keys
.map do |k|
[
k['kid'],
OpenSSL::X509::Certificate.new(
Base64.decode64(k['x5c'].first)
).public_key
]
end
]
end

def self.issuer
"#{ENV['CRYPTR_BASE_URL']}/t/#{ENV['TENANT_DOMAIN']}"
end
def self.jwks_uri
URI("#{issuer}/.well-known")
end
end

๐Ÿ‘€ Let's take a quick look at this file so that you can see how it works:

  • The public key is fetched in jwks_hash
  • The fetched token is verified with decoded param
  • The header token is compared with the fetched key

๐Ÿ› ๏ธ๏ธ Now open up config/application.rb and add this line to load the lib folder files:

config.eager_load_paths << Rails.root.join("lib")

config/application.rb
module CryptrRailsApiSample
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 6.1

# Add your Cryptr config:
config.before_configuration do
ENV['CRYPTR_BASE_URL'] = 'https://auth.cryptr.eu'
ENV['TENANT_DOMAIN'] = 'shark-academy'
ENV['CRYPTR_AUDIENCE'] = 'http://localhost:8081'
end
# Configuration for the application, engines, and railties goes here.
#
# These settings can be overridden in specific environments using the files
# in config/environments, which are processed later.
#
# config.time_zone = "Central Time (US & Canada)"
# config.eager_load_paths << Rails.root.join("extras")

# Load lib folder files
config.eager_load_paths << Rails.root.join("lib")

# Only loads a smaller set of middleware suitable for API only apps.
# Middleware like session, flash, cookies can be added back manually.
# Skip views, helpers and assets when generating a new resource.
config.api_only = true
end
end
The non rails files

We need to load the files from the lib folder because the file was not designed by rails, we must let it know that it can read it.

Define a Secured concernโ€‹

๐Ÿ› ๏ธ๏ธ Create these files in the app/controllers/concerns folder:

touch app/controllers/concerns/secured.rb

๐Ÿ› ๏ธ๏ธ Now open up the secured file in app/controllers/concerns/secured.rb and paste in the following code:

app/controllers/concerns/secured.rb
#frozen_string_literal: true
module Secured
extend ActiveSupport::Concern

included do
before_action :authenticate_request!, except: :options
end

private

def authenticate_request!
auth_payload, auth_header = auth_token

if auth_payload === nil || auth_header === nil
render json: { error: ['Not authenticated'] }, status: :unauthorized
end
end

def http_token
if request.headers['Authorization'].present?
request.headers['Authorization'].split(' ').last
end
end

def auth_token
puts http_token
JsonWebToken.verify(http_token)
end
end

This file will check the token in the Authorization headers:

  • If the token is not present, it is unauthorized.
  • If the token is present, it is passed to the JsonWebToken.verify
Scope

Thanks to the auth_payload, you can refine the request with scopes

app/controllers/concerns/secured.rb
def authenticate_request!
auth_payload, auth_header = auth_token

if auth_payload === nil || auth_header === nil
render json: { error: ['Not authenticated'] }, status: :unauthorized
# Handle scope checking
elsif auth_paylaod["scp"] !== ["openid" ....]
render json: { error: ['Insufficient scope'] }, status: :unauthorized
end
end

Render Coursesโ€‹

๐Ÿ› ๏ธ๏ธ Create a controller to render courses in app/controllers, itโ€™s an api controller where index returns courses:

touch app/controllers/api_v1_controller.rb

๐Ÿ› ๏ธ๏ธ Next, open up the api controller file in app/controllers/api_v1_controller.rb and paste in the following code:

app/controllers/api_v1_controller.rb
class ApiV1Controller < ApplicationController

def index
render json: [
{
"id" => 1,
"user_id" =>
"eba25511-afce-4c8e-8cab-f82822434648",
"title" => "learn git",
"tags" => ["colaborate", "git" ,"cli", "commit", "versionning"],
"img" => "https://carlchenet.com/wp-content/uploads/2019/04/git-logo.png",
"desc" => "Learn how to create, manage, fork, and collaborate on a project. Git stays a major part of all companies projects. Learning git is learning how to make your project better everyday",
"date" => '5 Nov',
"timestamp" => 1604577600000,
"teacher" => {
"name" => "Max",
"picture" => "https://images.unsplash.com/photo-1558531304-a4773b7e3a9c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80"
}
}
]
end

private

def set_access_control_headers
headers['Access-Control-Allow-Origin'] = ENV['CRYPTR_AUDIENCE']
headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS'
headers['Access-Control-Max-Age'] = '1000'
headers['Access-Control-Allow-Headers'] = '*,x-requested-with'
end
end

๐Ÿ› ๏ธ๏ธ Call the set_access_control_headers function in the before_action callback, all actions of the controller will inherit it. This will cause the set_access_control_headers function (which allows you to manage the cors) to be executed before each action of the controller.

app/controllers/api_v1_controller.rb
class ApiV1Controller < ApplicationController

before_action :set_access_control_headers
def index
render json: [
{
"id" => 1,
"user_id" =>
"eba25511-afce-4c8e-8cab-f82822434648",
"title" => "learn git",
"tags" => ["colaborate", "git" ,"cli", "commit", "versionning"],
"img" => "https://carlchenet.com/wp-content/uploads/2019/04/git-logo.png",
"desc" => "Learn how to create, manage, fork, and collaborate on a project. Git stays a major part of all companies projects. Learning git is learning how to make your project better everyday",
"date" => '5 Nov',
"timestamp" => 1604577600000,
"teacher" => {
"name" => "Max",
"picture" => "https://images.unsplash.com/photo-1558531304-a4773b7e3a9c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80"
}
}
]
end

# ...

๐Ÿ› ๏ธ๏ธ Now, open up config/routes.rb and copy paste this code to associate the controller with the routes:

config/routes.rb
Rails.application.routes.draw do
get "/api/v1/courses", to: 'api_v1#index'
options "/api/v1/courses", to: 'api_v1#options'
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end

We can retrieve the response using a HTTP GET request on http://localhost:3000/api/v1/courses

๐Ÿ› ๏ธ๏ธ Run the server with the command rails s and open insomnia or postman to make a GET request which should end with 200:

Rendering

5. Protect API Endpointsโ€‹

Include the secured concern and the options methodโ€‹

The protected endpoints need to include the secured concern and the options method (which handles CORS from the client App).

๐Ÿ› ๏ธ๏ธ Open up app/controllers/api_v1_controller.rb and add include Secured:

app/controllers/api_v1_controller.rb
class ApiV1Controller < ApplicationController
# 1. Add Secured concern
include Secured
before_action :set_access_control_headers

def index
render json: [
{
"id" => 1,
"user_id" =>
"eba25511-afce-4c8e-8cab-f82822434648",
"title" => "learn git",
"tags" => ["colaborate", "git" ,"cli", "commit", "versionning"],
"img" => "https://carlchenet.com/wp-content/uploads/2019/04/git-logo.png",
"desc" => "Learn how to create, manage, fork, and collaborate on a project. Git stays a major part of all companies projects. Learning git is learning how to make your project better everyday",
"date" => '5 Nov',
"timestamp" => 1604577600000,
"teacher" => {
"name" => "Max",
"picture" => "https://images.unsplash.com/photo-1558531304-a4773b7e3a9c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80"
}
}
]
end
# ...

๐Ÿ› ๏ธ๏ธ Now, add the options method:

app/controllers/api_v1_controller.rb
class ApiV1Controller < ApplicationController

include Secured
before_action :set_access_control_headers

def index
render json: [
{
"id" => 1,
"user_id" =>
"eba25511-afce-4c8e-8cab-f82822434648",
"title" => "learn git",
"tags" => ["colaborate", "git" ,"cli", "commit", "versionning"],
"img" => "https://carlchenet.com/wp-content/uploads/2019/04/git-logo.png",
"desc" => "Learn how to create, manage, fork, and collaborate on a project. Git stays a major part of all companies projects. Learning git is learning how to make your project better everyday",
"date" => '5 Nov',
"timestamp" => 1604577600000,
"teacher" => {
"name" => "Max",
"picture" => "https://images.unsplash.com/photo-1558531304-a4773b7e3a9c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80"
}
}
]
end

# 2. Add options method
def options
Rails.logger.info "option request"
if ENV['CRYPTR_AUDIENCE'] == request.env['HTTP_ORIGIN']
:ok
else
:forbidden
end
end

private

def set_access_control_headers
headers['Access-Control-Allow-Origin'] = ENV['CRYPTR_AUDIENCE']
headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS'
headers['Access-Control-Max-Age'] = '1000'
headers['Access-Control-Allow-Headers'] = '*,x-requested-with'
end
end

Test with a Cryptr Vue appโ€‹

It is now time to try this on an application. For this purpose, we have an example app on Vue.

๐Ÿ›  Run your code with rails s

๐Ÿ›  Clone our cryptr-vue-sample:

git clone --branch 07-backend-courses-api https://github.com/cryptr-examples/cryptr-vue2-sample.git

๐Ÿ›  Install the Vue project dependencies with yarn

๐Ÿ› ๏ธ๏ธ Create the .env.local file and add your variables. Don't forget to replace YOUR_CLIENT_ID & YOUR_DOMAIN:

VUE_APP_AUDIENCE=http://localhost:8080
VUE_APP_CLIENT_ID=YOUR_CLIENT_ID
VUE_APP_DEFAULT_LOCALE=fr
VUE_APP_DEFAULT_REDIRECT_URI=http://localhost:8080
VUE_APP_TENANT_DOMAIN=YOUR_DOMAIN
VUE_APP_CRYPTR_TELEMETRY=FALSE

๐Ÿ› ๏ธ๏ธ Open up the Profile Component in src/views/Profile.vue and modify the url request:

src/views/Profile.vue
<script>
import { getCryptrClient } from "../CryptrPlugin";
export default {
data() {
return {
courses: [],
errors: [],
};
},
created() {
const client = getCryptrClient();
console.log("created");
client
.decoratedRequest({
method: "GET",
// url: "http://localhost/api/v1/courses
// Add port 3000 for rails:
url: "http://localhost:3000/api/v1/courses",
})
.then((data) => {
console.log(data);
this.courses = data.data;
})
.catch((error) => {
console.error(error);
this.errors = [error];
});
},
};
</script>

๐Ÿ› ๏ธ๏ธ Run vue server with yarn serve and try to connect. Your Vue application redirects you to your sign form page, where you can sign in or sign up with an email.

SANDBOX EMAIL

You can log in with a sandbox email and we send you a magic link which should directly arrive in your personal inbox. Your sandbox email is based on your account's email. The email's structure is as follows: my-user-to-test@sandbox.my-admin-name. For example testemail@sandbox.lucas

Once you're connected, click on "Protected route". You can now view the list of the courses.

Congratulations if you made it to the end!

I hope this was helpful, and thanks for reading! ๐Ÿ™‚