zaikio-jwt_auth 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +92 -0
- data/Rakefile +37 -0
- data/lib/tasks/zaikio/jwt_auth_tasks.rake +4 -0
- data/lib/zaikio/jwt_auth.rb +113 -0
- data/lib/zaikio/jwt_auth/configuration.rb +49 -0
- data/lib/zaikio/jwt_auth/directory_cache.rb +67 -0
- data/lib/zaikio/jwt_auth/jwk.rb +44 -0
- data/lib/zaikio/jwt_auth/railtie.rb +6 -0
- data/lib/zaikio/jwt_auth/token_data.rb +75 -0
- data/lib/zaikio/jwt_auth/version.rb +5 -0
- metadata +96 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: 3e888abf5976a9f6837a1723da1f586f317add69e45caa8527c68c086e1f4c40
         | 
| 4 | 
            +
              data.tar.gz: 3ccd16afb7cdc1e808b01dab786cf24cdab31e2eff44bffaa695a0c8231afad5
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: 36540e2d6588c39f994e4ae5dc62c2bb274d505feb32404f3d0dd7568c57f665024ed164686bc3056c5f236b4471f271917d2f42090d62a3685952010bf27ba9
         | 
| 7 | 
            +
              data.tar.gz: 13447cfaf7386af9cbbfe83f54ccb0ffed13e310c9dd7fab94705ee1b321bf3d82daeb40a4ba9bb273dc313e355aaf822b33f702e5386674c5ce223cd629155f
         | 
    
        data/MIT-LICENSE
    ADDED
    
    | @@ -0,0 +1,20 @@ | |
| 1 | 
            +
            Copyright 2020 Jalyna
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Permission is hereby granted, free of charge, to any person obtaining
         | 
| 4 | 
            +
            a copy of this software and associated documentation files (the
         | 
| 5 | 
            +
            "Software"), to deal in the Software without restriction, including
         | 
| 6 | 
            +
            without limitation the rights to use, copy, modify, merge, publish,
         | 
| 7 | 
            +
            distribute, sublicense, and/or sell copies of the Software, and to
         | 
| 8 | 
            +
            permit persons to whom the Software is furnished to do so, subject to
         | 
| 9 | 
            +
            the following conditions:
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            The above copyright notice and this permission notice shall be
         | 
| 12 | 
            +
            included in all copies or substantial portions of the Software.
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
         | 
| 15 | 
            +
            EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
         | 
| 16 | 
            +
            MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
         | 
| 17 | 
            +
            NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
         | 
| 18 | 
            +
            LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
         | 
| 19 | 
            +
            OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
         | 
| 20 | 
            +
            WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
         | 
    
        data/README.md
    ADDED
    
    | @@ -0,0 +1,92 @@ | |
| 1 | 
            +
            # Zaikio::JWTAuth
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Gem for JWT-Based authentication and authorization with zaikio.
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            ## Usage
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            ## Installation
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            1. Add this line to your application's Gemfile:
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            ```ruby
         | 
| 12 | 
            +
            gem 'zaikio-jwt_auth'
         | 
| 13 | 
            +
            ```
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            And then execute:
         | 
| 16 | 
            +
            ```bash
         | 
| 17 | 
            +
            $ bundle
         | 
| 18 | 
            +
            ```
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            Or install it yourself as:
         | 
| 21 | 
            +
            ```bash
         | 
| 22 | 
            +
            $ gem install zaikio-jwt_auth
         | 
| 23 | 
            +
            ```
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            2. Configure the gem:
         | 
| 26 | 
            +
             | 
| 27 | 
            +
            ```rb
         | 
| 28 | 
            +
            # config/initializers/zaikio_jwt_auth.rb
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            Zaikio::JWTAuth.configure do |config|
         | 
| 31 | 
            +
              config.environment = :sandbox # or production
         | 
| 32 | 
            +
              config.app_name = "test_app" # Your Zaikio App-Name
         | 
| 33 | 
            +
              config.redis = Redis.new
         | 
| 34 | 
            +
            end
         | 
| 35 | 
            +
            ```
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            3. Extend your API application controller:
         | 
| 38 | 
            +
             | 
