omniauth-jwt2 0.1.0

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.
@@ -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