class Rack::JWT::Auth

Authentication middleware

Constants

BEARER_TOKEN_REGEX

The last segment gets dropped for ‘none’ algorithm since there is no signature so both of these patterns are valid. All character chunks are base64url format and periods.

Bearer abc123.abc123.abc123
Bearer abc123.abc123.
DEFAULT_ALGORITHM
SUPPORTED_ALGORITHMS

Attributes

exclude[R]
options[R]
secret[R]
verify[R]

Public Class Methods

new(app, opts = {}) click to toggle source

Initialization should fail fast with an ArgumentError if any args are invalid.

# File lib/rack/jwt/auth.rb, line 43
def initialize(app, opts = {})
  @app     = app
  @secret  = opts.fetch(:secret, nil)
  @verify  = opts.fetch(:verify, true)
  @options = opts.fetch(:options, {})
  @exclude = opts.fetch(:exclude, [])

  @secret  = @secret.strip if @secret.is_a?(String)
  @options[:algorithm] = DEFAULT_ALGORITHM if @options[:algorithm].nil?

  check_secret_type!
  check_secret!
  check_secret_and_verify_for_none_alg!
  check_verify_type!
  check_options_type!
  check_valid_algorithm!
  check_exclude_type!
end

Public Instance Methods

call(env) click to toggle source
# File lib/rack/jwt/auth.rb, line 62
def call(env)
  if path_matches_excluded_path?(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

check_exclude_type!() click to toggle source
# File lib/rack/jwt/auth.rb, line 149
def check_exclude_type!
  unless @exclude.is_a?(Array)
    raise ArgumentError, 'exclude argument must be an Array'
  end

  @exclude.each do |x|
    unless x.is_a?(String)
      raise ArgumentError, 'each exclude Array element must be a String'
    end

    if x.empty?
      raise ArgumentError, 'each exclude Array element must not be empty'
    end

    unless x.start_with?('/')
      raise ArgumentError, 'each exclude Array element must start with a /'
    end
  end
end
check_options_type!() click to toggle source
# File lib/rack/jwt/auth.rb, line 137
def check_options_type!
  raise ArgumentError, 'options argument must be a Hash' unless options.is_a?(Hash)
end
check_secret!() click to toggle source
# File lib/rack/jwt/auth.rb, line 115
def check_secret!
  if @secret.nil? || (@secret.is_a?(String) && @secret.empty?)
    unless @options[:algorithm] == 'none'
      raise ArgumentError, 'secret argument can only be nil/empty for the "none" algorithm'
    end
  end
end
check_secret_and_verify_for_none_alg!() click to toggle source
# File lib/rack/jwt/auth.rb, line 123
def check_secret_and_verify_for_none_alg!
  if @options && @options[:algorithm] && @options[:algorithm] == 'none'
    unless @secret.nil? && @verify.is_a?(FalseClass)
      raise ArgumentError, 'when "none" the secret must be "nil" and verify "false"'
    end
  end
end
check_secret_type!() click to toggle source
# File lib/rack/jwt/auth.rb, line 109
def check_secret_type!
  unless Token.secret_of_valid_type?(@secret)
    raise ArgumentError, 'secret argument must be a valid type'
  end
end
check_valid_algorithm!() click to toggle source
# File lib/rack/jwt/auth.rb, line 141
def check_valid_algorithm!
  unless @options &&
         @options[:algorithm] &&
         SUPPORTED_ALGORITHMS.include?(@options[:algorithm])
    raise ArgumentError, 'algorithm argument must be a supported type'
  end
end
check_verify_type!() click to toggle source
# File lib/rack/jwt/auth.rb, line 131
def check_verify_type!
  unless verify.is_a?(TrueClass) || verify.is_a?(FalseClass)
    raise ArgumentError, 'verify argument must be true or false'
  end
end
invalid_auth_header?(env) click to toggle source
# File lib/rack/jwt/auth.rb, line 177
def invalid_auth_header?(env)
  !valid_auth_header?(env)
end
missing_auth_header?(env) click to toggle source
# File lib/rack/jwt/auth.rb, line 181
def missing_auth_header?(env)
  env['HTTP_AUTHORIZATION'].nil? || env['HTTP_AUTHORIZATION'].strip.empty?
end
path_matches_excluded_path?(env) click to toggle source
# File lib/rack/jwt/auth.rb, line 169
def path_matches_excluded_path?(env)
  @exclude.any? { |ex| env['PATH_INFO'].start_with?(ex) }
end
return_error(message) click to toggle source
# File lib/rack/jwt/auth.rb, line 185
def return_error(message)
  body    = { error: message }.to_json
  headers = { 'Content-Type' => 'application/json' }

  [401, headers, [body]]
end
valid_auth_header?(env) click to toggle source
# File lib/rack/jwt/auth.rb, line 173
def valid_auth_header?(env)
  env['HTTP_AUTHORIZATION'] =~ BEARER_TOKEN_REGEX
end
verify_token(env) click to toggle source
# File lib/rack/jwt/auth.rb, line 76
def verify_token(env)
  # extract the token from the Authorization: Bearer header
  # with a regex capture group.
  token = BEARER_TOKEN_REGEX.match(env['HTTP_AUTHORIZATION'])[1]

  begin
    decoded_token = Token.decode(token, @secret, @verify, @options)
    env['jwt.payload'] = decoded_token.first
    env['jwt.header'] = decoded_token.last
    @app.call(env)
  rescue ::JWT::VerificationError
    return_error('Invalid JWT token : Signature Verification Error')
  rescue ::JWT::ExpiredSignature
    return_error('Invalid JWT token : Expired Signature (exp)')
  rescue ::JWT::IncorrectAlgorithm
    return_error('Invalid JWT token : Incorrect Key Algorithm')
  rescue ::JWT::ImmatureSignature
    return_error('Invalid JWT token : Immature Signature (nbf)')
  rescue ::JWT::InvalidIssuerError
    return_error('Invalid JWT token : Invalid Issuer (iss)')
  rescue ::JWT::InvalidIatError
    return_error('Invalid JWT token : Invalid Issued At (iat)')
  rescue ::JWT::InvalidAudError
    return_error('Invalid JWT token : Invalid Audience (aud)')
  rescue ::JWT::InvalidSubError
    return_error('Invalid JWT token : Invalid Subject (sub)')
  rescue ::JWT::InvalidJtiError
    return_error('Invalid JWT token : Invalid JWT ID (jti)')
  rescue ::JWT::DecodeError
    return_error('Invalid JWT token : Decode Error')
  end
end