| 39 | 
            +
            ```rb
         | 
| 40 | 
            +
            class API::ApplicationController < ActionController::Base
         | 
| 41 | 
            +
              include Zaikio::JWTAuth
         | 
| 42 | 
            +
             | 
| 43 | 
            +
              before_action :authenticate_by_jwt
         | 
| 44 | 
            +
             | 
| 45 | 
            +
              def after_jwt_auth(token_data)
         | 
| 46 | 
            +
                klass = token_data.subject_type == 'Organization' ? Organization : Person
         | 
| 47 | 
            +
                Current.scope = klass.find(token_data.subject_id)
         | 
| 48 | 
            +
              end
         | 
| 49 | 
            +
            end
         | 
| 50 | 
            +
            ```
         | 
| 51 | 
            +
             | 
| 52 | 
            +
            4. Update Revoked Access Tokens by Webhook
         | 
| 53 | 
            +
             | 
| 54 | 
            +
            ```rb
         | 
| 55 | 
            +
            # ENV['ZAIKIO_SHARED_SECRET'] needs to be defined first, you can find it on your
         | 
| 56 | 
            +
            # app details page in zaikio. Fore more help read:
         | 
| 57 | 
            +
            # https://docs.zaikio.com/guide/loom/receiving-events.html
         | 
| 58 | 
            +
            class WebhooksController < ActionController::Base
         | 
| 59 | 
            +
              include Zaikio::JWTAuth
         | 
| 60 | 
            +
             | 
| 61 | 
            +
              before_action :verify_signature
         | 
| 62 | 
            +
              before_action :update_blacklisted_access_tokens_by_webhook
         | 
| 63 | 
            +
             | 
| 64 | 
            +
              def create
         | 
| 65 | 
            +
                case params[:name]
         | 
| 66 | 
            +
                  # Manage other events
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
              end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
              private
         | 
| 71 | 
            +
             | 
| 72 | 
            +
              def verify_signature
         | 
| 73 | 
            +
                # Read More: https://docs.zaikio.com/guide/loom/receiving-events.html
         | 
| 74 | 
            +
                unless ActiveSupport::SecurityUtils.secure_compare(
         | 
| 75 | 
            +
                  OpenSSL::HMAC.hexdigest("SHA256", "shared-secret", request.body.read),
         | 
| 76 | 
            +
                  request.headers["X-Loom-Signature"]
         | 
| 77 | 
            +
                )
         | 
| 78 | 
            +
                  render status: :unauthorized, json: { errors: ["invalid_signature"] }
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
              end
         | 
| 81 | 
            +
            end
         | 
| 82 | 
            +
            ```
         | 
| 83 | 
            +
             | 
| 84 | 
            +
             | 
| 85 | 
            +
            5. Add more restrictions to your resources:
         | 
| 86 | 
            +
             | 
| 87 | 
            +
            ```rb
         | 
| 88 | 
            +
            class API::ResourcesController < API::ApplicationController
         | 
| 89 | 
            +
              authorize_by_jwt_subject_type 'Organization'
         | 
| 90 | 
            +
              authorize_by_jwt_scopes 'resources'
         | 
| 91 | 
            +
            end
         | 
| 92 | 
            +
            ```
         | 
    
        data/Rakefile
    ADDED
    
    | @@ -0,0 +1,37 @@ | |
| 1 | 
            +
            begin
         | 
| 2 | 
            +
              require 'bundler/setup'
         | 
| 3 | 
            +
            rescue LoadError
         | 
| 4 | 
            +
              puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
         | 
| 5 | 
            +
            end
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            require 'rdoc/task'
         | 
| 8 | 
            +
            require 'rubocop/rake_task'
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            RDoc::Task.new(:rdoc) do |rdoc|
         | 
| 11 | 
            +
              rdoc.rdoc_dir = 'rdoc'
         | 
| 12 | 
            +
              rdoc.title    = 'Zaikio::JWTAuth'
         | 
| 13 | 
            +
              rdoc.options << '--line-numbers'
         | 
| 14 | 
            +
              rdoc.rdoc_files.include('README.md')
         | 
| 15 | 
            +
              rdoc.rdoc_files.include('lib/**/*.rb')
         | 
