warden_openid_bearer 0.1.4 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0775412bb8b81fb2caf0435f670021a0a65af25512364fd072afb68b9caa2aac
4
- data.tar.gz: 451d23f7debbb38ce46d024555f327a79ae7f13bc0e5b98dc47e2c63b402d6be
3
+ metadata.gz: 3185e14bde01f3b44bae689e3942051973e8a38ee25eb382c549a87175a80597
4
+ data.tar.gz: 00d8d47d1f1656ac9dafd8257b818c80b32ca3df22d63895b50765cabc29d199
5
5
  SHA512:
6
- metadata.gz: 76d650b1652813f9f0a8d83308a1caacacaa47958c1a3b721b7defc134cb9f8d512ebd0e0d016e29eabe3f230e0f11de17788bef993b28d8982c84ee2a4444c0
7
- data.tar.gz: 281d78bb3196f616dc2fe894a14f953e8b75577f69405b3a66f73cde5c8d0b15202321fc73c41ccda410643f4ba17f747e0ca5b84bd3118b38ab6d30f96bbee7
6
+ metadata.gz: 15407ea9f324dca19089df0862d150a303fe695313c3e1aad83dc023c824a6bcdbaa367e182c4f86014d8199d6341e9d62ec75b362e87c2a2e9ab41b742512a0
7
+ data.tar.gz: 80efe7bdd1fb90077dd74718144b5c014b165b04395ae3d8e4e54894e49436a211c844e74319cf4342b192cc1e6c024835ed4994147f1ef96d9a191389820942
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.2.1] - 2023-11-02
2
+ - Rewritten to *not* depend on the auth token being JWT (an assumption which only works with Keycloak)
3
+ - Support user-configured (bogus) certificate for development
4
+
5
+ N.B.: 0.2.0 only differs from 0.2.1 by the fact that `rake standard:fix` ran inbetweeen both.
6
+
1
7
  ## [0.1.4] - 2022-10-11
2
8
  - Clean up a stray `puts` left when debugging
3
9
 
data/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # WardenOpenidBearer
2
2
 
