grape-jwt-authentication 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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