class Rack::CloudflareJwt::Auth
Authentication middleware
@see developers.cloudflare.com/access/setting-up-access/validate-jwt-tokens/
Constants
- CERTS_PATH
Certs path
- DEFAULT_ALGORITHM
Default algorithm
- HEADER_NAME
CloudFlare JWT header.
- PATH_INFO
Key for get current path.
- TOKEN_REGEX
Token regex.
@see github.com/jwt/ruby-jwt/tree/v2.2.1#algorithms-and-usage
Attributes
Public Class Methods
Initializes middleware
@example Initialize middleware in Rails
config.middleware.use( Rack::CloudflareJwt::Auth, ENV['RACK_CLOUDFLARE_JWT_TEAM_DOMAIN'], '/admin' => <cloudflare-aud-1>, '/manager' => <cloudflare-aud-2>, )
@param team_domain
[String] the Team Domain (e.g. 'test.cloudflareaccess.com'). @param policies [Hash<String, String>] the policies with paths and AUDs.
# File lib/rack/cloudflare_jwt/auth.rb, line 50 def initialize(app, team_domain, policies = {}) @app = app @team_domain = team_domain @policies = policies check_policy_auds! check_paths_type! end
Public Instance Methods
Public: Call a middleware.
# File lib/rack/cloudflare_jwt/auth.rb, line 60 def call(env) if !path_matches?(env) @app.call(env) elsif missing_auth_header?(env) return_error('Missing Authorization header') elsif invalid_auth_header?(env) return_error('Invalid Authorization header format') else verify_token(env) end end
Private Instance Methods
Private: Check paths type.
# File lib/rack/cloudflare_jwt/auth.rb, line 86 def check_paths_type! policies.each_key do |path| raise ArgumentError, 'each key element must be a String' unless path.is_a?(String) raise ArgumentError, 'each key element must not be empty' if path.empty? raise ArgumentError, 'each key element must start with a /' unless path.start_with?('/') end end
Private: Check policy auds.
# File lib/rack/cloudflare_jwt/auth.rb, line 75 def check_policy_auds! raise ArgumentError, 'policies cannot be nil/empty' if policies.values.empty? policies.each_value do |policy_aud| next unless !policy_aud.is_a?(String) || policy_aud.strip.empty? raise ArgumentError, 'policy AUD argument cannot be nil/empty' end end
Private: Decode a token.
@param token [String] the token. @param secret [String] the public key. @param policy_aud [String] the CloudFlare AUD.
@example
[ {"data"=>"test"}, # payload {"alg"=>"RS256"} # header ]
@return [Array<Hash>] the token or `nil` at error. @raise [DecodeTokenError] if the token is invalid.
@see github.com/jwt/ruby-jwt/tree/v2.2.1#algorithms-and-usage
# File lib/rack/cloudflare_jwt/auth.rb, line 134 def decode_token(token, secret, policy_aud) Rack::JWT::Token.decode(token, secret, true, aud: policy_aud, verify_aud: true, algorithm: DEFAULT_ALGORITHM) rescue ::JWT::VerificationError raise DecodeTokenError, 'Invalid JWT token : Signature Verification Error' rescue ::JWT::ExpiredSignature raise DecodeTokenError, 'Invalid JWT token : Expired Signature (exp)' rescue ::JWT::IncorrectAlgorithm raise DecodeTokenError, 'Invalid JWT token : Incorrect Key Algorithm' rescue ::JWT::ImmatureSignature raise DecodeTokenError, 'Invalid JWT token : Immature Signature (nbf)' rescue ::JWT::InvalidIssuerError raise DecodeTokenError, 'Invalid JWT token : Invalid Issuer (iss)' rescue ::JWT::InvalidIatError raise DecodeTokenError, 'Invalid JWT token : Invalid Issued At (iat)' rescue ::JWT::InvalidAudError raise DecodeTokenError, 'Invalid JWT token : Invalid Audience (aud)' rescue ::JWT::InvalidSubError raise DecodeTokenError, 'Invalid JWT token : Invalid Subject (sub)' rescue ::JWT::InvalidJtiError raise DecodeTokenError, 'Invalid JWT token : Invalid JWT ID (jti)' rescue ::JWT::DecodeError raise DecodeTokenError, 'Invalid JWT token : Decode Error' end
Private: Fetch public keys.
@return [Array<Hash>] the public keys.
# File lib/rack/cloudflare_jwt/auth.rb, line 199 def fetch_public_keys json = Net::HTTP.get(team_domain, CERTS_PATH) json.empty? ? [] : MultiJson.load(json, symbolize_keys: true).fetch(:keys) rescue StandardError [] end
Private: Get cached public keys.
Store a keys in the cache only 10 minutes.
@return [Array<Hash>] the public keys.
# File lib/rack/cloudflare_jwt/auth.rb, line 211 def fetch_public_keys_cached key = [self.class.name, '#secrets'].join('_') if defined? Rails Rails.cache.fetch(key, expires_in: 600) { fetch_public_keys } elsif defined? Padrino keys = Padrino.cache[key] keys || Padrino.cache.store(key, fetch_public_keys, expires: 600) else fetch_public_keys end end
Private: Check if auth header is invalid.
@return [Boolean] true if it is, false otherwise.
# File lib/rack/cloudflare_jwt/auth.rb, line 168 def invalid_auth_header?(env) env[HEADER_NAME] !~ TOKEN_REGEX end
Private: Get a logger.
@return [ActiveSupport::Logger] the logger.
# File lib/rack/cloudflare_jwt/auth.rb, line 227 def logger if defined? Rails Rails.logger elsif defined? Padrino Padrino.logger end end
Private: Check if no auth header.
@return [Boolean] true if it is, false otherwise.
# File lib/rack/cloudflare_jwt/auth.rb, line 175 def missing_auth_header?(env) env[HEADER_NAME].nil? || env[HEADER_NAME].strip.empty? end
Private: Check if current path is in the policies.
@return [Boolean] true if it is, false otherwise.
# File lib/rack/cloudflare_jwt/auth.rb, line 161 def path_matches?(env) policies.empty? || policies.keys.any? { |ex| env[PATH_INFO].start_with?(ex) } end
Private: Get public keys.
@return [Array<OpenSSL::PKey::RSA>] the public keys.
# File lib/rack/cloudflare_jwt/auth.rb, line 190 def public_keys fetch_public_keys_cached.map do |jwk_data| ::JWT::JWK.import(jwk_data).keypair end end
Private: Return an error.
# File lib/rack/cloudflare_jwt/auth.rb, line 180 def return_error(message) body = { error: message }.to_json headers = { 'Content-Type' => 'application/json' } [403, headers, [body]] end
Private: Verify a token.
# File lib/rack/cloudflare_jwt/auth.rb, line 95 def verify_token(env) # extract the token from header. token = env[HEADER_NAME] policy_aud = policies.find { |path, _aud| env[PATH_INFO].start_with?(path) }&.last decoded_token = public_keys.find do |key| break decode_token(token, key.public_key, policy_aud) rescue DecodeTokenError => e logger.info e.message nil end if decoded_token logger.debug 'CloudFlare JWT token is valid' env['jwt.payload'] = decoded_token.first env['jwt.header'] = decoded_token.last @app.call(env) else return_error('Invalid token') end end