class JWT::Rack::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
ERRORS_TO_RESCUE
InvalidAuthHeaderFormat
JWT_DECODE_ERRORS
MissingAuthHeader
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/jwt/rack/auth.rb, line 64
def initialize(app, opts = {})
  @app     = app
  @secret  = opts.fetch(:secret, nil)
  @verify  = opts.fetch(:verify, true)
  @options = opts.fetch(:options, {})
  @exclude = opts.fetch(:exclude, [])

  @on_error = opts.fetch(:on_error, method(:default_on_error))

  @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!
  check_on_error_callable!
end

Public Instance Methods

call(env) click to toggle source
# File lib/jwt/rack/auth.rb, line 86
def call(env)
  if path_matches_excluded_path?(env)
    @app.call(env)
  else
    verify_token(env)
  end
end

Private Instance Methods

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

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

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

    unless x.start_with?('/')
      raise ArgumentError, 'each exclude Array element must start with a /'
    end
  end
end
check_on_error_callable!() click to toggle source
# File lib/jwt/rack/auth.rb, line 166
def check_on_error_callable!
  unless @on_error.respond_to?(:call)
    raise ArgumentError, 'on_error argument must respond to call'
  end
end
check_options_type!() click to toggle source
# File lib/jwt/rack/auth.rb, line 140
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/jwt/rack/auth.rb, line 118
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/jwt/rack/auth.rb, line 126
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/jwt/rack/auth.rb, line 112
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/jwt/rack/auth.rb, line 144
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/jwt/rack/auth.rb, line 134
def check_verify_type!
  unless verify.is_a?(TrueClass) || verify.is_a?(FalseClass)
    raise ArgumentError, 'verify argument must be true or false'
  end
end
default_on_error(error) click to toggle source
# File lib/jwt/rack/auth.rb, line 188
def default_on_error(error)
  error_message = {
    ::JWT::DecodeError => 'Invalid JWT token : Decode Error',
    ::JWT::VerificationError => 'Invalid JWT token : Signature Verification Error',
    ::JWT::ExpiredSignature => 'Invalid JWT token : Expired Signature (exp)',
    ::JWT::IncorrectAlgorithm => 'Invalid JWT token : Incorrect Key Algorithm',
    ::JWT::ImmatureSignature => 'Invalid JWT token : Immature Signature (nbf)',
    ::JWT::InvalidIssuerError => 'Invalid JWT token : Invalid Issuer (iss)',
    ::JWT::InvalidIatError => 'Invalid JWT token : Invalid Issued At (iat)',
    ::JWT::InvalidAudError => 'Invalid JWT token : Invalid Audience (aud)',
    ::JWT::InvalidSubError => 'Invalid JWT token : Invalid Subject (sub)',
    ::JWT::InvalidJtiError => 'Invalid JWT token : Invalid JWT ID (jti)',
    ::JWT::InvalidPayload => 'Invalid JWT token : Invalid Payload',
    MissingAuthHeader => 'Missing Authorization header',
    InvalidAuthHeaderFormat => 'Invalid Authorization header format'
  }
  message = error_message.fetch(error.class)
  body    = { error: message }.to_json
  headers = { 'Content-Type' => 'application/json', 'Content-Length' => body.bytesize.to_s }

  [401, headers, [body]]
end
invalid_auth_header?(env) click to toggle source
# File lib/jwt/rack/auth.rb, line 180
def invalid_auth_header?(env)
  !valid_auth_header?(env)
end
missing_auth_header?(env) click to toggle source
# File lib/jwt/rack/auth.rb, line 184
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/jwt/rack/auth.rb, line 172
def path_matches_excluded_path?(env)
  @exclude.any? { |ex| env['PATH_INFO'].start_with?(ex) }
end
valid_auth_header?(env) click to toggle source
# File lib/jwt/rack/auth.rb, line 176
def valid_auth_header?(env)
  env['HTTP_AUTHORIZATION'] =~ BEARER_TOKEN_REGEX
end
verify_token(env) click to toggle source
# File lib/jwt/rack/auth.rb, line 96
def verify_token(env)
  raise MissingAuthHeader if missing_auth_header?(env)
  raise InvalidAuthHeaderFormat if invalid_auth_header?(env)

  # extract the token from the Authorization: Bearer header
  # with a regex capture group.
  token = BEARER_TOKEN_REGEX.match(env['HTTP_AUTHORIZATION'])[1]

  decoded_token = Token.decode(token, @secret, @verify, @options)
  env['jwt.payload'] = decoded_token.first
  env['jwt.header'] = decoded_token.last
  @app.call(env)
rescue *ERRORS_TO_RESCUE => e
  @on_error.call(e)
end