| 16 | 
            +
            end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            require 'bundler/gem_tasks'
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            require 'rake/testtask'
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            Rake::TestTask.new(:test) do |t|
         | 
| 23 | 
            +
              t.libs << 'test'
         | 
| 24 | 
            +
              t.pattern = 'test/**/*_test.rb'
         | 
| 25 | 
            +
              t.verbose = false
         | 
| 26 | 
            +
            end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            task default: :test
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            namespace :test do
         | 
| 31 | 
            +
              desc 'Runs RuboCop on specified directories'
         | 
| 32 | 
            +
              RuboCop::RakeTask.new(:rubocop) do |task|
         | 
| 33 | 
            +
                task.fail_on_error = false
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
            end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            Rake::Task[:test].enhance ['test:rubocop']
         | 
| @@ -0,0 +1,113 @@ | |
| 1 | 
            +
            require "jwt"
         | 
| 2 | 
            +
            require "oj"
         | 
| 3 | 
            +
            require "zaikio/jwt_auth/railtie"
         | 
| 4 | 
            +
            require "zaikio/jwt_auth/configuration"
         | 
| 5 | 
            +
            require "zaikio/jwt_auth/directory_cache"
         | 
| 6 | 
            +
            require "zaikio/jwt_auth/jwk"
         | 
| 7 | 
            +
            require "zaikio/jwt_auth/token_data"
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            module Zaikio
         | 
| 10 | 
            +
              module JWTAuth
         | 
| 11 | 
            +
                class << self
         | 
| 12 | 
            +
                  attr_accessor :configuration
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def self.configure
         | 
| 16 | 
            +
                  self.configuration ||= Configuration.new
         | 
| 17 | 
            +
                  yield(configuration)
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def self.included(base)
         | 
| 21 | 
            +
                  base.send :include, InstanceMethods
         | 
| 22 | 
            +
                  base.send :extend, ClassMethods
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                module ClassMethods
         | 
| 26 | 
            +
                  def authorize_by_jwt_subject_type(type = nil)
         | 
| 27 | 
            +
                    @authorize_by_jwt_subject_type ||= type
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  def authorize_by_jwt_scopes(scopes = nil, options = {})
         | 
| 31 | 
            +
                    @authorize_by_jwt_scopes ||= options.merge(scopes: scopes)
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                module InstanceMethods
         | 
| 36 | 
            +
                  def authenticate_by_jwt
         | 
| 37 | 
            +
                    render_error("no_jwt_passed", status: :unauthorized) && return unless jwt_from_auth_header
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    token_data = TokenData.new(jwt_payload)
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                    return if show_error_if_token_is_blacklisted(token_data)
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    return if show_error_if_authorize_by_jwt_subject_type_fails(token_data)
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                    return if show_error_if_authorize_by_jwt_scopes_fails(token_data)
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    send(:after_jwt_auth, token_data) if respond_to?(:after_jwt_auth)
         | 
| 48 | 
            +
                  rescue JWT::ExpiredSignature
         | 
| 49 | 
            +
                    render_error("jwt_expired") && (return)
         | 
| 50 | 
            +
                  rescue JWT::DecodeError
         | 
| 51 | 
            +
                    render_error("invalid_jwt") && (return)
         | 
| 52 | 
            +
                  end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  def update_blacklisted_access_tokens_by_webhook
         | 
| 55 | 
            +
                    return unless params[:name] == "directory.revoked_access_token"
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                    DirectoryCache.update("api/v1/blacklisted_token_ids.json", expires_after: 60.minutes) do |data|
         | 
| 58 | 
            +
                      data["blacklisted_token_ids"] << params[:payload][:access_token_id]
         | 
| 59 | 
            +
                      data
         | 
| 60 | 
            +
                    end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                    render json: { received: true }
         | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  private
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                  def jwt_from_auth_header
         | 
| 68 | 
            +
                    auth_header = request.headers["Authorization"]
         | 
| 69 | 
            +
                    auth_header.split("Bearer ").last if /Bearer/.match?(auth_header)
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  def jwt_payload
         | 
