Class: Rack::JsonWebTokenAuth

Inherits:
Object
  • Object
show all
Includes:
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

Overview

Rack Middleware for JSON Web Token Authentication

Defined Under Namespace

Classes: Algorithm, DecodedToken, DecodedTokenClaims, DecodedTokenHeader, Key, RackRequestHttpAuth, RackResponse, Resource, Resources

Constant Summary collapse

C =
Contracts
ENV_KEY =
'jwt.claims'.freeze
PATH_INFO_HEADER_KEY =
'PATH_INFO'.freeze
VERSION =
'0.1.1'.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.



23
24
25
26
27
# File 'lib/rack/json_web_token_auth.rb', line 23

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



113
114
115
# File 'lib/rack/json_web_token_auth.rb', line 113

def all_resources
  @all_resources ||= []
end

#call(env) ⇒ Object



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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/rack/json_web_token_auth.rb', line 48

def call(env)
  begin
    resource = resource_for_path(env[PATH_INFO_HEADER_KEY])

    if resource && resource.public_resource?
      # whitelisted as `unsecured`. skip all token authentication.
      @app.call(env)
    elsif resource.nil?
      # no matching `secured` or `unsecured` resource.
      # fail-safe with 401 unauthorized
      raise TokenError, 'No resource for path defined. Deny by default.'
    else
      # a `secured` resource, validate the token to see if authenticated

      # 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)

      # JwtClaims.verify returns a JWT claims set hash, if the
      # JWT Message Authentication Code (MAC), or signature,
      # are verified and the registered claims are also verified.
      if Contract.valid?(jwt, C::HashOf[ok: C::HashOf[Symbol => C::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, C::HashOf[error: C::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, C::HashOf[error: 'invalid JWT'])
        # the JWT MAC is not verified
        raise TokenError, 'invalid JWT'
      elsif Contract.valid?(jwt, C::HashOf[error: 'invalid input'])
        # otherwise
        raise TokenError, 'invalid JWT input'
      else
        raise TokenError, 'unhandled JWT error'
      end

      @app.call(env)
    end
  rescue TokenError => e
    body = e.message.nil? ? 'Unauthorized' : "Unauthorized : #{e.message}"
    headers = { 'WWW-Authenticate' => 'Bearer error="invalid_token"',
                'Content-Type' => 'text/plain',
                'Content-Length' => body.bytesize.to_s }
    [401, headers, [body]]
  rescue StandardError => e
    # puts e.message
    body = 'Unauthorized'
    headers = { 'WWW-Authenticate' => 'Bearer error="invalid_token"',
                'Content-Type' => 'text/plain',
                'Content-Length' => body.bytesize.to_s }
    [401, headers, [body]]
  end
end

#resource_for_path(path_info) ⇒ Object



118
119
120
121
122
123
124
125
# File 'lib/rack/json_web_token_auth.rb', line 118

def resource_for_path(path_info)
  all_resources.each do |r|
    if found = r.resource_for_path(path_info)
      return found
    end
  end
  nil
end

#secured(&block) ⇒ Object



30
31
32
33
34
35
36
# File 'lib/rack/json_web_token_auth.rb', line 30

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



39
40
41
42
43
44
45
# File 'lib/rack/json_web_token_auth.rb', line 39

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