grape-jwt-authentication 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "grape/jwt/authentication"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
Binary file
Binary file
Binary file
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'grape/jwt/authentication/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'grape-jwt-authentication'
9
+ spec.version = Grape::Jwt::Authentication::VERSION
10
+ spec.authors = ['Hermann Mayer']
11
+ spec.email = ['hermann.mayer92@gmail.com']
12
+
13
+ spec.summary = 'A reusable Grape JWT authentication concern'
14
+ spec.description = 'A reusable Grape JWT authentication concern'
15
+ spec.homepage = 'https://github.com/hausgold/grape-jwt-authentication'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = 'exe'
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_development_dependency 'bundler', '~> 1.16'
25
+ spec.add_development_dependency 'rake', '~> 10.0'
26
+ spec.add_development_dependency 'rspec', '~> 3.0'
27
+ spec.add_development_dependency 'simplecov', '~> 0.15'
28
+ spec.add_development_dependency 'vcr', '~> 3.0'
29
+ spec.add_development_dependency 'webmock', '~> 3.1'
30
+ spec.add_development_dependency 'timecop', '~> 0.9.1'
31
+ spec.add_development_dependency 'rack', '~> 2.0'
32
+ spec.add_development_dependency 'rack-test', '~> 0.8.2'
33
+
34
+ spec.add_runtime_dependency 'activesupport', '>= 3.2.0'
35
+ spec.add_runtime_dependency 'grape', '~> 1.0'
36
+ spec.add_runtime_dependency 'httparty'
37
+ spec.add_runtime_dependency 'jwt', '~> 2.1'
38
+ spec.add_runtime_dependency 'recursive-open-struct', '~> 1.0'
39
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/concern'
5
+ require 'active_support/configurable'
6
+ require 'active_support/cache'
7
+ require 'active_support/core_ext/hash'
8
+ require 'active_support/time'
9
+ require 'active_support/time_with_zone'
10
+
11
+ require 'jwt'
12
+
13
+ require 'grape'
14
+ require 'grape/jwt/authentication/version'
15
+ require 'grape/jwt/authentication/configuration'
16
+ require 'grape/jwt/authentication/jwt_handler'
17
+ require 'grape/jwt/authentication/jwt'
18
+ require 'grape/jwt/authentication/rsa_public_key'
19
+
20
+ module Grape
21
+ module Jwt
22
+ # The Grape JWT authentication concern.
23
+ module Authentication
24
+ extend ActiveSupport::Concern
25
+ include Grape::DSL::API
26
+
27
+ class << self
28
+ attr_writer :configuration
29
+ end
30
+
31
+ # Retrieve the current configuration object.
32
+ #
33
+ # @return [Configuration]
34
+ def self.configuration
35
+ @configuration ||= Configuration.new
36
+ end
37
+
38
+ # Configure the concern by providing a block which takes
39
+ # care of this task. Example:
40
+ #
41
+ # Grape::Jwt::Authentication.configure do |conf|
42
+ # # conf.xyz = [..]
43
+ # end
44
+ def self.configure
45
+ yield(configuration)
46
+ end
47
+
48
+ # Reset the current configuration with the default one.
49
+ def self.reset_configuration!
50
+ self.configuration = Configuration.new
51
+ end
52
+
53
+ included do
54
+ # Configure a new Grape authentication strategy which will be
55
+ # backed by the JwtHandler middleware. We do not want
56
+ # gem-internal claim verification or database lookup
57
+ # functionality. Let the user handle this the way he want.
58
+ Grape::Middleware::Auth::Strategies.add(:jwt,
59
+ JwtHandler,
60
+ ->(opts) { [opts] })
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Jwt
5
+ module Authentication
6
+ # The configuration for the Grape JWT authentication concern.
7
+ class Configuration
8
+ include ActiveSupport::Configurable
9
+
10
+ # The authenticator function which must be defined by the user to
11
+ # verify the given JSON Web Token. Here comes all your logic to lookup
12
+ # the related user on your database, the token claim verification
13
+ # and/or the token cryptographic signing. The function must return true
14
+ # or false to indicate the validity of the token.
15
+ #
16
+ # Grape::Jwt::Authentication.configure do |conf|
17
+ # conf.authenticator = proc do |token|
18
+ # # Verify the token the way you like.
19
+ # end
20
+ # end
21
+ config_accessor(:authenticator) { proc { false } }
22
+
23
+ # Whenever the given value on the +Authorization+ header is not a valid
24
+ # Bearer authentication scheme or the token itself is not a valid JSON
25
+ # Web Token, this user defined function will be called. You can add
26
+ # custom handling of this situations, like responding different HTTP
27
+ # status code, or bodies. By default the Rack stack will be interrupted
28
+ # and a response with the 400 Bad Request status code will be send to
29
+ # the client. The raw token (value of the +Authorization+ header) and
30
+ # the Rack app will be injected to your function for maximum
31
+ # flexibility.
32
+ #
33
+ # Grape::Jwt::Authentication.configure do |conf|
34
+ # conf.malformed_auth_handler = proc do |raw_token, app|
35
+ # # Do your own error handling.
36
+ # end
37
+ # end
38
+ config_accessor(:malformed_auth_handler) do
39
+ proc do |_raw_token, _app|
40
+ [400, {}, ['Authorization header is malformed.']]
41
+ end
42
+ end
43
+
44
+ # When the client sends a corrected formatted JSON Web Token with the
45
+ # Bearer authentication scheme within the +Authorization+ header and
46
+ # your authenticator fails for some reason (token claims, wrong
47
+ # audience, bad subject, expired token, wrong cryptographic signing
48
+ # etc), this function is called to handle the bad authentication. By
49
+ # default the Rack stack will be interrupted and a response with the
50
+ # 401 Unauthorized status code will be send to the client. You can
51
+ # customize this the way you like and send different error codes, or
52
+ # handle the error completely different. The parsed JSON Web Token and
53
+ # the Rack app will be injected to your function to allow any
54
+ # customized error handling.
55
+ #
56
+ # Grape::Jwt::Authentication.configure do |conf|
57
+ # conf.failed_auth_handler = proc do |token, app|
58
+ # # Do your own error handling.
59
+ # end
60
+ # end
61
+ config_accessor(:failed_auth_handler) do
62
+ proc do |_token, _app|
63
+ [401, {}, ['Access denied.']]
64
+ end
65
+ end
66
+
67
+ # Whenever you want to use the {RsaPublicKey} class you configure the
68
+ # default URL on the singleton instance, or use the gem configure
69
+ # method and set it up accordingly. We allow the fetch of the public
70
+ # key from a remote server (HTTP/HTTPS) or from a local file which is
71
+ # accessible by the ruby process. Specify the URL or the local path
72
+ # here.
73
+ config_accessor(:rsa_public_key_url) { nil }
74
+
75
+ # You can preconfigure the {RsaPublickey} class to enable/disable
76
+ # caching. For a remote public key location it is handy to cache the
77
+ # result for some time to keep the traffic low to this resource server.
78
+ # For a local file you can skip this.
79
+ config_accessor(:rsa_public_key_caching) { false }
80
+
81
+ # When you make use of the caching of the {RsaPublicKey} class you can
82
+ # fine tune the expiration time of this cache. The RSA public key from
83
+ # your identity provider should not change this frequent, so a cache
84
+ # for at least one hour is fine. You should not set it lower than one
85
+ # minute. Keep this setting in mind when you change keys. Your
86
+ # infrastructure could be inoperable for this configured time.
87
+ config_accessor(:rsa_public_key_expiration) { 1.hour }
88
+
89
+ # The JSON Web Token isser which should be used for verification.
90
+ config_accessor(:jwt_issuer) { nil }
91
+
92
+ # The resource server (namely the one which configures this right now)
93
+ # which MUST be present on the JSON Web Token audience claim.
94
+ config_accessor(:jwt_beholder) { nil }
95
+
96
+ # You can configure a different JSON Web Token verification option hash
97
+ # if your algorithm differs or you want some extra/different options.
98
+ # Just watch out that you have to pass a proc to this configuration
99
+ # property. On the {Grape::Jwt::Authentication::Jwt} class it has to be
100
+ # a simple hash. The default is here the RS256 algorithm with enabled
101
+ # expiration check, and issuer+audience check when the
102
+ # {jwt_issuer}/{jwt_beholder} are configured accordingly.
103
+ config_accessor(:jwt_options) do
104
+ proc do
105
+ conf = Grape::Jwt::Authentication.configuration
106
+ { algorithm: 'RS256',
107
+ exp_leeway: 30.seconds.to_i,
108
+ iss: conf.jwt_issuer,
109
+ verify_iss: !conf.jwt_issuer.nil?,
110
+ aud: conf.jwt_beholder,
111
+ verify_aud: !conf.jwt_beholder.nil?,
112
+ # @TODO: https://github.com/jwt/ruby-jwt/issues/247
113
+ verify_iat: false }
114
+ end
115
+ end
116
+
117
+ # You can configure your own verification key on the Jwt wrapper class.
118
+ # This way you can pass your HMAC secret or your ECDSA public key to
119
+ # the JSON Web Token validation method. Here you need to pass a proc,
120
+ # on the {Grape::Jwt::Authentication::Jwt} class it has to be a scalar
121
+ # value. By default we use the
122
+ # {Grape::Jwt::Authentication::RsaPublicKey} class to retrieve the RSA
123
+ # public key.
124
+ config_accessor(:jwt_verification_key) do
125
+ proc { RsaPublicKey.instance.fetch }
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'recursive-open-struct'
4
+
5
+ module Grape
6
+ module Jwt
7
+ module Authentication
8
+ # A easy to use model for verification of JSON Web Tokens. This is just a
9
+ # wrapper class for the excellent ruby-jwt gem. It's completely up to you
10
+ # to use it. But be aware, its a bit optinionated by default.
11
+ class Jwt
12
+ # All the following JWT verification issues lead to a failed validation.
13
+ RESCUE_JWT_EXCEPTIONS = [
14
+ ::JWT::DecodeError,
15
+ ::JWT::VerificationError,
16
+ ::JWT::ExpiredSignature,
17
+ ::JWT::IncorrectAlgorithm,
18
+ ::JWT::ImmatureSignature,
19
+ ::JWT::InvalidIssuerError,
20
+ ::JWT::InvalidIatError,
21
+ ::JWT::InvalidAudError,
22
+ ::JWT::InvalidSubError,
23
+ ::JWT::InvalidJtiError,
24
+ ::JWT::InvalidPayload
25
+ ].freeze
26
+
27
+ # :reek:Attribute because its fine to be extern-modifiable at these
28
+ # instances
29
+ attr_reader :payload, :token
30
+ attr_writer :verification_key, :jwt_options
31
+ attr_accessor :issuer, :beholder
32
+
33
+ # Setup a new JWT instance. You have to pass the raw JSON Web Token to
34
+ # the initializer. Example:
35
+ #
36
+ # Jwt.new('j.w.t')
37
+ # # => <Jwt>
38
+ #
39
+ # @return [Jwt]
40
+ def initialize(token)
41
+ parsed_payload = JWT.decode(token, nil, false).first.symbolize_keys
42
+ @token = token
43
+ @payload = RecursiveOpenStruct.new(parsed_payload)
44
+ end
45
+
46
+ # Checks if the payload says this is a refresh token.
47
+ #
48
+ # @return [Boolean] Whenever this is a access token
49
+ def access_token?
50
+ payload.typ == 'access'
51
+ end
52
+
53
+ # Checks if the payload says this is a refresh token.
54
+ #
55
+ # @return [Boolean] Whenever this is a refresh token
56
+ def refresh_token?
57
+ payload.typ == 'refresh'
58
+ end
59
+
60
+ # Retrives the expiration date from the payload when set.
61
+ #
62
+ # @return [nil|ActiveSupport::TimeWithZone] The expiration date
63
+ def expires_at
64
+ exp = payload.exp
65
+ return nil unless exp
66
+ Time.zone.at(exp)
67
+ end
68
+
69
+ # Deliver the public key for verification by default. This uses the
70
+ # {RsaPublicKey} class, but you can configure the verification key the
71
+ # way you like. (Especially for different algorithms, like HMAC or
72
+ # ECDSA) Just make use of the same named setter.
73
+ #
74
+ # @return [OpenSSL::PKey::RSA|Mixed] The verification key
75
+ def verification_key
76
+ unless @verification_key
77
+ conf = Grape::Jwt::Authentication.configuration
78
+ return conf.jwt_verification_key.call
79
+ end
80
+ @verification_key
81
+ end
82
+
83
+ # This getter passes back the default JWT verification option hash
84
+ # which is optinionated. You can change this the way you like by
85
+ # configuring your options with the help of the same named setter.
86
+ #
87
+ # @return [Hash] The JWT verification options hash
88
+ def jwt_options
89
+ unless @jwt_options
90
+ conf = Grape::Jwt::Authentication.configuration
91
+ return conf.jwt_options.call
92
+ end
93
+ @jwt_options
94
+ end
95
+
96
+ # Verify the current token by our hard and strict rules. Whenever the
97
+ # token was not parsed from a string, we encode the current state to a
98
+ # JWT string representation and check this.
99
+ #
100
+ # @return [Boolean] Whenever the token is valid or not
101
+ #
102
+ # :reek:NilCheck because we have to check the token
103
+ # origin and react on it
104
+ def valid?
105
+ JWT.decode(token, verification_key, true, jwt_options) && true
106
+ rescue *RESCUE_JWT_EXCEPTIONS
107
+ false
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Jwt
5
+ module Authentication
6
+ # Take care of the token validation and verification on this
7
+ # Rack middleware. It is a self contained implementation of a
8
+ # valid Rack handler which checks for a common JWT token
9
+ # Authorization header and calls a user given verification block
10
+ # which performs the database lookup or whatever is necessary
11
+ # for the verification.
12
+ class JwtHandler
13
+ # A internal exception handling for failed authentications.
14
+ class AuthenticationError < StandardError; end
15
+ # A internal exception handling for malformed headers.
16
+ class MalformedHeaderError < StandardError; end
17
+
18
+ # A common JWT validation regex which meets the RFC specs.
19
+ JWT_REGEX = /^[a-zA-Z0-9\-_]+?\.[a-zA-Z0-9\-_]+?\.([a-zA-Z0-9\-_]+)?$/
20
+
21
+ # Initialize a new Rack middleware for Bearer token
22
+ # processing.
23
+ #
24
+ # @param app [Proc] The regular Rack application
25
+ # @param options [Hash] A global-overwritting configuration hash
26
+ def initialize(app, options = {})
27
+ @app = app
28
+ @options = options
29
+ end
30
+
31
+ # A shared configuration lookup helper which selects the requested
32
+ # entry from the local or global configuration object. The local
33
+ # configuration takes presedence over the global one.
34
+ #
35
+ # @param key [Symbol] The local config key
36
+ # @param global_key [Symbol] The global config key
37
+ # @return [Mixed] The configuration value
38
+ def config(key, global_key)
39
+ block = @options[key]
40
+ unless block
41
+ global_conf = Grape::Jwt::Authentication.configuration
42
+ return global_conf.send(global_key)
43
+ end
44
+ block
45
+ end
46
+
47
+ # Get the local or global defined authenticator for the JWT handler.
48
+ #
49
+ # @return [Proc] The authenticator block
50
+ def authenticator
51
+ config(:proc, :authenticator)
52
+ end
53
+
54
+ # Get the local or global defined malformed authentication handler for
55
+ # the JWT handler.
56
+ #
57
+ # @return [Proc] The malformed authorization handler block
58
+ def malformed_handler
59
+ config(:malformed, :malformed_auth_handler)
60
+ end
61
+
62
+ # Get the local or global defined failed authentication handler for the
63
+ # JWT handler.
64
+ #
65
+ # @return [Proc] The failed authentication handler block
66
+ def failed_handler
67
+ config(:failed, :failed_auth_handler)
68
+ end
69
+
70
+ # Validate the Bearer authentication scheme on the given
71
+ # authorization header and validate the JWT token when it was
72
+ # found.
73
+ #
74
+ # @param header [String] The authorization header value
75
+ # @return [String] The parsed and well-formatted JWT
76
+ def parse_token(header)
77
+ token = header.to_s.scan(/^Bearer (.*)/).flatten.first
78
+ raise MalformedHeaderError unless JWT_REGEX =~ token
79
+ token
80
+ end
81
+
82
+ # Perform the authentication logic on the Rack compatible
83
+ # interface.
84
+ #
85
+ # :reek:TooManyStatements because reek counts exception
86
+ # handling as statements
87
+ def call(env)
88
+ # Unfortunately Grape's middleware stack orders the error
89
+ # handling higher than the formatter. So when a error is
90
+ # raised, the Rack env was not yet analysed and the content
91
+ # type not negotiated. This would result in allways-JSON
92
+ # responses on authentication errors. We want to be smarter
93
+ # here and respond in the requested format on authentication
94
+ # errors, that why we invoke the formatter middleware here.
95
+ Grape::Middleware::Formatter.new(->(_) {}).call(env)
96
+
97
+ # Parse the JWT token and give it to the user defined block
98
+ # for futher verification. The user given block MUST return
99
+ # a positive result to allow the request to be further
100
+ # processed, or a negative result to stop processing.
101
+ token = parse_token(env['HTTP_AUTHORIZATION'])
102
+ raise AuthenticationError unless authenticator.call(token)
103
+
104
+ # Looks like we are on a good path and the given token was
105
+ # valid on all checks. So we continue the regular
106
+ # application logic now.
107
+ @app.call(env)
108
+ rescue MalformedHeaderError
109
+ # Call the user defined malformed authentication handler.
110
+ malformed_handler.call(env['HTTP_AUTHORIZATION'], @app)
111
+ rescue AuthenticationError
112
+ # Call the user defined failed authentication handler.
113
+ failed_handler.call(token, @app)
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end