warden_openid_bearer 0.1.3 → 0.2.0

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: 7d1514a1fefcebd1c255a74d18df49f546d103ca7382e52464777f12c32f34e7
4
- data.tar.gz: 91c9843e458d7bf1cedc6f360f54f40a28b67eddc9891b9fe364251c6a1c78a0
3
+ metadata.gz: 968af30fb6b9b5daf5ec6f2c5a3ad26e776e2749a9f0fd5564458d436b98f244
4
+ data.tar.gz: b2fb133b65e2bba2c1484c0218de5ae83daf50cee85b02ed3bf4fb24402d1af1
5
5
  SHA512:
6
- metadata.gz: 7d82af75ad73daffb395154b35ecefef6196078e6bfc9333d0c8d8f59a9e218100d163fcd4f0a7c6f67098bc81d47a8bc92a0a1a2f26149ddca491f02bb3bce4
7
- data.tar.gz: 870825f9e3f800506a1c4b45ecd2783da72e8077c5a47a74abe2b4e3dc434c43ac8998cbbfe5f861aae5d964b39ebfd3aff42fd74f9aa12d070e794a17cf8f49
6
+ metadata.gz: 1f1f9e1be38b4577968d609454c09fd9fff276918f993a02de13bcef716c8f21be3d8a657065b9e8b078c57c4a19d3e03803b4c32505b6942c6a3c77855146d2
7
+ data.tar.gz: 53febc3f4cfe6d6e2d6928d864c1232cf3571fb72ba4ad8a44b803570cbd46b11666165080a592a573e55ea4d8a5ffd618781254808541d80210009a1c7882ed
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [0.2.0] - 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
+ ## [0.1.4] - 2022-10-11
6
+ - Clean up a stray `puts` left when debugging
7
+
1
8
  ## [0.1.3] - 2022-10-07
2
9
  - Fix `require`s
3
10
 
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[0][/`.*'/][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 = if last_key_is_value_type
18
+ @__cache_mixin__cache[first_keys] ||= {}
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
+ @__cache_mixin__cache[first_keys] ||= ::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
- require "net/http"
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,16 +20,12 @@ 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]
27
+ def peer_cert=(peer_cert)
28
+ @peer_cert = peer_cert
32
29
  end
33
30
 
34
31
  private
@@ -39,7 +36,12 @@ module WardenOpenidBearer
39
36
 
40
37
  def json(uri)
41
38
  cached_by(uri) do
42
- JSON.parse(Net::HTTP.get_response(URI(uri)).body, symbolize_names: true)
39
+ if uri.scheme == 'https'
40
+ response = WardenOpenidBearer::NetHTTPS.get_response(URI(uri), @peer_cert)
41
+ else
42
+ response = Net::HTTP.get_response(URI(uri))
43
+ end
44
+ JSON.parse(response.body, symbolize_names: true)
43
45
  end
44
46
  end
45
47
  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 = self.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,
@@ -102,46 +105,33 @@ module WardenOpenidBearer
102
105
  # this class and re-uses it across requests (see
103
106
  # `_fetch_strategy` in `lib/warden/proxy.rb`).
104
107
  cached_by(request) do
105
- puts request.headers
106
108
  strategy, token = (request.headers["Authorization"] || "").split(" ")
107
109
  token if (strategy || "").downcase == "bearer"
108
110
  end
109
111
  end
110
112
 
111
- # Returns the JWT claims, only if the cryptographic signature and
112
- # other security requirements (in particular, the expiration
113
- # timestamp) check out.
114
- def claims
115
- JWT.decode(token, nil, true, jwt_decode_opts).first
116
- end
117
-
118
- def jwt_decode_opts
119
- # Note: issuer check was already done in `valid?`, see
120
- # explanations there; skip it here.
121
- {
122
- algorithm: algorithm,
123
- verify_expiration: true,
124
- verify_not_before: true,
125
- verify_iat: true,
126
- jwks: config.jwks
127
- }
128
- end
129
-
130
- def algorithm
131
- return untrusted_algorithm if
132
- config.authorization_algs.member? untrusted_algorithm
133
- end
134
-
135
- def untrusted_fields
136
- JWT.decode(token, nil, false)
113
+ def oauth2_userinfo_response
114
+ cached_by(request) do
115
+ _do_oauth2_userinfo
116
+ end
137
117
  end
138
118
 
139
- def untrusted_algorithm
140
- untrusted_fields.last["alg"]
141
- 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}"
142
123
 
143
- def untrusted_issuer
144
- 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
145
135
  end
146
136
  end
147
137
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WardenOpenidBearer
4
- VERSION = "0.1.3"
4
+ VERSION = "0.2.0"
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) { if pem; OpenSSL::X509::Certificate.new(pem); else nil; end }
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.3
4
+ version: 0.2.0
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-07 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