class Rack::JsonWebTokenAuth

Rack Middleware for JSON Web Token Authentication

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.
ENV_KEY
PATH_INFO_HEADER_KEY
VERSION

Public Class Methods

new(app, &block) click to toggle source
# File lib/rack/json_web_token_auth.rb, line 21
def initialize(app, &block)
  @app = app
  # execute the block methods provided in the context of this class
  instance_eval(&block)
end

Public Instance Methods

all_resources() click to toggle source
# File lib/rack/json_web_token_auth.rb, line 86
def all_resources
  @all_resources ||= []
end
call(env) click to toggle source
# File lib/rack/json_web_token_auth.rb, line 46
def call(env)
  resource = resource_for_path(env[PATH_INFO_HEADER_KEY])

  # no matching `secured` or `unsecured` resource.
  # fail-safe with 401 unauthorized
  if resource.nil?
    raise TokenError, 'No resource for path defined. Deny by default.'
  end

  if resource.public_resource?
    # whitelisted as `unsecured`. skip all token authentication.
    @app.call(env)
  else
    # HTTP method not permitted
    if resource.invalid_http_method?(env['REQUEST_METHOD'])
      raise HttpMethodError, 'HTTP request method denied'
    end

    # Test that `env` has a well formed Authorization header
    unless Contract.valid?(env, RackRequestHttpAuth)
      raise TokenError, 'malformed Authorization header or token'
    end

    # Extract the token from the 'Authorization: Bearer token' string
    token = BEARER_TOKEN_REGEX.match(env['HTTP_AUTHORIZATION'])[1]

    # Verify the token and its claims are valid
    jwt_opts = resource.opts[:jwt]
    jwt = ::JwtClaims.verify(token, jwt_opts)
    handle_token(env, jwt)

    @app.call(env)
  end
rescue TokenError => e
  return_401(e.message)
rescue StandardError
  return_401
end
handle_token(env, jwt) click to toggle source
# File lib/rack/json_web_token_auth.rb, line 112
def handle_token(env, jwt)
  if Contract.valid?(jwt, HashOf[ok: HashOf[Symbol => Any]])
    # Authenticated! Pass all claims into the app env for app use
    # with the hash keys converted to strings to match Rack env.
    env[ENV_KEY] = Hashie.stringify_keys(jwt[:ok])
  elsif Contract.valid?(jwt, HashOf[error: ArrayOf[Symbol]])
    # a list of any registered claims that fail validation, if the JWT MAC is verified
    raise TokenError, "invalid JWT claims : #{jwt[:error].sort.join(', ')}"
  elsif Contract.valid?(jwt, HashOf[error: 'invalid JWT'])
    # the JWT MAC is not verified
    raise TokenError, 'invalid JWT'
  elsif Contract.valid?(jwt, HashOf[error: 'invalid input'])
    # otherwise
    raise TokenError, 'invalid JWT input'
  else
    raise TokenError, 'unhandled JWT error'
  end
end
resource_for_path(path_info) click to toggle source
# File lib/rack/json_web_token_auth.rb, line 91
def resource_for_path(path_info)
  all_resources.each do |r|
    found = r.resource_for_path(path_info)
    return found unless found.nil?
  end
  nil
end
return_401(msg = nil) click to toggle source
# File lib/rack/json_web_token_auth.rb, line 100
def return_401(msg = nil)
  body = msg.nil? ? 'Unauthorized' : "Unauthorized : #{msg}"
  headers = { 'WWW-Authenticate' => 'Bearer error="invalid_token"',
              'Content-Type' => 'text/plain',
              'Content-Length' => body.bytesize.to_s }
  [401, headers, [body]]
end
secured(&block) click to toggle source
# File lib/rack/json_web_token_auth.rb, line 28
def secured(&block)
  resources = Resources.new(public_resource: false)
  # execute the methods in the 'secured' block in the context of
  # a new Resources object
  resources.instance_eval(&block)
  all_resources << resources
end
unsecured(&block) click to toggle source
# File lib/rack/json_web_token_auth.rb, line 37
def unsecured(&block)
  resources = Resources.new(public_resource: true)
  # execute the methods in the 'unsecured' block in the context of
  # a new Resources object
  resources.instance_eval(&block)
  all_resources << resources
end