Zaikio::JWTAuth

Gem for JWT-Based authentication and authorization with zaikio.

Installation

1. Add this line to your application's Gemfile:

gem 'zaikio-jwt_auth'

And then execute:

$ bundle

Or install it yourself as:

$ gem install zaikio-jwt_auth

2. Configure the gem:

# config/initializers/zaikio_jwt_auth.rb

Zaikio::JWTAuth.configure do |config|
  config.environment = :sandbox # or production
  config.app_name = "test_app" # Your Zaikio App-Name

  # Enable caching Hub API responses for e.g. revoked tokens
  config.cache = Rails.cache
end

3. Extend your API application controller:

class API::ApplicationController < ActionController::Base
  include Zaikio::JWTAuth

  before_action :authenticate_by_jwt

  def after_jwt_auth(token_data)
    klass = token_data.subject_type == 'Organization' ? Organization : Person
    Current.scope = klass.find(token_data.subject_id)
  end
end

4. Update Revoked Access Tokens by Webhook

This gem automatically registers a webhook, if you have properly setup Zaikio::Webhooks.

5. Add more restrictions to your resources:

class API::ResourcesController < API::ApplicationController
  authorize_by_jwt_subject_type 'Organization'
  authorize_by_jwt_scopes 'resources'
end

By convention, authorize_by_jwt_scopes automatically maps all CRUD actions in a controller. Requests for show and index with a read or read_write scope are allowed. All other actions like create, update and destroy are accepted if the scope is a write or read_write scope. Therefore it is strongly recommended to always create standard Rails resources. If a custom action is required, you will need to authorize yourself using the after_jwt_auth.

Both of these behaviours are automatically inherited by child classes, for example:

class API::ChildController < API::ResourcesController
end

API::ChildController.authorize_by_jwt_subject_type
#=> "Organization"

You can always override the behaviour in children if needed:

class API::ChildController < API::ResourcesController
  authorize_by_jwt_subject_type nil
end

Modifying required scopes

If you nonetheless want to change the required scopes for CRUD routes, you can use the type option which accepts the following values: :read, :write, :read_write

class API::ResourcesController < API::ApplicationController
  # Require a write or read_write scope on the index route
  authorize_by_jwt_scopes 'resources', only: :index, type: :write
end

Using custom actions

You can also specify authorization for custom actions. When doing so the type option is required.

class API::ResourcesController < API::ApplicationController
  # Require the index use to have a write or read_write scope
  authorize_by_jwt_scopes 'resources', only: :my_custom_route, type: :write
end

6. Optionally, if you are using SSO: Check revoked tokens

Additionally, the API provides a method called revoked_jwt? which expects the jti of the JWT.

Zaikio::JWTAuth.revoked_jwt?('jti-of-token') # returns true if token was revoked

7. Optionally, use the test helper module to mock JWTs in your minitests

# in your test_helper.rb
class ActiveSupport::TestCase
  # ...
  include Zaikio::JWTAuth::TestHelper
  # ...
end

# in your integration tests you can use:
class ResourcesControllerTest < ActionDispatch::IntegrationTest
  def setup
    mock_jwt(sub: 'Organization/123', scope: ['directory.organization.r'])
  end

  test "do a request with a mocked jwt" do
    get resources_path
    # test the actual business logic
  end
end

8. Setup rack-attack for throttling

This gem ships with a rack middleware that should be used to throttle requests by app and/or subject. You can use the middleware with rack-attack as described here:

# config/initializers/rack_attack.rb

MyApp::Application.config.middleware.insert_before Rack::Attack, Zaikio::JWTAuth::RackMiddleware

class Rack::Attack
  Rack::Attack.throttled_response_retry_after_header = true

  throttle("zaikio/by_app_sub", limit: 600, period: 1.minute) do |request|
    next unless request.path.start_with?("/api/")
    next unless request.env[Zaikio::JWTAuth::RackMiddleware::SUBJECT] # does not use zaikio JWT

    "#{request.env[Zaikio::JWTAuth::RackMiddleware::AUDIENCE]}/#{request.env[Zaikio::JWTAuth::RackMiddleware::SUBJECT]}"
  end
end

Advanced

only and except

Similar to Rails' controller callbacks, authorize_by_jwt_scopes can also be passed a list of actions:

class API::ResourcesController < API::ApplicationController
  authorize_by_jwt_subject_type 'Organization'
  authorize_by_jwt_scopes 'resources', except: :destroy
  authorize_by_jwt_scopes 'remove_resources', only: [:destroy]
end

if and unless

Similar to Rails' controller callbacks, authorize_by_jwt_scopes can also handle a lambda in the context of the controller to request parameters.

class API::ResourcesController < API::ApplicationController
  authorize_by_jwt_scopes 'resources', unless: -> { params[:skip] == '1' }
end

Usage outside a Rails controller

If you need to access a JWT outside the normal Rails controllers (e.g. in a Rack middleware), there's a static helper method .extract which you can use:

class MyRackMiddleware < Rack::Middleware
  def call(env)
    token = Zaikio::JWTAuth.extract(env["HTTP_AUTHORIZATION"])
    puts token.subject_type #=> "Organization"
    ...

This function expects to receive the string in the format "Bearer $token". If the JWT is invalid, expired, or has some other fundamental issues, the JWT library may throw additional errors, and you should be prepared to handle these, for example:

def call(env)
  token = Zaikio::JWTAuth.extract("definitely.not.jwt")
rescue JWT::DecodeError, JWT::ExpiredSignature
  [401, {}, ["Unauthorized"]]
end

Using a different cache backend

This client supports any implementation of ActiveSupport::Cache::Store, but you can also write your own client that supports these methods: #read(key), #write(key, value), #delete(key)

Pass custom options to JWT auth

In some cases you want to add custom options to the JWT check. For example you want to allow expired JWTs when revoking access tokens.

class API::RevokedAccessTokensController < API::ApplicationController
  def jwt_options
    { verify_expiration: false }
  end
end

Contributing

Make sure you have the dummy app running locally to validate your changes.

  • Make your changes and submit a pull request for them
  • Make sure to update CHANGELOG.md

To release a new version of the gem:

  • Update the version in lib/zaikio/jwt_auth/version.rb
  • Update CHANGELOG.md to include the new version and its release date
  • Commit and push your changes
  • Create a new release on GitHub
  • CircleCI will build the Gem package and push it Rubygems for you