| 73 | 
            +
                    payload, = JWT.decode(jwt_from_auth_header, nil, true, algorithms: ["RS256"], jwks: JWK.loader)
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                    payload
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  def show_error_if_authorize_by_jwt_scopes_fails(token_data)
         | 
| 79 | 
            +
                    scope_data = self.class.authorize_by_jwt_scopes
         | 
| 80 | 
            +
                    return if !scope_data[:scopes] || token_data.scope?(scope_data[:scopes], action_name, scope_data[:app_name])
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                    render_error("unpermitted_scope")
         | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                  def show_error_if_authorize_by_jwt_subject_type_fails(token_data)
         | 
| 86 | 
            +
                    if !self.class.authorize_by_jwt_subject_type ||
         | 
| 87 | 
            +
                       self.class.authorize_by_jwt_subject_type == token_data.subject_type
         | 
| 88 | 
            +
                      return
         | 
| 89 | 
            +
                    end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                    render_error("unpermitted_subject")
         | 
| 92 | 
            +
                  end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                  def show_error_if_token_is_blacklisted(token_data)
         | 
| 95 | 
            +
                    return unless blacklisted_token_ids.include?(token_data.jti)
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                    render_error("invalid_jwt")
         | 
| 98 | 
            +
                  end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                  def blacklisted_token_ids
         | 
| 101 | 
            +
                    if Zaikio::JWTAuth.configuration.blacklisted_token_ids
         | 
| 102 | 
            +
                      return Zaikio::JWTAuth.configuration.blacklisted_token_ids
         | 
| 103 | 
            +
                    end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                    DirectoryCache.fetch("api/v1/blacklisted_token_ids.json", expires_after: 60.minutes)["blacklisted_token_ids"]
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                  def render_error(error, status: :forbidden)
         | 
| 109 | 
            +
                    render(status: status, json: { "errors" => [error] })
         | 
| 110 | 
            +
                  end
         | 
| 111 | 
            +
                end
         | 
| 112 | 
            +
              end
         | 
| 113 | 
            +
            end
         | 
| @@ -0,0 +1,49 @@ | |
| 1 | 
            +
            require "logger"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Zaikio
         | 
| 4 | 
            +
              module JWTAuth
         | 
| 5 | 
            +
                class Configuration
         | 
| 6 | 
            +
                  HOSTS = {
         | 
| 7 | 
            +
                    development: "http://directory.zaikio.test",
         | 
| 8 | 
            +
                    test: "http://directory.zaikio.test",
         | 
| 9 | 
            +
                    staging: "https://directory.staging.zaikio.com",
         | 
| 10 | 
            +
                    sandbox: "https://directory.sandbox.zaikio.com",
         | 
| 11 | 
            +
                    production: "https://directory.zaikio.com"
         | 
| 12 | 
            +
                  }.freeze
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  attr_accessor :app_name
         | 
| 15 | 
            +
                  attr_accessor :redis, :host
         | 
| 16 | 
            +
                  attr_reader :environment
         | 
| 17 | 
            +
                  attr_writer :logger, :blacklisted_token_ids, :keys
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  def initialize
         | 
| 20 | 
            +
                    @environment = :sandbox
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  def logger
         | 
| 24 | 
            +
                    @logger ||= Logger.new(STDOUT)
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def environment=(env)
         | 
| 28 | 
            +
                    @environment = env.to_sym
         | 
| 29 | 
            +
                    @host = host_for(environment)
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  def keys
         | 
| 33 | 
            +
                    @keys.is_a?(Proc) ? @keys.call : @keys
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  def blacklisted_token_ids
         | 
| 37 | 
            +
                    @blacklisted_token_ids.is_a?(Proc) ? @blacklisted_token_ids.call : @blacklisted_token_ids
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  private
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  def host_for(environment)
         | 
| 43 | 
            +
                    HOSTS.fetch(environment) do
         | 
| 44 | 
            +
                      raise StandardError.new, "Invalid Zaikio::JWTAuth environment '#{environment}'"
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
              end
         | 
| 49 | 
            +
            end
         | 
| @@ -0,0 +1,67 @@ | |
| 1 | 
            +
            require "net/http"
         | 
