Class: Rack::JsonWebTokenAuth

Inherits:
Object
  • Object
show all
Includes:
Contracts::Builtin, Contracts::Core
Defined in:
lib/rack/json_web_token_auth.rb,
lib/rack/json_web_token_auth/version.rb,
lib/rack/json_web_token_auth/resource.rb,
lib/rack/json_web_token_auth/contracts.rb,
lib/rack/json_web_token_auth/resources.rb,
lib/rack/json_web_token_auth/exceptions.rb

Overview

Rack Middleware for JSON Web Token Authentication

Defined Under Namespace

Classes: HttpMethodError, HttpMethods, Key, RackRequestHttpAuth, RackResponse, Resource, ResourceHttpMethods, Resources, TokenError

Constant Summary collapse

ENV_KEY =
'jwt.claims'.freeze
PATH_INFO_HEADER_KEY =
'PATH_INFO'.freeze
VERSION =
'0.2.0'.freeze
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.
%r{
  ^Bearer\s{1}(       # starts with Bearer and a single space
  [a-zA-Z0-9\-\_]+\.  # 1 or more chars followed by a single period
  [a-zA-Z0-9\-\_]+\.  # 1 or more chars followed by a single period
  [a-zA-Z0-9\-\_]*    # 0 or more chars, no trailing chars
  )$
}x

Instance Method Summary collapse

Constructor Details

#initialize(app, &block) ⇒ JsonWebTokenAuth

Returns a new instance of JsonWebTokenAuth.



21
22
23
24
25
# 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

Instance Method Details

#all_resourcesObject



86
87
88
# File 'lib/rack/json_web_token_auth.rb', line 86

def all_resources
  @all_resources ||= []
end

#call(env) ⇒ Object



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# 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) ⇒ Object



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# 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) ⇒ Object



91
92
93
94
95
96
97
# 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) ⇒ Object



100
101
102
103
104
105
106
# 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) ⇒ Object



28
29
30
31
32
33
34
# 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) ⇒ Object



37
38
39
40
41
42
43
# 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