rack-cloudflare-jwt 0.0.9 → 0.1.0
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 +4 -4
- data/README.md +5 -5
- data/lib/rack/cloudflare/jwt.rb +1 -0
- data/lib/rack/cloudflare_jwt.rb +3 -4
- data/lib/rack/cloudflare_jwt/auth.rb +205 -190
- data/lib/rack/cloudflare_jwt/version.rb +2 -2
- metadata +18 -4
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 240d66a2a123b06cf3625765ce2d7b4772c2a46f059fd62decc90acd4d9e68bc
         | 
| 4 | 
            +
              data.tar.gz: 9bf13c926defed079e9266752c64ab3b38d004e3665252b3dd7a663db77ae66c
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 735971f62a1c16c83d6591baa3d60c052107ef61076850a0598c2456c386aec32c62b79927e15560f2d9f47ef17f991ff084ee6bb27284ab17195e6fcd805148
         | 
| 7 | 
            +
              data.tar.gz: c3f15c032fa1715e728e6e0337ddfae61165a8bda598cc69f077b5f87f71e93d2e24b249ae79ac197361a533ac730cf8b1c0823f2f1b00e794b15b3d1180c41d
         | 
    
        data/README.md
    CHANGED
    
    | @@ -32,17 +32,17 @@ $ gem install rack-cloudflare-jwt | |
| 32 32 |  | 
| 33 33 | 
             
            ## Usage
         | 
| 34 34 |  | 
| 35 | 
            -
            `Rack::CloudflareJwt::Auth` accepts  | 
| 35 | 
            +
            `Rack::CloudflareJwt::Auth` accepts configuration options. All options are passed in a single Ruby `Hash<String, String>`. E.g. `{ '/admin' => 'aud-1', '/manager' => 'aud-2' }`.
         | 
| 36 36 |  | 
| 37 | 
            -
            * ` | 
| 37 | 
            +
            * `Hash` key : `String` : A path string representing paths that should be checked for the presence of a valid JWT token. Includes sub-paths as of specified path as well (e.g. `/docs` includes `/docs/some/thing.html` also). Each path should start with a `/`. If a path doesn't matches the current request path this entire middleware is skipped and no authentication or verification of tokens takes place.
         | 
| 38 | 
            +
             | 
| 39 | 
            +
            * `Hash` value : `String` : A Application Audience (AUD) Tag.
         | 
| 38 40 |  | 
| 39 | 
            -
            * `include_paths` : optional : Array : An Array of path strings representing paths that should be checked for the presence of a valid JWT token. Includes sub-paths as of specified paths as well (e.g. `%w(/docs)` includes `/docs/some/thing.html` also). Each path should start with a `/`. If a path not matches the current request path this entire middleware is skipped and no authentication or verification of tokens takes place.
         | 
| 40 41 |  | 
| 41 42 | 
             
            ### Rails
         | 
| 42 43 |  | 
| 43 44 | 
             
            ```ruby
         | 
| 44 | 
            -
             | 
| 45 | 
            -
            Rails.application.config.middleware.use Rack::CloudflareJwt::Auth, policy_aud: 'xxx.yyy.zzz', include_paths: %w[/foo]
         | 
| 45 | 
            +
            Rails.application.config.middleware.use Rack::CloudflareJwt::Auth, '/my-path' => 'xxx.yyy.zzz'
         | 