| 2 | 
            +
            require "json"
         | 
| 3 | 
            +
            require "logger"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Zaikio
         | 
| 6 | 
            +
              module JWTAuth
         | 
| 7 | 
            +
                class DirectoryCache
         | 
| 8 | 
            +
                  class << self
         | 
| 9 | 
            +
                    def fetch(directory_path, options = {})
         | 
| 10 | 
            +
                      cache = Zaikio::JWTAuth.configuration.redis.get("zaikio::jwt_auth::#{directory_path}")
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                      json = Oj.load(cache) if cache
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                      if !cache || options[:invalidate] || cache_expired?(json, options[:expires_after])
         | 
| 15 | 
            +
                        return reload(directory_path)
         | 
| 16 | 
            +
                      end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                      json["data"]
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    def update(directory_path, options = {})
         | 
| 22 | 
            +
                      data = fetch(directory_path, options)
         | 
| 23 | 
            +
                      data = yield(data)
         | 
| 24 | 
            +
                      Zaikio::JWTAuth.configuration.redis.set("zaikio::jwt_auth::#{directory_path}", {
         | 
| 25 | 
            +
                        fetched_at: Time.now.to_i,
         | 
| 26 | 
            +
                        data: data
         | 
| 27 | 
            +
                      }.to_json)
         | 
| 28 | 
            +
                    end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    def reset(directory_path)
         | 
| 31 | 
            +
                      Zaikio::JWTAuth.configuration.redis.del("zaikio::jwt_auth::#{directory_path}")
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                    private
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                    def cache_expired?(json, expires_after)
         | 
| 37 | 
            +
                      DateTime.strptime(json["fetched_at"].to_s, "%s") < Time.now.utc - (expires_after || 1.hour)
         | 
| 38 | 
            +
                    end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    def reload(directory_path)
         | 
| 41 | 
            +
                      retries = 0
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                      begin
         | 
| 44 | 
            +
                        data = fetch_from_directory(directory_path)
         | 
| 45 | 
            +
                        Zaikio::JWTAuth.configuration.redis.set("zaikio::jwt_auth::#{directory_path}", {
         | 
| 46 | 
            +
                          fetched_at: Time.now.to_i,
         | 
| 47 | 
            +
                          data: data
         | 
| 48 | 
            +
                        }.to_json)
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                        data
         | 
| 51 | 
            +
                      rescue Errno::ECONNREFUSED, Net::ReadTimeout => e
         | 
| 52 | 
            +
                        raise unless (retries += 1) <= 3
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                        Zaikio::JWTAuth.configuration.logger.log("Timeout (#{e}), retrying in 1 second...")
         | 
| 55 | 
            +
                        sleep(1)
         | 
| 56 | 
            +
                        retry
         | 
| 57 | 
            +
                      end
         | 
| 58 | 
            +
                    end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                    def fetch_from_directory(directory_path)
         | 
| 61 | 
            +
                      uri = URI("#{Zaikio::JWTAuth.configuration.host}/#{directory_path}")
         | 
| 62 | 
            +
                      Oj.load(Net::HTTP.get(uri))
         | 
| 63 | 
            +
                    end
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
              end
         | 
| 67 | 
            +
            end
         | 
| @@ -0,0 +1,44 @@ | |
| 1 | 
            +
            require "net/http"
         | 
| 2 | 
            +
            require "json"
         | 
| 3 | 
            +
            require "logger"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Zaikio
         | 
| 6 | 
            +
              module JWTAuth
         | 
| 7 | 
            +
                class JWK
         | 
| 8 | 
            +
                  CACHE_EXPIRES_AFTER = 1.hour.freeze
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  class << self
         | 
| 11 | 
            +
                    def loader
         | 
| 12 | 
            +
                      lambda do |options|
         | 
| 13 | 
            +
                        reload_keys if options[:invalidate]
         | 
| 14 | 
            +
                        {
         | 
| 15 | 
            +
                          keys: keys.map do |key_data|
         | 
| 16 | 
            +
                            JWT::JWK.import(key_data.with_indifferent_access).export
         | 
| 17 | 
            +
                          end
         | 
| 18 | 
            +
                        }
         | 
