omniauth-jwt2 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ source "https://rubygems.org"
6
+
7
+ # Gemfile is only for local development.
8
+ # On CI we only need the gemspecs' dependencies (including development dependencies).
9
+ # Exceptions, if any, will be found in gemfiles/*
10
+
11
+ # Coverage
12
+ eval_gemfile "contexts/coverage.gemfile"
13
+
14
+ # Testing
15
+ eval_gemfile "contexts/testing.gemfile"
16
+
17
+ # Debugging
18
+ eval_gemfile "contexts/debug.gemfile"
19
+
20
+ gemspec path: "../"
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ source "https://rubygems.org"
6
+
7
+ # Gemfile is only for local development.
8
+ # On CI we only need the gemspecs' dependencies (including development dependencies).
9
+ # Exceptions, if any, will be found in gemfiles/*
10
+
11
+ # Testing
12
+ gem "rspec", "~> 3.12" # ruby *
13
+ gem "rack-test", "~> 2.1" # ruby 2.0
14
+ gem "rack" # ruby 2.4
15
+ gem "rack-session", "< 2", github: "pboling/rack-session", branch: "fix-missing-rack-session" # ruby < 2.4
16
+ gem "json" # ruby 2.3
17
+ gem "openssl" # ruby 2.3
18
+ gem "openssl-signature_algorithm" # ruby 2.4
19
+ gem "ed25519" # ruby 2.4
20
+
21
+ # Debugging
22
+ eval_gemfile "contexts/debug.gemfile"
23
+
24
+ gemspec path: "../"
25
+
26
+ gem "omniauth", "< 2"
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ source "https://rubygems.org"
6
+
7
+ # Gemfile is only for local development.
8
+ # On CI we only need the gemspecs' dependencies (including development dependencies).
9
+ # Exceptions, if any, will be found in gemfiles/*
10
+
11
+ # Coverage
12
+ eval_gemfile "contexts/coverage.gemfile"
13
+
14
+ # Style
15
+ eval_gemfile "contexts/style.gemfile"
16
+
17
+ # Debugging
18
+ eval_gemfile "contexts/debug.gemfile"
19
+
20
+ gemspec path: "../"
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ source "https://rubygems.org"
6
+
7
+ # Gemfile is only for local development.
8
+ # On CI we only need the gemspecs' dependencies (including development dependencies).
9
+ # Exceptions, if any, will be found in gemfiles/*
10
+
11
+ # Coverage
12
+ eval_gemfile "contexts/coverage.gemfile"
13
+
14
+ # Testing
15
+ eval_gemfile "contexts/testing.gemfile"
16
+
17
+ # Debugging
18
+ eval_gemfile "contexts/debug.gemfile"
19
+
20
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ module Omniauth
2
+ module JWT
3
+ module Version
4
+ VERSION = "0.1.0"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ # External gems
2
+ require "version_gem"
3
+
4
+ # This gem
5
+ require "omniauth/jwt/version"
6
+ require "omniauth/strategies/jwt"
7
+
8
+ Omniauth::JWT::Version.class_eval do
9
+ extend VersionGem::Basic
10
+ end
@@ -0,0 +1,97 @@
1
+ require "omniauth"
2
+ require "jwt"
3
+
4
+ module OmniAuth
5
+ module Strategies
6
+ class JWT
7
+ class ClaimInvalid < StandardError; end
8
+
9
+ class BadJwt < StandardError; end
10
+
11
+ include OmniAuth::Strategy
12
+
13
+ args [:secret]
14
+
15
+ option :secret, nil
16
+ option :decode_options, {}
17
+ option :jwks_loader
18
+ option :algorithm, "HS256" # overridden by options.decode_options[:algorithms]
19
+ option :decode_options, {}
20
+ option :uid_claim, "email"
21
+ option :required_claims, %w(name email)
22
+ option :info_map, {name: "name", email: "email"}
23
+ option :auth_url, nil
24
+ option :valid_within, nil
25
+
26
+ def request_phase
27
+ redirect(options.auth_url)
28
+ end
29
+
30
+ def decoded
31
+ begin
32
+ secret = if defined?(OpenSSL)
33
+ case options.algorithm
34
+ when "RS256", "RS384", "RS512"
35
+ OpenSSL::PKey::RSA.new(options.secret).public_key
36
+ when "ES256", "ES384", "ES512"
37
+ OpenSSL::PKey::EC.new(options.secret)
38
+ when "HS256", "HS384", "HS512"
39
+ options.secret
40
+ else
41
+ raise NotImplementedError, "Unsupported algorithm: #{options.algorithm}"
42
+ end
43
+ else
44
+ options.secret
45
+ end
46
+
47
+ # JWT.decode can handle either algorithms or algorithm, but not both.
48
+ default_algos = options.decode_options.key?(:algorithms) ? options.decode_options[:algorithms] : [options.algorithm]
49
+ @decoded ||= ::JWT.decode(
50
+ request.params["jwt"],
51
+ secret,
52
+ true,
53
+ options.decode_options.merge(
54
+ {
55
+ algorithms: default_algos,
56
+ jwks: options.jwks_loader,
57
+ }.delete_if { |_, v| v.nil? },
58
+ ),
59
+ )[0]
60
+ rescue Exception => e
61
+ raise BadJwt.new("#{e.class}: #{e.message}")
62
+ end
63
+ (options.required_claims || []).each do |field|
64
+ raise ClaimInvalid.new("Missing required '#{field}' claim.") if !@decoded.key?(field.to_s)
65
+ end
66
+ raise ClaimInvalid.new("Missing required 'iat' claim.") if options.valid_within && !@decoded["iat"]
67
+ if options.valid_within && (Time.now.to_i - @decoded["iat"]).abs > options.valid_within.to_i
68
+ raise ClaimInvalid, "'iat' timestamp claim is too skewed from present"
69
+ end
70
+
71
+ @decoded
72
+ end
73
+
74
+ def callback_phase
75
+ super
76
+ rescue BadJwt => e
77
+ fail!("bad_jwt", e)
78
+ rescue ClaimInvalid => e
79
+ fail!(:claim_invalid, e)
80
+ end
81
+
82
+ uid { decoded[options.uid_claim] }
83
+
84
+ extra do
85
+ {raw_info: decoded}
86
+ end
87
+
88
+ info do
89
+ options.info_map.each_with_object({}) do |(k, v), h|
90
+ h[k.to_s] = decoded[v.to_s]
91
+ end
92
+ end
93
+ end
94
+
95
+ class Jwt < JWT; end
96
+ end
97
+ end
@@ -0,0 +1,41 @@
1
+ # Get the GEMFILE_VERSION without *require* "my_gem/version", for code coverage accuracy
2
+ # See: https://github.com/simplecov-ruby/simplecov/issues/557#issuecomment-825171399
3
+ load "lib/omniauth/jwt/version.rb"
4
+ gem_version = Omniauth::JWT::Version::VERSION
5
+ Omniauth::JWT::Version.send(:remove_const, :VERSION)
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "omniauth-jwt2"
9
+ spec.version = gem_version
10
+ spec.authors = ["Michael Bleigh", "Robin Ward", "Peter Boling"]
11
+ spec.email = ["mbleigh@mbleigh.com", "robin.ward@gmail.com", "peter.boling@gmail.com"]
12
+ spec.description = "An OmniAuth strategy to accept JWT-based single sign-on."
13
+ spec.summary = "An OmniAuth strategy to accept JWT-based single sign-on."
14
+ spec.homepage = "http://github.com/pboling/omniauth-jwt2"
15
+ spec.license = "MIT"
16
+ spec.required_ruby_version = ">= 2.2"
17
+
18
+ spec.files = %x(git ls-files).split($/)
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ # TODO: Since this gem supports Ruby >= 2.2 we need to ensure no gems are
23
+ # added here that require a newer version. Once this gem progresses to
24
+ # only support non-EOL Rubies, all dependencies can be listed in this
25
+ # gemspec, and the gemfiles/* pattern can be dispensed with.
26
+ spec.add_dependency("jwt", "~> 2.2", ">= 2.2.1") # ruby 2.1
27
+ spec.add_dependency("omniauth", ">= 1.1") # ruby 2.2
28
+
29
+ # Utilities
30
+ spec.add_dependency("version_gem", "~> 1.1", ">= 1.1.3") # ruby 2.2
31
+ spec.add_development_dependency("rake", "~> 13.0") # ruby 2.2, v13.1 is >= 2.3
32
+
33
+ # Hot reload
34
+ spec.add_development_dependency("guard", "~> 2.18", ">= 2.18.1") # ruby 1.9.3
35
+ spec.add_development_dependency("guard-rspec", "~> 4.7", ">= 4.7.3") # ruby *
36
+
37
+ # Testing
38
+ spec.add_development_dependency("rack-test", "~> 2.1") # ruby 2.0
39
+ spec.add_development_dependency("rspec", "~> 3.12") # ruby *
40
+ spec.add_development_dependency("rspec-pending_for", "~> 0.1") # ruby *
41
+ end
@@ -0,0 +1,213 @@
1
+ require "spec_helper"
2
+
3
+ describe OmniAuth::Strategies::JWT do
4
+ let(:response_json) { JSON.parse(last_response.body) }
5
+ let(:rand_secret) { SecureRandom.hex(10) }
6
+ let(:args) { [rand_secret, {auth_url: "http://example.com/login"}] }
7
+
8
+ let(:app) {
9
+ the_args = args
10
+ Rack::Builder.new do |b|
11
+ b.use Rack::Session::Cookie, secret: SecureRandom.hex(32)
12
+ b.use OmniAuth::Strategies::JWT, *the_args
13
+ b.run lambda { |env|
14
+ [200, {}, [(env["omniauth.auth"] || {}).to_json]]
15
+ }
16
+ end
17
+ }
18
+
19
+ context "request phase" do
20
+ it "redirects to the configured login url" do
21
+ # TODO: Figure out how to write this test without using the deprecated
22
+ # and unsafe, "get" method for the request phase.
23
+ get "/auth/jwt"
24
+ expect(last_response.status).to eq(302)
25
+ expect(last_response.headers["Location"]).to eq("http://example.com/login")
26
+ end
27
+ end
28
+
29
+ context "callback phase" do
30
+ it "decodes the response" do
31
+ encoded = JWT.encode({name: "Bob", email: "steve@example.com"}, rand_secret)
32
+ get "/auth/jwt/callback?jwt=" + encoded
33
+ expect(response_json["info"]["email"]).to eq("steve@example.com")
34
+ end
35
+
36
+ it "does not work without required fields" do
37
+ encoded = JWT.encode({name: "Steve"}, rand_secret)
38
+ get "/auth/jwt/callback?jwt=" + encoded
39
+ expect(last_response.status).to eq(302)
40
+ end
41
+
42
+ it "assigns the uid" do
43
+ encoded = JWT.encode({name: "Steve", email: "dude@awesome.com"}, rand_secret)
44
+ get "/auth/jwt/callback?jwt=" + encoded
45
+ expect(response_json["uid"]).to eq("dude@awesome.com")
46
+ end
47
+
48
+ context "with a non-default encoding algorithm" do
49
+ let(:args) { [rand_secret, {auth_url: "http://example.com/login", decode_options: {algorithms: ["HS512", "HS256"]}}] }
50
+
51
+ it "decodes the response with an allowed algorithm" do
52
+ encoded = JWT.encode({name: "Bob", email: "steve@example.com"}, rand_secret, "HS512")
53
+ get "/auth/jwt/callback?jwt=" + encoded
54
+ expect(JSON.parse(last_response.body)["info"]["email"]).to eq("steve@example.com")
55
+
56
+ encoded = JWT.encode({name: "Bob", email: "steve@example.com"}, rand_secret, "HS256")
57
+ get "/auth/jwt/callback?jwt=" + encoded
58
+ expect(JSON.parse(last_response.body)["info"]["email"]).to eq("steve@example.com")
59
+ end
60
+
61
+ it "fails decoding the response with a different algorithm" do
62
+ encoded = JWT.encode({name: "Bob", email: "steve@example.com"}, rand_secret, "HS384")
63
+ get "/auth/jwt/callback?jwt=" + encoded
64
+ expect(last_response.headers["Location"]).to include("/auth/failure")
65
+ end
66
+ end
67
+
68
+ context "with a :valid_within option set" do
69
+ let(:args) { [rand_secret, {auth_url: "http://example.com/login", valid_within: 300}] }
70
+
71
+ it "works if the iat key is within the time window" do
72
+ encoded = JWT.encode({name: "Ted", email: "ted@example.com", iat: Time.now.to_i}, rand_secret)
73
+ get "/auth/jwt/callback?jwt=" + encoded
74
+ expect(last_response.status).to eq(200)
75
+ end
76
+
77
+ it "does not work if the iat key is outside the time window" do
78
+ encoded = JWT.encode({name: "Ted", email: "ted@example.com", iat: Time.now.to_i + 500}, rand_secret)
79
+ get "/auth/jwt/callback?jwt=" + encoded
80
+ expect(last_response.status).to eq(302)
81
+ end
82
+
83
+ it "does not work if the iat key is missing" do
84
+ encoded = JWT.encode({name: "Ted", email: "ted@example.com"}, rand_secret)
85
+ get "/auth/jwt/callback?jwt=" + encoded
86
+ expect(last_response.status).to eq(302)
87
+ end
88
+ end
89
+ end
90
+
91
+ describe "#decoded" do
92
+ subject { described_class.new({}) }
93
+
94
+ let(:timestamp) { Time.now.to_i }
95
+ let(:claims) do
96
+ {
97
+ id: 123,
98
+ name: "user_example",
99
+ email: "user@example.com",
100
+ iat: timestamp,
101
+ }
102
+ end
103
+
104
+ let(:algorithm) { "HS256" }
105
+ let(:secret) { rand_secret }
106
+ let(:private_key) { secret }
107
+ let(:payload) { JWT.encode(claims, private_key, algorithm) }
108
+
109
+ before do
110
+ subject.options[:secret] = secret
111
+ subject.options[:algorithm] = algorithm
112
+
113
+ # We use Rack::Request instead of ActionDispatch::Request because
114
+ # Rack::Test::Methods enables testing of this module.
115
+ expect_next_instance_of(Rack::Request) do |rack_request|
116
+ expect(rack_request).to receive(:params).and_return("jwt" => payload)
117
+ end
118
+ end
119
+
120
+ ecdsa_named_curves = {
121
+ "ES256" => "prime256v1",
122
+ "ES384" => "secp384r1",
123
+ "ES512" => "secp521r1",
124
+ }.freeze
125
+
126
+ algos = {
127
+ OpenSSL::PKey::RSA => %w[RS256 RS384 RS512],
128
+ String => %w[HS256 HS384 HS512],
129
+ }
130
+ algos.merge!(OpenSSL::PKey::EC => %w[ES256 ES384 ES512]) unless ["2.2.10", "2.3.8"].include?(RubyVersion.to_s)
131
+ algos.each do |private_key_class, algorithms|
132
+ algorithms.each do |algorithm|
133
+ context "when the #{algorithm} algorithm is used" do
134
+ let(:algorithm) { algorithm }
135
+ let(:secret) do
136
+ # rubocop:disable Style/CaseLikeIf
137
+ if private_key_class == OpenSSL::PKey::RSA
138
+ private_key_class.generate(2048)
139
+ .to_pem
140
+ elsif private_key_class == OpenSSL::PKey::EC
141
+ private_key_class.generate(ecdsa_named_curves[algorithm])
142
+ .to_pem
143
+ else
144
+ private_key_class.new(rand_secret)
145
+ end
146
+ # rubocop:enable Style/CaseLikeIf
147
+ end
148
+
149
+ let(:private_key) { private_key_class ? private_key_class.new(secret) : secret }
150
+
151
+ it "decodes the user information" do
152
+ result = subject.decoded
153
+
154
+ expect(result).to eq(claims.stringify_keys)
155
+ end
156
+ end
157
+ end
158
+ end
159
+
160
+ context "required claims is missing" do
161
+ let(:claims) do
162
+ {
163
+ id: 123,
164
+ email: "user@example.com",
165
+ iat: timestamp,
166
+ }
167
+ end
168
+
169
+ it "raises error" do
170
+ expect { subject.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
171
+ end
172
+ end
173
+
174
+ context "when valid_within is specified but iat attribute is missing in response" do
175
+ let(:claims) do
176
+ {
177
+ id: 123,
178
+ name: "user_example",
179
+ email: "user@example.com",
180
+ }
181
+ end
182
+
183
+ before do
184
+ # Omniauth config values are always strings!
185
+ subject.options[:valid_within] = (60 * 60 * 24 * 2).to_s # 2 days
186
+ end
187
+
188
+ it "raises error" do
189
+ expect { subject.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
190
+ end
191
+ end
192
+
193
+ context "when timestamp claim is too skewed from present" do
194
+ let(:claims) do
195
+ {
196
+ id: 123,
197
+ name: "user_example",
198
+ email: "user@example.com",
199
+ iat: timestamp - (60 * 60 * 10), # minus ten minutes
200
+ }
201
+ end
202
+
203
+ before do
204
+ # Omniauth config values are always strings!
205
+ subject.options[:valid_within] = "2" # 2 seconds
206
+ end
207
+
208
+ it "raises error" do
209
+ expect { subject.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,64 @@
1
+ # Std Lib
2
+ require "securerandom"
3
+
4
+ # 3rd party gems
5
+ require "rspec/pending_for"
6
+ begin
7
+ require "rack/session"
8
+ rescue LoadError
9
+ nil # File won't exist in old rack for Ruby 2.2 & 2.3
10
+ end
11
+ require "rack/test"
12
+ require "json"
13
+ require "omniauth"
14
+ begin
15
+ require "openssl"
16
+ require "openssl/signature_algorithm"
17
+ require "ed25519"
18
+ rescue LoadError
19
+ nil # Gem doesn't exist for ancient Rubies 2.2 & 2.3
20
+ end
21
+
22
+ require "byebug" if ENV["DEBUG"] == "true"
23
+ # This does not require "simplecov",
24
+ # because that has a side-effect of running `.simplecov`
25
+ begin
26
+ require "kettle-soup-cover"
27
+ rescue LoadError
28
+ puts "Not analyzing test coverage"
29
+ end
30
+
31
+ require "support/hash"
32
+ require "support/next_instance_of"
33
+
34
+ OmniAuth.config.logger = Logger.new("/dev/null")
35
+ require "omniauth/version"
36
+ puts "OMNIAUTH VERSION: #{OmniAuth::VERSION}"
37
+ if Gem::Version.new(OmniAuth::VERSION) > Gem::Version.new("2.0")
38
+ OmniAuth.config.silence_get_warning = true
39
+ OmniAuth.config.allowed_request_methods |= [:get, :post]
40
+ end
41
+ # This file was generated by the `rspec --init` command. Conventionally, all
42
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
43
+ # Require this file using `require "spec_helper"` to ensure that it is only
44
+ # loaded once.
45
+ #
46
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
47
+ RSpec.configure do |config|
48
+ config.run_all_when_everything_filtered = true
49
+ config.filter_run :focus
50
+
51
+ include Rack::Test::Methods
52
+ include NextInstanceOf
53
+
54
+ # Run specs in random order to surface order dependencies. If you find an
55
+ # order dependency and want to debug it, you can fix the order by providing
56
+ # the seed, which is printed after each run.
57
+ # --seed 1234
58
+ config.order = "random"
59
+ end
60
+
61
+ # Last thing before loading this library, load simplecov:
62
+ require "simplecov" if defined?(Kettle::Soup::Cover) && Kettle::Soup::Cover::DO_COV
63
+
64
+ require "omniauth/jwt"
@@ -0,0 +1,9 @@
1
+ class Hash
2
+ def self.stringify_keys(h)
3
+ h.is_a?(Hash) ? h.collect { |k, v| [k.to_s, stringify_keys(v)] }.to_h : h
4
+ end
5
+
6
+ def stringify_keys
7
+ self.class.stringify_keys(self)
8
+ end
9
+ end
@@ -0,0 +1,43 @@
1
+ # From: https://github.com/gitlabhq/gitlabhq/blob/master/gems/gitlab-rspec/lib/gitlab/rspec/next_instance_of.rb#L4
2
+ module NextInstanceOf
3
+ def expect_next_instance_of(klass, *new_args, &blk)
4
+ stub_new(expect(klass), nil, false, *new_args, &blk)
5
+ end
6
+
7
+ def expect_next_instances_of(klass, number, ordered = false, *new_args, &blk)
8
+ stub_new(expect(klass), number, ordered, *new_args, &blk)
9
+ end
10
+
11
+ def allow_next_instance_of(klass, *new_args, &blk)
12
+ stub_new(allow(klass), nil, false, *new_args, &blk)
13
+ end
14
+
15
+ def allow_next_instances_of(klass, number, ordered = false, *new_args, &blk)
16
+ stub_new(allow(klass), number, ordered, *new_args, &blk)
17
+ end
18
+
19
+ private
20
+
21
+ def stub_new(target, number, ordered = false, *new_args, &blk)
22
+ receive_new = receive(:new)
23
+ receive_new.ordered if ordered
24
+ receive_new.with(*new_args) if !(new_args.empty? || new_args.blank?)
25
+
26
+ if number.is_a?(Range)
27
+ receive_new.at_least(number.begin).times if number.begin
28
+ receive_new.at_most(number.end).times if number.end
29
+ elsif number
30
+ receive_new.exactly(number).times
31
+ end
32
+
33
+ target.to receive_new.and_wrap_original do |*original_args, **original_kwargs|
34
+ method, *original_args = original_args
35
+ begin
36
+ method.call(*original_args, **original_kwargs).tap(&blk)
37
+ rescue ArgumentError
38
+ # Kludge for old ruby < 2.7
39
+ method.call(*original_args).tap(&blk)
40
+ end
41
+ end
42
+ end
43
+ end