| 46 46 | 
             
            ```
         | 
| 47 47 |  | 
| 48 48 | 
             
            ## Contributing
         | 
| @@ -0,0 +1 @@ | |
| 1 | 
            +
            require 'rack/cloudflare_jwt'
         | 
    
        data/lib/rack/cloudflare_jwt.rb
    CHANGED
    
    
| @@ -5,219 +5,234 @@ require 'multi_json' | |
| 5 5 | 
             
            require 'net/http'
         | 
| 6 6 | 
             
            require 'rack/jwt'
         | 
| 7 7 |  | 
| 8 | 
            -
            module Rack
         | 
| 9 | 
            -
               | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 13 | 
            -
                 | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
| 18 | 
            -
             | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
                   | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
| 47 | 
            -
             | 
| 8 | 
            +
            module Rack::CloudflareJwt
         | 
| 9 | 
            +
              # Authentication middleware
         | 
| 10 | 
            +
              #
         | 
| 11 | 
            +
              # @see https://developers.cloudflare.com/access/setting-up-access/validate-jwt-tokens/
         | 
| 12 | 
            +
              class Auth
         | 
| 13 | 
            +
                # Custom decode token error.
         | 
| 14 | 
            +
                class DecodeTokenError < StandardError; end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                # Certs path
         | 
| 17 | 
            +
                CERTS_PATH = '/cdn-cgi/access/certs'
         | 
| 18 | 
            +
                # Default algorithm
         | 
| 19 | 
            +
                DEFAULT_ALGORITHM = 'RS256'
         | 
| 20 | 
            +
                # CloudFlare JWT header.
         | 
| 21 | 
            +
                HEADER_NAME = 'HTTP_CF_ACCESS_JWT_ASSERTION'
         | 
| 22 | 
            +
                # HTTP_HOST header.
         | 
| 23 | 
            +
                HEADER_HTTP_HOST = 'HTTP_HOST'
         | 
| 24 | 
            +
                # Key for get current path.
         | 
| 25 | 
            +
                PATH_INFO = 'PATH_INFO'
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                # Token regex.
         | 
| 28 | 
            +
                #
         | 
| 29 | 
            +
                # @see https://github.com/jwt/ruby-jwt/tree/v2.2.1#algorithms-and-usage
         | 
| 30 | 
            +
                TOKEN_REGEX = /
         | 
| 31 | 
            +
                  ^(
         | 
| 32 | 
            +
                  [a-zA-Z0-9\-_]+\.  # 1 or more chars followed by a single period
         | 
| 33 | 
            +
                  [a-zA-Z0-9\-_]+\.  # 1 or more chars followed by a single period
         | 
| 34 | 
            +
                  [a-zA-Z0-9\-_]+    # 1 or more chars, no trailing chars
         | 
| 35 | 
            +
                  )$
         | 
| 36 | 
            +
                /x.freeze
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                attr_reader :policies
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                # Initializes middleware
         | 
| 41 | 
            +
                #
         | 
| 42 | 
            +
                # @example Initialize middleware in Rails
         | 
| 43 | 
            +
                #   config.middleware.use(
         | 
| 44 | 
            +
                #     Rack::CloudflareJwt::Auth,
         | 
| 45 | 
            +
                #     '/admin'   => <cloudflare-aud-1>,
         | 
| 46 | 
            +
                #     '/manager' => <cloudflare-aud-2>,
         | 
| 47 | 
            +
                #   )
         | 
| 48 | 
            +
                #
         | 
| 49 | 
            +
                # @param policies [Hash<String, String>] the policies with paths and AUDs.
         | 
| 50 | 
            +
                def initialize(app, policies = {})
         | 
| 51 | 
            +
                  @app      = app
         | 
| 52 | 
            +
                  @policies = policies
         | 
| 48 53 |  | 
| 49 | 
            -
                   | 
| 50 | 
            -
                   | 
| 51 | 
            -
             | 
| 52 | 
            -
                      @app.call(env)
         | 
| 53 | 
            -
                    elsif missing_auth_header?(env)
         | 
| 54 | 
            -
                      return_error('Missing Authorization header')
         | 
| 55 | 
            -
                    elsif invalid_auth_header?(env)
         | 
| 56 | 
            -
                      return_error('Invalid Authorization header format')
         | 
| 57 | 
            -
                    else
         | 
| 58 | 
            -
                      verify_token(env)
         | 
| 59 | 
            -
                    end
         | 
| 60 | 
            -
                  end
         | 
| 54 | 
            +
                  check_policy_auds!
         | 
| 55 | 
            +
                  check_paths_type!
         | 
| 56 | 
            +
                end
         | 
| 61 57 |  | 
| 62 | 
            -
             | 
| 58 | 
            +
                # Public: Call a middleware.
         | 
| 59 | 
            +
                def call(env)
         | 
| 60 | 
            +
                  if !path_matches?(env)
         | 
| 61 | 
            +
                    @app.call(env)
         | 
| 62 | 
            +
                  elsif missing_auth_header?(env)
         | 
| 63 | 
            +
                    return_error('Missing Authorization header')
         | 
| 64 | 
            +
                  elsif invalid_auth_header?(env)
         | 
| 65 | 
            +
                    return_error('Invalid Authorization header format')
         | 
| 66 | 
            +
                  else
         | 
| 67 | 
            +
                    verify_token(env)
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
                end
         | 
| 63 70 |  | 
| 64 | 
            -
             | 
| 65 | 
            -
                  def check_policy_aud!
         | 
| 66 | 
            -
                    return unless !policy_aud.is_a?(String) || policy_aud.strip.empty?
         | 
| 71 | 
            +
                private
         | 
| 67 72 |  | 
| 68 | 
            -
             | 
| 69 | 
            -
             | 
| 73 | 
            +
                # Private: Check policy auds.
         | 
| 74 | 
            +
                def check_policy_auds!
         | 
| 75 | 
            +
                  raise ArgumentError, 'policies cannot be nil/empty' if policies.values.empty?
         | 
| 70 76 |  | 
| 71 | 
            -
                   | 
| 72 | 
            -
             | 
| 73 | 
            -
                    raise ArgumentError, 'include_paths argument must be an Array' unless include_paths.is_a?(Array)
         | 
| 77 | 
            +
                  policies.each_value do |policy_aud|
         | 
| 78 | 
            +
                    next unless !policy_aud.is_a?(String) || policy_aud.strip.empty?
         | 
| 74 79 |  | 
| 75 | 
            -
                     | 
| 76 | 
            -
                      raise ArgumentError, 'each include_paths Array element must be a String' unless path.is_a?(String)
         | 
| 77 | 
            -
                      raise ArgumentError, 'each include_paths Array element must not be empty' if path.empty?
         | 
| 78 | 
            -
                      raise ArgumentError, 'each include_paths Array element must start with a /' unless path.start_with?('/')
         | 
| 79 | 
            -
                    end
         | 
| 80 | 
            +
                    raise ArgumentError, 'policy AUD argument cannot be nil/empty'
         | 
| 80 81 | 
             
                  end
         | 
| 82 | 
            +
                end
         | 
| 81 83 |  | 
| 82 | 
            -
             | 
| 83 | 
            -
             | 
| 84 | 
            -
             | 
| 85 | 
            -
                     | 
| 86 | 
            -
                     | 
| 87 | 
            -
             | 
| 88 | 
            -
                    rescue DecodeTokenError => e
         | 
| 89 | 
            -
                      logger.info e.message
         | 
| 90 | 
            -
                      nil
         | 
| 91 | 
            -
                    end
         | 
| 92 | 
            -
             | 
| 93 | 
            -
                    if decoded_token
         | 
| 94 | 
            -
                      logger.debug 'CloudFlare JWT token is valid'
         | 
| 95 | 
            -
             | 
| 96 | 
            -
                      env['jwt.payload'] = decoded_token.first
         | 
| 97 | 
            -
                      env['jwt.header']  = decoded_token.last
         | 
| 98 | 
            -
                      @app.call(env)
         | 
| 99 | 
            -
                    else
         | 
| 100 | 
            -
                      return_error('Invalid token')
         | 
| 101 | 
            -
                    end
         | 
| 84 | 
            +
                # Private: Check paths type.
         | 
| 85 | 
            +
                def check_paths_type!
         | 
| 86 | 
            +
                  policies.each_key do |path|
         | 
| 87 | 
            +
                    raise ArgumentError, 'each key element must be a String' unless path.is_a?(String)
         | 
| 88 | 
            +
                    raise ArgumentError, 'each key element must not be empty' if path.empty?
         | 
| 89 | 
            +
                    raise ArgumentError, 'each key element must start with a /' unless path.start_with?('/')
         | 
| 102 90 | 
             
                  end
         | 
| 91 | 
            +
                end
         | 
| 103 92 |  | 
| 104 | 
            -
             | 
| 105 | 
            -
             | 
| 106 | 
            -
                  #  | 
| 107 | 
            -
                   | 
| 108 | 
            -
                   | 
| 109 | 
            -
                   | 
| 110 | 
            -
             | 
| 111 | 
            -
                   | 
| 112 | 
            -
             | 
| 113 | 
            -
             | 
| 114 | 
            -
                  # @raise [DecodeTokenError] if the token is invalid.
         | 
| 115 | 
            -
                  #
         | 
| 116 | 
            -
                  # @see https://github.com/jwt/ruby-jwt/tree/v2.2.1#algorithms-and-usage
         | 
| 117 | 
            -
                  def decode_token(token, secret)
         | 
| 118 | 
            -
                    Rack::JWT::Token.decode(token, secret, true, aud: policy_aud, verify_aud: true, algorithm: DEFAULT_ALGORITHM)
         | 
| 119 | 
            -
                  rescue ::JWT::VerificationError
         | 
| 120 | 
            -
                    raise DecodeTokenError, 'Invalid JWT token : Signature Verification Error'
         | 
| 121 | 
            -
                  rescue ::JWT::ExpiredSignature
         | 
| 122 | 
            -
                    raise DecodeTokenError, 'Invalid JWT token : Expired Signature (exp)'
         | 
| 123 | 
            -
                  rescue ::JWT::IncorrectAlgorithm
         | 
| 124 | 
            -
                    raise DecodeTokenError, 'Invalid JWT token : Incorrect Key Algorithm'
         | 
| 125 | 
            -
                  rescue ::JWT::ImmatureSignature
         | 
| 126 | 
            -
                    raise DecodeTokenError, 'Invalid JWT token : Immature Signature (nbf)'
         | 
| 127 | 
            -
                  rescue ::JWT::InvalidIssuerError
         | 
| 128 | 
            -
                    raise DecodeTokenError, 'Invalid JWT token : Invalid Issuer (iss)'
         | 
| 129 | 
            -
                  rescue ::JWT::InvalidIatError
         | 
| 130 | 
            -
                    raise DecodeTokenError, 'Invalid JWT token : Invalid Issued At (iat)'
         | 
| 131 | 
            -
                  rescue ::JWT::InvalidAudError
         | 
| 132 | 
            -
                    raise DecodeTokenError, 'Invalid JWT token : Invalid Audience (aud)'
         | 
| 133 | 
            -
                  rescue ::JWT::InvalidSubError
         | 
| 134 | 
            -
                    raise DecodeTokenError, 'Invalid JWT token : Invalid Subject (sub)'
         | 
| 135 | 
            -
                  rescue ::JWT::InvalidJtiError
         | 
| 136 | 
            -
                    raise DecodeTokenError, 'Invalid JWT token : Invalid JWT ID (jti)'
         | 
| 137 | 
            -
                  rescue ::JWT::DecodeError
         | 
| 138 | 
            -
                    raise DecodeTokenError, 'Invalid JWT token : Decode Error'
         | 
| 93 | 
            +
                # Private: Verify a token.
         | 
| 94 | 
            +
                def verify_token(env)
         | 
| 95 | 
            +
                  # extract the token from header.
         | 
| 96 | 
            +
                  token         = env[HEADER_NAME]
         | 
| 97 | 
            +
                  policy_aud    = policies.find { |path, _aud| env[PATH_INFO].start_with?(path) }&.last
         | 
| 98 | 
            +
                  decoded_token = public_keys(env).find do |key|
         | 
| 99 | 
            +
                    break decode_token(token, key.public_key, policy_aud)
         | 
| 100 | 
            +
                  rescue DecodeTokenError => e
         | 
| 101 | 
            +
                    logger.info e.message
         | 
| 102 | 
            +
                    nil
         | 
| 139 103 | 
             
                  end
         | 
| 140 104 |  | 
| 141 | 
            -
                   | 
| 142 | 
            -
             | 
| 143 | 
            -
                  # @return [Boolean] true if it is, false otherwise.
         | 
| 144 | 
            -
                  def path_matches_include_paths?(env)
         | 
| 145 | 
            -
                    include_paths.empty? || include_paths.any? { |ex| env['PATH_INFO'].start_with?(ex) }
         | 
| 146 | 
            -
                  end
         | 
| 105 | 
            +
                  if decoded_token
         | 
| 106 | 
            +
                    logger.debug 'CloudFlare JWT token is valid'
         | 
| 147 107 |  | 
| 148 | 
            -
             | 
| 149 | 
            -
             | 
| 150 | 
            -
             | 
| 151 | 
            -
                   | 
| 152 | 
            -
                     | 
| 108 | 
            +
                    env['jwt.payload'] = decoded_token.first
         | 
| 109 | 
            +
                    env['jwt.header']  = decoded_token.last
         | 
| 110 | 
            +
                    @app.call(env)
         | 
| 111 | 
            +
                  else
         | 
| 112 | 
            +
                    return_error('Invalid token')
         | 
| 153 113 | 
             
                  end
         | 
| 114 | 
            +
                end
         | 
| 154 115 |  | 
| 155 | 
            -
             | 
| 156 | 
            -
             | 
| 157 | 
            -
             | 
| 158 | 
            -
             | 
| 159 | 
            -
             | 
| 160 | 
            -
             | 
| 116 | 
            +
                # Private: Decode a token.
         | 
| 117 | 
            +
                #
         | 
| 118 | 
            +
                # @param token [String] the token.
         | 
| 119 | 
            +
                # @param secret [String] the public key.
         | 
| 120 | 
            +
                # @param policy_aud [String] the CloudFlare AUD.
         | 
| 121 | 
            +
                #
         | 
| 122 | 
            +
                # @example
         | 
| 123 | 
            +
                #
         | 
| 124 | 
            +
                #   [
         | 
| 125 | 
            +
                #     {"data"=>"test"}, # payload
         | 
| 126 | 
            +
                #     {"alg"=>"RS256"} # header
         | 
| 127 | 
            +
                #   ]
         | 
| 128 | 
            +
                #
         | 
| 129 | 
            +
                # @return [Array<Hash>] the token or `nil` at error.
         | 
| 130 | 
            +
                # @raise [DecodeTokenError] if the token is invalid.
         | 
| 131 | 
            +
                #
         | 
| 132 | 
            +
                # @see https://github.com/jwt/ruby-jwt/tree/v2.2.1#algorithms-and-usage
         | 
| 133 | 
            +
                def decode_token(token, secret, policy_aud)
         | 
| 134 | 
            +
                  Rack::JWT::Token.decode(token, secret, true, aud: policy_aud, verify_aud: true, algorithm: DEFAULT_ALGORITHM)
         | 
| 135 | 
            +
                rescue ::JWT::VerificationError
         | 
| 136 | 
            +
                  raise DecodeTokenError, 'Invalid JWT token : Signature Verification Error'
         | 
| 137 | 
            +
                rescue ::JWT::ExpiredSignature
         | 
| 138 | 
            +
                  raise DecodeTokenError, 'Invalid JWT token : Expired Signature (exp)'
         | 
| 139 | 
            +
                rescue ::JWT::IncorrectAlgorithm
         | 
| 140 | 
            +
                  raise DecodeTokenError, 'Invalid JWT token : Incorrect Key Algorithm'
         | 
| 141 | 
            +
                rescue ::JWT::ImmatureSignature
         | 
| 142 | 
            +
                  raise DecodeTokenError, 'Invalid JWT token : Immature Signature (nbf)'
         | 
| 143 | 
            +
                rescue ::JWT::InvalidIssuerError
         | 
| 144 | 
            +
                  raise DecodeTokenError, 'Invalid JWT token : Invalid Issuer (iss)'
         | 
| 145 | 
            +
                rescue ::JWT::InvalidIatError
         | 
| 146 | 
            +
                  raise DecodeTokenError, 'Invalid JWT token : Invalid Issued At (iat)'
         | 
| 147 | 
            +
                rescue ::JWT::InvalidAudError
         | 
| 148 | 
            +
                  raise DecodeTokenError, 'Invalid JWT token : Invalid Audience (aud)'
         | 
| 149 | 
            +
                rescue ::JWT::InvalidSubError
         | 
| 150 | 
            +
                  raise DecodeTokenError, 'Invalid JWT token : Invalid Subject (sub)'
         | 
| 151 | 
            +
                rescue ::JWT::InvalidJtiError
         | 
| 152 | 
            +
                  raise DecodeTokenError, 'Invalid JWT token : Invalid JWT ID (jti)'
         | 
| 153 | 
            +
                rescue ::JWT::DecodeError
         | 
| 154 | 
            +
                  raise DecodeTokenError, 'Invalid JWT token : Decode Error'
         | 
| 155 | 
            +
                end
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                # Private: Check if current path is in the policies.
         | 
| 158 | 
            +
                #
         | 
| 159 | 
            +
                # @return [Boolean] true if it is, false otherwise.
         | 
| 160 | 
            +
                def path_matches?(env)
         | 
| 161 | 
            +
                  policies.empty? || policies.keys.any? { |ex| env[PATH_INFO].start_with?(ex) }
         | 
| 162 | 
            +
                end
         | 
| 161 163 |  | 
| 162 | 
            -
             | 
| 163 | 
            -
             | 
| 164 | 
            -
             | 
| 165 | 
            -
             | 
| 164 | 
            +
                # Private: Check if auth header is invalid.
         | 
| 165 | 
            +
                #
         | 
| 166 | 
            +
                # @return [Boolean] true if it is, false otherwise.
         | 
| 167 | 
            +
                def invalid_auth_header?(env)
         | 
| 168 | 
            +
                  env[HEADER_NAME] !~ TOKEN_REGEX
         | 
| 169 | 
            +
                end
         | 
| 166 170 |  | 
| 167 | 
            -
             | 
| 168 | 
            -
             | 
| 171 | 
            +
                # Private: Check if no auth header.
         | 
| 172 | 
            +
                #
         | 
| 173 | 
            +
                # @return [Boolean] true if it is, false otherwise.
         | 
| 174 | 
            +
                def missing_auth_header?(env)
         | 
| 175 | 
            +
                  env[HEADER_NAME].nil? || env[HEADER_NAME].strip.empty?
         | 
| 176 | 
            +
                end
         | 
| 169 177 |  | 
| 170 | 
            -
             | 
| 171 | 
            -
             | 
| 172 | 
            -
                   | 
| 173 | 
            -
                   | 
| 174 | 
            -
                    host = env[HEADER_HTTP_HOST]
         | 
| 175 | 
            -
                    fetch_public_keys_cached(host).map do |jwk_data|
         | 
| 176 | 
            -
                      ::JWT::JWK.import(jwk_data).keypair
         | 
| 177 | 
            -
                    end
         | 
| 178 | 
            -
                  end
         | 
| 178 | 
            +
                # Private: Return an error.
         | 
| 179 | 
            +
                def return_error(message)
         | 
| 180 | 
            +
                  body    = { error: message }.to_json
         | 
| 181 | 
            +
                  headers = { 'Content-Type' => 'application/json' }
         | 
| 179 182 |  | 
| 180 | 
            -
                   | 
| 181 | 
            -
             | 
| 182 | 
            -
             | 
| 183 | 
            -
             | 
| 184 | 
            -
             | 
| 185 | 
            -
             | 
| 186 | 
            -
             | 
| 187 | 
            -
             | 
| 188 | 
            -
                   | 
| 189 | 
            -
                     | 
| 183 | 
            +
                  [403, headers, [body]]
         | 
| 184 | 
            +
                end
         | 
| 185 | 
            +
             | 
| 186 | 
            +
                # Private: Get public keys.
         | 
| 187 | 
            +
                #
         | 
| 188 | 
            +
                # @return [Array<OpenSSL::PKey::RSA>] the public keys.
         | 
| 189 | 
            +
                def public_keys(env)
         | 
| 190 | 
            +
                  host = env[HEADER_HTTP_HOST]
         | 
| 191 | 
            +
                  fetch_public_keys_cached(host).map do |jwk_data|
         | 
| 192 | 
            +
                    ::JWT::JWK.import(jwk_data).keypair
         | 
| 190 193 | 
             
                  end
         | 
| 194 | 
            +
                end
         | 
| 195 | 
            +
             | 
| 196 | 
            +
                # Private: Fetch public keys.
         | 
| 197 | 
            +
                #
         | 
| 198 | 
            +
                # @param host [String] The host.
         | 
| 199 | 
            +
                #
         | 
| 200 | 
            +
                # @return [Array<Hash>] the public keys.
         | 
| 201 | 
            +
                def fetch_public_keys(host)
         | 
| 202 | 
            +
                  json = Net::HTTP.get(host, CERTS_PATH)
         | 
| 203 | 
            +
                  json.empty? ? [] : MultiJson.load(json, symbolize_keys: true).fetch(:keys)
         | 
| 204 | 
            +
                rescue StandardError
         | 
| 205 | 
            +
                  []
         | 
| 206 | 
            +
                end
         | 
| 191 207 |  | 
| 192 | 
            -
             | 
| 193 | 
            -
             | 
| 194 | 
            -
             | 
| 195 | 
            -
             | 
| 196 | 
            -
             | 
| 197 | 
            -
             | 
| 198 | 
            -
             | 
| 199 | 
            -
             | 
| 200 | 
            -
             | 
| 201 | 
            -
             | 
| 202 | 
            -
             | 
| 203 | 
            -
             | 
| 204 | 
            -
             | 
| 205 | 
            -
             | 
| 206 | 
            -
             | 
| 207 | 
            -
             | 
| 208 | 
            -
             | 
| 209 | 
            -
                    end
         | 
| 208 | 
            +
                # Private: Get cached public keys.
         | 
| 209 | 
            +
                #
         | 
| 210 | 
            +
                # Store a keys in the cache only 10 minutes.
         | 
| 211 | 
            +
                #
         | 
| 212 | 
            +
                # @param host [String] The host.
         | 
| 213 | 
            +
                #
         | 
| 214 | 
            +
                # @return [Array<Hash>] the public keys.
         | 
| 215 | 
            +
                def fetch_public_keys_cached(host)
         | 
| 216 | 
            +
                  key = [self.class.name, '#secrets', host].join('_')
         | 
| 217 | 
            +
             | 
| 218 | 
            +
                  if defined? Rails
         | 
| 219 | 
            +
                    Rails.cache.fetch(key, expires_in: 600) { fetch_public_keys(host) }
         | 
| 220 | 
            +
                  elsif defined? Padrino
         | 
| 221 | 
            +
                    keys = Padrino.cache[key]
         | 
| 222 | 
            +
                    keys || Padrino.cache.store(key, fetch_public_keys(host), expires: 600)
         | 
| 223 | 
            +
                  else
         | 
| 224 | 
            +
                    fetch_public_keys(host)
         | 
| 210 225 | 
             
                  end
         | 
| 226 | 
            +
                end
         | 
| 211 227 |  | 
| 212 | 
            -
             | 
| 213 | 
            -
             | 
| 214 | 
            -
             | 
| 215 | 
            -
             | 
| 216 | 
            -
             | 
| 217 | 
            -
             | 
| 218 | 
            -
             | 
| 219 | 
            -
             | 
| 220 | 
            -
                    end
         | 
| 228 | 
            +
                # Private: Get a logger.
         | 
| 229 | 
            +
                #
         | 
| 230 | 
            +
                # @return [ActiveSupport::Logger] the logger.
         | 
| 231 | 
            +
                def logger
         | 
| 232 | 
            +
                  if defined? Rails
         | 
| 233 | 
            +
                    Rails.logger
         | 
| 234 | 
            +
                  elsif defined? Padrino
         | 
| 235 | 
            +
                    Padrino.logger
         | 
| 221 236 | 
             
                  end
         | 
| 222 237 | 
             
                end
         | 
| 223 238 | 
             
              end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: rack-cloudflare-jwt
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.0 | 
| 4 | 
            +
              version: 0.1.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Aleksei Vokhmin
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date:  | 
| 11 | 
            +
            date: 2021-03-10 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: bundler
         | 
| @@ -80,6 +80,20 @@ dependencies: | |
| 80 80 | 
             
                - - ">="
         | 
| 81 81 | 
             
                  - !ruby/object:Gem::Version
         | 
| 82 82 | 
             
                    version: 1.0.0
         | 
| 83 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 84 | 
            +
              name: rubocop-rspec
         | 
| 85 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 86 | 
            +
                requirements:
         | 
| 87 | 
            +
                - - ">="
         | 
| 88 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 89 | 
            +
                    version: 2.0.0
         | 
| 90 | 
            +
              type: :development
         | 
| 91 | 
            +
              prerelease: false
         | 
| 92 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 93 | 
            +
                requirements:
         | 
| 94 | 
            +
                - - ">="
         | 
| 95 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 96 | 
            +
                    version: 2.0.0
         | 
| 83 97 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 84 98 | 
             
              name: simplecov
         | 
| 85 99 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -174,6 +188,7 @@ extra_rdoc_files: [] | |
| 174 188 | 
             
            files:
         | 
| 175 189 | 
             
            - LICENSE
         | 
| 176 190 | 
             
            - README.md
         | 
| 191 | 
            +
            - lib/rack/cloudflare/jwt.rb
         | 
| 177 192 | 
             
            - lib/rack/cloudflare_jwt.rb
         | 
| 178 193 | 
             
            - lib/rack/cloudflare_jwt/auth.rb
         | 
| 179 194 | 
             
            - lib/rack/cloudflare_jwt/version.rb
         | 
| @@ -196,8 +211,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement | |
| 196 211 | 
             
                - !ruby/object:Gem::Version
         | 
| 197 212 | 
             
                  version: '0'
         | 
| 198 213 | 
             
            requirements: []
         | 
| 199 | 
            -
             | 
| 200 | 
            -
            rubygems_version: 2.7.8
         | 
| 214 | 
            +
            rubygems_version: 3.0.3
         | 
| 201 215 | 
             
            signing_key: 
         | 
| 202 216 | 
             
            specification_version: 4
         | 
| 203 217 | 
             
            summary: Rack middleware that provides authentication based on CloudFlare JSON Web
         |