| 19 | 
            +
                      end
         | 
| 20 | 
            +
                    end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    private
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    def reload_keys
         | 
| 25 | 
            +
                      return if Zaikio::JWTAuth.configuration.keys
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                      fetch_from_cache(invalidate: true)
         | 
| 28 | 
            +
                    end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    def keys
         | 
| 31 | 
            +
                      return Zaikio::JWTAuth.configuration.keys if Zaikio::JWTAuth.configuration.keys
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                      fetch_from_cache["keys"]
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                    def fetch_from_cache(options = {})
         | 
| 37 | 
            +
                      DirectoryCache.fetch("api/v1/jwt_public_keys.json", {
         | 
| 38 | 
            +
                        expires_after: CACHE_EXPIRES_AFTER
         | 
| 39 | 
            +
                      }.merge(options))
         | 
| 40 | 
            +
                    end
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
              end
         | 
| 44 | 
            +
            end
         | 
| @@ -0,0 +1,75 @@ | |
| 1 | 
            +
            module Zaikio
         | 
| 2 | 
            +
              module JWTAuth
         | 
| 3 | 
            +
                class TokenData
         | 
| 4 | 
            +
                  def self.subject_format
         | 
| 5 | 
            +
                    %r{^((\w+)/((\w|-)+)\>)?(\w+)/((\w|-)+)$}
         | 
| 6 | 
            +
                  end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def self.actions_by_permission
         | 
| 9 | 
            +
                    {
         | 
| 10 | 
            +
                      "r" => %w[show index],
         | 
| 11 | 
            +
                      "w" => %w[update create destroy],
         | 
| 12 | 
            +
                      "rw" => %w[show index update create destroy]
         | 
| 13 | 
            +
                    }.freeze
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def initialize(payload)
         | 
| 17 | 
            +
                    @payload = payload
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  def audience
         | 
| 21 | 
            +
                    audiences.first
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def audiences
         | 
| 25 | 
            +
                    @payload["aud"] || []
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def scope
         | 
| 29 | 
            +
                    @payload["scope"]
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  def jti
         | 
| 33 | 
            +
                    @payload["jti"]
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  def scope?(allowed_scopes, action_name, app_name = nil)
         | 
| 37 | 
            +
                    app_name ||= Zaikio::JWTAuth.configuration.app_name
         | 
| 38 | 
            +
                    Array(allowed_scopes).map(&:to_s).any? do |allowed_scope|
         | 
| 39 | 
            +
                      scope.any? do |s|
         | 
| 40 | 
            +
                        parts = s.split(".")
         | 
| 41 | 
            +
                        parts[0] == app_name &&
         | 
| 42 | 
            +
                          parts[1] == allowed_scope &&
         | 
| 43 | 
            +
                          action_in_permission?(action_name, parts[2])
         | 
| 44 | 
            +
                      end
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  def subject_id
         | 
| 49 | 
            +
                    subject_match[6]
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  def subject_type
         | 
| 53 | 
            +
                    subject_match[5]
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  def on_behalf_of_id
         | 
| 57 | 
            +
                    subject_match[3]
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                  def on_behalf_of_type
         | 
| 61 | 
            +
                    subject_match[2]
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                  def subject_match
         | 
| 65 | 
            +
                    self.class.subject_format.match(@payload["sub"]) || []
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  private
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                  def action_in_permission?(action_name, permission)
         | 
| 71 | 
            +
                    self.class.actions_by_permission[permission].include?(action_name)
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
                end
         | 
| 74 | 
            +
              end
         | 
| 75 | 
            +
            end
         | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,96 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: zaikio-jwt_auth
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 0.1.3
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - Crispy Mountain GmbH
         | 
| 8 | 
            +
            autorequire: 
         | 
| 9 | 
            +
            bindir: bin
         | 
| 10 | 
            +
            cert_chain: []
         | 
| 11 | 
            +
            date: 2020-02-03 00:00:00.000000000 Z
         | 
| 12 | 
            +
            dependencies:
         | 
| 13 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 14 | 
            +
              name: oj
         | 
| 15 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 16 | 
            +
                requirements:
         | 
| 17 | 
            +
                - - ">="
         | 