3
- [Warden](https://github.com/wardencommunity/warden) strategy for authentication with OpenID-Connect JWT bearer tokens.
3
+ [Warden](https://github.com/wardencommunity/warden) strategy for authentication with OpenID-Connect bearer tokens.
4
4
 
5
5
  This gem is like
6
6
  [the `warden_openid_auth gem`](https://rubygems.org/gems/warden_openid_auth),
7
7
  except that it only provides support for the very last step of
8
8
  the OAuth code flow, i.e. when the resource server / relying party
9
- (your Ruby Web app) validates and decodes the JWT token.
9
+ (your Ruby Web app) validates and decodes the bearer token.
10
10
 
11
11
  Use this gem if your client-side Web (or mobile) app will be taking
12
12
  care of the rest of the OAuth2 motions, such as redirecting (or
@@ -26,6 +26,14 @@ with iframes, etc.
26
26
  manager.default_strategies WardenOpenidBearer::Strategy.register!
27
27
  WardenOpenidBearer.configure do |oidc|
28
28
  oidc.openid_metadata_url = "https://example.com/.well-known/openid-configuration"
29
+ oidc.scope = ["openid", "email"]
30
+ oidc.redirect_uri = ["openid", "email"]
31
+ # Optional — Explicit OpenID-Connect server certificate (e.g. for a development rig):
32
+ oidc.openid_server_certificate = <<-CERT
33
+ -----BEGIN CERTIFICATE-----
34
+ MIIDCTBLAHBLAHBLAH==
35
+ -----END CERTIFICATE-----
36
+ CERT
29
37
  end
30
38
 
31
39
  manager.failure_app = Proc.new { |_env|
@@ -45,7 +53,7 @@ with iframes, etc.
45
53
  ### Subclassing
46
54
 
47
55
  Subclassing `WardenOpenidBearer::Strategy` is the recommended way to
48
- - support more than one authentication server (overriding `metadata_url` and/or `cache_timeout`),
56
+ - support more than one authentication server (overriding `valid?`, `metadata_url` and/or `cache_timeout`),
49
57
  - provide user hydration into the class of your choice (overriding `user_of_claims`).
50
58
 
51
59
  More details available in the rubydoc comments of
@@ -69,7 +77,7 @@ After checking out the Git repository, run `bin/setup` to install dependencies.
69
77
 
70
78
  The `debugger` gem is a development-time requirement (in the Gemfile). In order to activate it:
71
79
 
72
- 1. Uncomment the line that says `require "debug"` in `./spec_helper.rb`
80
+ 1. Uncomment the line that says `require "debug"` in `./spec/spec_helper.rb`
73
81
  1. Stick `debugger` somewhere in the source or test code
74
82
  1. Run the test suite
75
83
 
@@ -87,7 +95,7 @@ To release a new version:
87
95
 
88
96
  ## Contributing
89
97
 
90
- Bug reports and pull requests are welcome on GitHub at https://github.com/epfl-si/warden_openid_bearer.
98
+ Bug reports and pull requests are welcome on GitHub at https://github.com/epfl-si/warden_openid_bearer .
91
99
 
92
100
  ## License
93
101
 
@@ -4,31 +4,36 @@ module WardenOpenidBearer
4
4
  # We don't need an overengineered approach based on the Rails cache.
5
5
  # No, really.
6
6
  module CacheMixin
7
- def cached_by(key, &do_it)
8
- # We could support more complex types (e.g. arrays) as
9
- # value-type cache keys; but right now, our use cases don't
10
- # require it:
11
- is_value_type = key.is_a? String
12
- cache = if is_value_type
13
- @__cache_mixin__cache ||= {}
14
- else
15
- # Use the ::ObjectSpace::WeakMap private API, because the
16
- # endeavor of reinventing weak maps on top of (public)
17
- # WeakRef's would be called an inversion of abstraction and
18
- # would be considered harmful. Sue me (I have unit tests).
19
- @__cache_mixin__weakmap_cache ||= ::ObjectSpace::WeakMap.new
20
- end
7
+ def cached_by(*keys, &do_it)
8
+ @__cache_mixin__cache ||= {}
9
+
10
+ caller_method = caller(1..1).first[/`.*'/][1..-2]
11
+ keys.unshift(caller_method)
12
+
13
+ first_keys = keys.slice!(0, keys.length - 1).join("|")
14
+ last_key = keys[0]
15
+
16
+ last_key_is_value_type = last_key.is_a? String
17
+ cache = @__cache_mixin__cache[first_keys] ||= if last_key_is_value_type
18
+ {}
19
+ else
20
+ # Use the ::ObjectSpace::WeakMap private API, because the
21
+ # endeavor of reinventing weak maps on top of (public)
22
+ # WeakRef's would be called an inversion of abstraction and
23
+ # would be considered harmful. Sue me (I have unit tests).
24
+ ::ObjectSpace::WeakMap.new
25
+ end
21
26
 
22
27
  now = Time.now()
23
28
 
24
- if (cached = cache[key])
29
+ if (cached = cache[last_key])
25
30
  unless respond_to?(:cache_timeout) && now - cached[:fetched_at] > cache_timeout
26
31
  return cached[:payload]
27
32
  end
28
33
  end
29
34
 
30
35
  retval = do_it.call
31
- cache[key] = {payload: retval, fetched_at: now}
36
+ cache[last_key] = {payload: retval, fetched_at: now}
32
37
  retval
33
38
  end
34
39
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "net/http"
4
+ require "warden_openid_bearer/net_https"
4
5
 
5
6
  module WardenOpenidBearer
6
7
  # Cacheable configuration (periodically re-)fetched starting from
@@ -19,17 +20,11 @@ module WardenOpenidBearer
19
20
  # Provide a public API for tuning the timeout.
20
21
  attr_writer :cache_timeout
21
22
 
22
- def jwks
23
- json(metadata[:jwks_uri])
23
+ def userinfo_endpoint
24
+ metadata[:userinfo_endpoint]
24
25
  end
25
26
 
26
- def issuer
27
- metadata[:issuer]
28
- end
29
-
30
- def authorization_algs
31
- metadata[:authorization_signing_alg_values_supported]
32
- end
27
+ attr_writer :peer_cert
33
28
 
34
29
  private
35
30
 
@@ -39,7 +34,12 @@ module WardenOpenidBearer
39
34
 
40
35
  def json(uri)
41
36
  cached_by(uri) do
42
- JSON.parse(Net::HTTP.get_response(URI(uri)).body, symbolize_names: true)
37
+ response = if uri.scheme == "https"
38
+ WardenOpenidBearer::NetHTTPS.get_response(URI(uri), @peer_cert)
39
+ else
40
+ Net::HTTP.get_response(URI(uri))
41
+ end
42
+ JSON.parse(response.body, symbolize_names: true)
43
43
  end
44
44
  end
45
45
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+
5
+ module WardenOpenidBearer
6
+ # Like Net::HTTP, but with TLS and VERIFY_PEER always on.
7
+ class NetHTTPS < Net::HTTP
8
+ def initialize(*things)
9
+ super(*things)
10
+ self.use_ssl = true
11
+ self.verify_mode = OpenSSL::SSL::VERIFY_PEER
12
+ end
13
+
14
+ def peer_cert=(peer_cert)
15
+ self.verify_hostname = false
16
+ self.verify_callback = lambda do |preverify_ok, cert_store|
17
+ end_cert_der = cert_store.chain[0].to_der
18
+ return preverify_ok unless end_cert_der == cert_store.current_cert.to_der
19
+
20
+ return end_cert_der == peer_cert.to_der
21
+ end
22
+ end
23
+
24
+ def self.get_response(uri, peer_cert = nil)
25
+ https = new(uri.hostname, uri.port)
26
+ https.peer_cert = peer_cert if peer_cert
27
+
28
+ req = Net::HTTP::Get.new(uri)
29
+ https.start do |https|
30
+ https.request(req)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,13 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "jwt"
3
+ require "uri"
4
+ require "net/http"
5
+ require "warden_openid_bearer/net_https"
4
6
 
5
7
  module WardenOpenidBearer
6
8
  # Like `WardenOpenidAuth::Strategy` in
7
9
  # `lib/warden_openid_auth/strategy.rb` from the `warden_openid_auth`
8
10
  # gem, except done right for a modern, split-backend Web application
9
11
  # (in which the browser takes charge of the OAuth2 login dance, and
10
- # the back-end only checks signatures on the JWT claims).
12
+ # the back-end only validates bearer tokens).
11
13
  #
12
14
  # You shoud subclass `WardenOpenidBearer::Strategy` and override the
13
15
  # `user_of_claims` protected method if you want `env['warden'].user`
@@ -24,32 +26,28 @@ module WardenOpenidBearer
24
26
  include WardenOpenidBearer::Registerer # Provides self.register!
25
27
  include WardenOpenidBearer::CacheMixin
26
28
 
29
+ # Override in a subclass to support multiple authentication
30
+ # servers (if tokens can be discriminated between them somehow).
31
+ # The base class returns True whenever an `Authentication: Bearer`
32
+ # request header is present.
27
33
  def valid?
28
- return if !token
29
- # Do the issuer check here, so as to seamlessly support multiple
30
- # OIDC issuers inside the same app. If a token is not “for us”,
31
- # we want to defer to the other Warden strategy instances in the
32
- # stack (one which could typically be another instance of either
33
- # WardenOpenidBearer::Strategy, or one of its subclasses); therefore, we
34
- # want to return `false` if issuers don't match.
35
- untrusted_issuer == config.issuer
34
+ !!token
36
35
  end
37
36
 
38
37
  def authenticate!
39
- if (c = claims)
40
- success! user_of_claims(c)
38
+ res = oauth2_userinfo_response
39
+ body = res.body
40
+
41
+ if res.is_a?(Net::HTTPSuccess)
42
+ success! user_of_claims(JSON.parse(body))
41
43
  else
42
- # Given that `valid?` did return true previously,
43
- # we know the status with precision:
44
- fail! "Invalid OIDC bearer token"
44
+ fail! body
45
45
  end
46
- rescue JWT::ExpiredSignature
47
- fail! "Expired OIDC bearer token"
48
46
  end
49
47
 
50
48
  # Overridden to always return false, because we typically *don't*
51
49
  # want persistent sessions for an OpenID-Connect resource server —
52
- # Everything we need to know is in the JWT token.
50
+ # If we cached, we would break logout.
53
51
  def store?
54
52
  false
55
53
  end
@@ -57,7 +55,12 @@ module WardenOpenidBearer
57
55
  # Made public so that one may tune the `strategy.config.cache_timeout`:
58
56
  def config
59
57
  return @config if @config
58
+
60
59
  @config = WardenOpenidBearer::DiscoveredConfig.new(metadata_url)
60
+ if (peer_cert = WardenOpenidBearer.config.openid_server_certificate)
61
+ @config.peer_cert = peer_cert
62
+ end
63
+
61
64
  @config.cache_timeout = cache_timeout
62
65
  @config
63
66
  end
@@ -94,7 +97,7 @@ module WardenOpenidBearer
94
97
  WardenOpenidBearer.config.cache_timeout
95
98
  end
96
99
 
97
- # Returns the JWT token from `request.headers['Authorization']`
100
+ # Returns the bearer token from `request.headers['Authorization']`
98
101
  # (which may or may not be valid)
99
102
  def token
100
103
  # We call this one quite a lot, so we want some caching. Also,
@@ -107,40 +110,28 @@ module WardenOpenidBearer
107
110
  end
108
111
  end
109
112
 
110
- # Returns the JWT claims, only if the cryptographic signature and
111
- # other security requirements (in particular, the expiration
112
- # timestamp) check out.
113
- def claims
114
- JWT.decode(token, nil, true, jwt_decode_opts).first
115
- end
116
-
117
- def jwt_decode_opts
118
- # Note: issuer check was already done in `valid?`, see
119
- # explanations there; skip it here.
120
- {
121
- algorithm: algorithm,
122
- verify_expiration: true,
123
- verify_not_before: true,
124
- verify_iat: true,
125
- jwks: config.jwks
126
- }
127
- end
128
-
129
- def algorithm
130
- return untrusted_algorithm if
131
- config.authorization_algs.member? untrusted_algorithm
132
- end
133
-
134
- def untrusted_fields
135
- JWT.decode(token, nil, false)
113
+ def oauth2_userinfo_response
114
+ cached_by(request) do
115
+ _do_oauth2_userinfo
116
+ end
136
117
  end
137
118
 
138
- def untrusted_algorithm
139
- untrusted_fields.last["alg"]
140
- end
119
+ def _do_oauth2_userinfo
120
+ uri = URI.parse(config.userinfo_endpoint)
121
+ req = Net::HTTP::Get.new(uri)
122
+ req["Authorization"] = "Bearer #{token}"
141
123
 
142
- def untrusted_issuer
143
- untrusted_fields.first["iss"]
124
+ if uri.scheme == "https"
125
+ http = WardenOpenidBearer::NetHTTPS.new(uri.hostname, uri.port)
126
+ if (peer_cert = WardenOpenidBearer.config.openid_server_certificate)
127
+ http.peer_cert = peer_cert
128
+ end
129
+ else
130
+ http = Net::HTTP.new(uri.hostname, uri.port)
131
+ end
132
+ http.start do |http|
133
+ http.request(req)
134
+ end
144
135
  end
145
136
  end
146
137
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WardenOpenidBearer
4
- VERSION = "0.1.4"
4
+ VERSION = "0.2.1"
5
5
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "dry/configurable"
4
+ require "openssl"
5
+ require "uri"
4
6
 
5
7
  require_relative "warden_openid_bearer/version"
6
8
  require_relative "warden_openid_bearer/registerer"
@@ -12,5 +14,6 @@ module WardenOpenidBearer
12
14
  extend Dry::Configurable
13
15
 
14
16
  setting :openid_metadata_url, constructor: ->(url) { URI(url) }
17
+ setting :openid_server_certificate, default: nil, constructor: ->(pem) { pem ? OpenSSL::X509::Certificate.new(pem) : nil }
15
18
  setting :cache_timeout, default: 900
16
19
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: warden_openid_bearer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dominique Quatravaux
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-11 00:00:00.000000000 Z
11
+ date: 2023-11-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: warden
@@ -52,26 +52,12 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: 0.2.2
55
- - !ruby/object:Gem::Dependency
56
- name: jwt
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '2.5'
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '2.5'
69
55
  description: |2+
70
56
 
71
57
  This gem is like the `warden_openid_auth` gem, except that it only
72
58
  provides support for the very last step of the OAuth code flow, i.e.
73
59
  when the resource server / relying party (your Ruby Web app)
74
- validates and decodes the JWT token.
60
+ validates the bearer token.
75
61
 
76
62
  Use this gem if your client-side Web (or mobile) app will be taking
77
63
  care of the rest of the OAuth2 motions, such as redirecting (or
@@ -95,11 +81,11 @@ files:
95
81
  - lib/warden_openid_bearer.rb
96
82
  - lib/warden_openid_bearer/cache_mixin.rb
97
83
  - lib/warden_openid_bearer/discovered_config.rb
84
+ - lib/warden_openid_bearer/net_https.rb
98
85
  - lib/warden_openid_bearer/registerer.rb
99
86
  - lib/warden_openid_bearer/strategy.rb
100
87
  - lib/warden_openid_bearer/version.rb
101
88
  - sig/warden_openid_bearer.rbs
102
- - warden_openid_bearer.gemspec
103
89
  homepage: https://github.com/epfl-si/warden_openid_bearer
104
90
  licenses:
105
91
  - MIT
@@ -123,7 +109,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
109
  - !ruby/object:Gem::Version
124
110
  version: '0'
125
111
  requirements: []
126
- rubygems_version: 3.3.11
112
+ rubygems_version: 3.3.26
127
113
  signing_key:
128
114
  specification_version: 4
129
115
  summary: Warden strategy to validate OpenID-Connect bearer tokens
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/warden_openid_bearer/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "warden_openid_bearer"
7
- spec.version = WardenOpenidBearer::VERSION
8
- spec.authors = ["Dominique Quatravaux"]
9
- spec.email = ["dominique.quatravaux@epfl.ch"]
10
-
11
- spec.summary = "Warden strategy to validate OpenID-Connect bearer tokens"
12
- spec.description = <<~END_DESCRIPTION
13
-
14
- This gem is like the `warden_openid_auth` gem, except that it only
15
- provides support for the very last step of the OAuth code flow, i.e.
16
- when the resource server / relying party (your Ruby Web app)
17
- validates and decodes the JWT token.
18
-
19
- Use this gem if your client-side Web (or mobile) app will be taking
20
- care of the rest of the OAuth2 motions, such as redirecting (or
21
- opening a popup window) to the authentication server at login time,
22
- managing and refreshing tokens, doing all these unspeakable things
23
- with iframes, etc.
24
-
25
- END_DESCRIPTION
26
- spec.homepage = "https://github.com/epfl-si/warden_openid_bearer"
27
- spec.license = "MIT"
28
- spec.required_ruby_version = ">= 2.6.0"
29
-
30
- spec.metadata["homepage_uri"] = spec.homepage
31
- spec.metadata["source_code_uri"] = spec.homepage
32
- spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"
33
- spec.metadata["my_side_project_has_a_side_project"] = "https://github.com/epfl-si/rails.starterkit"
34
-
35
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
36
- spec.files = Dir.chdir(__dir__) do
37
- `git ls-files -z`.split("\x0").reject do |f|
38
- (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
39
- end
40
- end
41
- spec.require_paths = ["lib"]
42
-
43
- spec.add_dependency "warden", "~> 1.2.0"
44
- spec.add_dependency "dry-configurable", "~> 0.15.0"
45
- spec.add_dependency "net-http", "~> 0.2.2"
46
- spec.add_dependency "jwt", "~> 2.5"
47
- end