| 18 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 19 | 
            +
                    version: 3.0.0
         | 
| 20 | 
            +
              type: :runtime
         | 
| 21 | 
            +
              prerelease: false
         | 
| 22 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 23 | 
            +
                requirements:
         | 
| 24 | 
            +
                - - ">="
         | 
| 25 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 26 | 
            +
                    version: 3.0.0
         | 
| 27 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 28 | 
            +
              name: rails
         | 
| 29 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 30 | 
            +
                requirements:
         | 
| 31 | 
            +
                - - ">="
         | 
| 32 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 33 | 
            +
                    version: 6.0.1
         | 
| 34 | 
            +
              type: :runtime
         | 
| 35 | 
            +
              prerelease: false
         | 
| 36 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 37 | 
            +
                requirements:
         | 
| 38 | 
            +
                - - ">="
         | 
| 39 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 40 | 
            +
                    version: 6.0.1
         | 
| 41 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 42 | 
            +
              name: jwt
         | 
| 43 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 44 | 
            +
                requirements:
         | 
| 45 | 
            +
                - - ">="
         | 
| 46 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 47 | 
            +
                    version: 2.2.1
         | 
| 48 | 
            +
              type: :runtime
         | 
| 49 | 
            +
              prerelease: false
         | 
| 50 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 51 | 
            +
                requirements:
         | 
| 52 | 
            +
                - - ">="
         | 
| 53 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 54 | 
            +
                    version: 2.2.1
         | 
| 55 | 
            +
            description: JWT-Based authentication and authorization with zaikio.
         | 
| 56 | 
            +
            email:
         | 
| 57 | 
            +
            - js@crispymtn.com
         | 
| 58 | 
            +
            executables: []
         | 
| 59 | 
            +
            extensions: []
         | 
| 60 | 
            +
            extra_rdoc_files: []
         | 
| 61 | 
            +
            files:
         | 
| 62 | 
            +
            - MIT-LICENSE
         | 
| 63 | 
            +
            - README.md
         | 
| 64 | 
            +
            - Rakefile
         | 
| 65 | 
            +
            - lib/tasks/zaikio/jwt_auth_tasks.rake
         | 
| 66 | 
            +
            - lib/zaikio/jwt_auth.rb
         | 
| 67 | 
            +
            - lib/zaikio/jwt_auth/configuration.rb
         | 
| 68 | 
            +
            - lib/zaikio/jwt_auth/directory_cache.rb
         | 
| 69 | 
            +
            - lib/zaikio/jwt_auth/jwk.rb
         | 
| 70 | 
            +
            - lib/zaikio/jwt_auth/railtie.rb
         | 
| 71 | 
            +
            - lib/zaikio/jwt_auth/token_data.rb
         | 
| 72 | 
            +
            - lib/zaikio/jwt_auth/version.rb
         | 
| 73 | 
            +
            homepage: https://www.zaikio.com/
         | 
| 74 | 
            +
            licenses:
         | 
| 75 | 
            +
            - MIT
         | 
| 76 | 
            +
            metadata: {}
         | 
| 77 | 
            +
            post_install_message: 
         | 
| 78 | 
            +
            rdoc_options: []
         | 
| 79 | 
            +
            require_paths:
         | 
| 80 | 
            +
            - lib
         | 
| 81 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 82 | 
            +
              requirements:
         | 
| 83 | 
            +
              - - ">="
         | 
| 84 | 
            +
                - !ruby/object:Gem::Version
         | 
| 85 | 
            +
                  version: '0'
         | 
| 86 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 87 | 
            +
              requirements:
         | 
| 88 | 
            +
              - - ">="
         | 
| 89 | 
            +
                - !ruby/object:Gem::Version
         | 
| 90 | 
            +
                  version: '0'
         | 
| 91 | 
            +
            requirements: []
         | 
| 92 | 
            +
            rubygems_version: 3.1.2
         | 
| 93 | 
            +
            signing_key: 
         | 
| 94 | 
            +
            specification_version: 4
         | 
| 95 | 
            +
            summary: JWT-Based authentication and authorization with zaikio
         | 
| 96 | 
            +
            test_files: []
         |