warden_openid_bearer 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f228ea62cd8329a39ec230cb8b764c8b83db2557a767c41f0f18b28f35fabe8c
4
+ data.tar.gz: 11272a48dc32c8063f08957d7e409100bfb14818a0795a86acbf9416b88a2b3d
5
+ SHA512:
6
+ metadata.gz: 0602d2ade77d620f6aab62b0f0c55e02f4ff4d7ceb360d33c875a18b1318ec4b8664f3d0b0fc8a6eee8c423d8674dbfe1b3ad28feb80d2638489f013171a252c
7
+ data.tar.gz: dfe6d0e9418db37a762124a8cd94a00ab1127ceef9617acfcbc7bb94683ee92f6f661efde51a570a5b79d0fa21db4f480ac1e868b6ec4250065f73625f419046
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/testdouble/standard
3
+ ruby_version: 2.6
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2022-10-07
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in warden_openid_bearer.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
11
+
12
+ gem "standard", "~> 1.3"
13
+
14
+ gem "debug"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Dominique Quatravaux
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # WardenOpenidBearer
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/warden_openid_bearer`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Install the gem and add to the application's Gemfile by executing:
10
+
11
+ $ bundle add warden_openid_bearer
12
+
13
+ If bundler is not being used to manage dependencies, install the gem by executing:
14
+
15
+ $ gem install warden_openid_bearer
16
+
17
+ ## Usage
18
+
19
+ TODO: Write usage instructions here
20
+
21
+ ## Development
22
+
23
+ After checking out the Git repository, run `bin/setup` to install dependencies. Then, run `bundle exec rake` to run the test suite and linter checks. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
24
+
25
+ ### Debugger
26
+
27
+ The `debugger` gem is a development-time requirement (in the Gemfile). In order to activate it:
28
+
29
+ 1. Uncomment the line that says `require "debug"` in `./spec_helper.rb`
30
+ 1. Stick `debugger` somewhere in the source or test code
31
+ 1. Run the test suite
32
+
33
+ ### Local Install
34
+
35
+ To install this gem onto your local machine, run `bundle exec rake install`.
36
+
37
+ ### Release
38
+
39
+ To release a new version:
40
+ 1. Make sure that the version you want to publish is the current `master` branch on GitHub, and that the tests are green
41
+ 1. Check out the `master` branch in your working directory
42
+ 1. Update the version number in `version.rb`
43
+ 1. Run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org)
44
+
45
+ ## Contributing
46
+
47
+ Bug reports and pull requests are welcome on GitHub at https://github.com/epfl-si/warden_openid_bearer.
48
+
49
+ ## License
50
+
51
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
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
+ require "standard/rake"
9
+
10
+ task default: %i[spec standard]
@@ -0,0 +1,33 @@
1
+ module WardenOpenidBearer
2
+ # We don't need an overengineered approach based on the Rails cache.
3
+ # No, really.
4
+ module CacheMixin
5
+ def cached_by(key, &do_it)
6
+ # We could support more complex types (e.g. arrays) as
7
+ # value-type cache keys; but right now, our use cases don't
8
+ # require it:
9
+ is_value_type = key.is_a? String
10
+ cache = if is_value_type
11
+ @__cache_mixin__cache ||= {}
12
+ else
13
+ # Use the ::ObjectSpace::WeakMap private API, because the
14
+ # endeavor of reinventing weak maps on top of (public)
15
+ # WeakRef's would be called an inversion of abstraction and
16
+ # would be considered harmful. Sue me (I have unit tests).
17
+ @__cache_mixin__weakmap_cache ||= ::ObjectSpace::WeakMap.new
18
+ end
19
+
20
+ now = Time.now()
21
+
22
+ if (cached = cache[key])
23
+ unless respond_to?(:cache_timeout) && now - cached[:fetched_at] > cache_timeout
24
+ return cached[:payload]
25
+ end
26
+ end
27
+
28
+ retval = do_it.call
29
+ cache[key] = {payload: retval, fetched_at: now}
30
+ retval
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,42 @@
1
+ module WardenOpenidBearer
2
+ # Cacheable configuration (periodically re-)fetched starting from
3
+ # the OpenID authentication server's “well-known” endpoint
4
+ class DiscoveredConfig
5
+ include CacheMixin
6
+
7
+ def initialize(metadata_uri)
8
+ @metadata_uri = metadata_uri
9
+ end
10
+
11
+ # Called by the CacheMixin.
12
+ def cache_timeout
13
+ @cache_timeout ||= 900
14
+ end
15
+ # Provide a public API for tuning the timeout.
16
+ attr_writer :cache_timeout
17
+
18
+ def jwks
19
+ json(metadata[:jwks_uri])
20
+ end
21
+
22
+ def issuer
23
+ metadata[:issuer]
24
+ end
25
+
26
+ def authorization_algs
27
+ metadata[:authorization_signing_alg_values_supported]
28
+ end
29
+
30
+ private
31
+
32
+ def metadata
33
+ json(@metadata_uri)
34
+ end
35
+
36
+ def json(uri)
37
+ cached_by(uri) do
38
+ JSON.parse(Net::HTTP.get_response(URI(uri)).body, symbolize_names: true)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,31 @@
1
+ require "warden"
2
+
3
+ module WardenOpenidBearer
4
+ # Add this mixin to your `Warden::Strategies::Base` subclass to
5
+ # streamline the `Warden::Strategies.add()` business.
6
+ #
7
+ # If you mix this into `Your::Class` (or inherit from one that
8
+ # does, such as `OIDCBearer::Strategy`), then you can say
9
+ #
10
+ # manager.default_strategies Your::Class.register!
11
+ #
12
+ module Registerer
13
+ def self.included(klass)
14
+ klass.extend(ClassMethods)
15
+ end
16
+
17
+ module ClassMethods
18
+ def register!(as_symbol = default_registration_symbol)
19
+ return @registered_symbol if @registered_symbol
20
+ Warden::Strategies.add(as_symbol, self)
21
+ @registered_symbol = as_symbol
22
+ end
23
+
24
+ protected
25
+
26
+ def default_registration_symbol
27
+ name.delete(":").sub(/Strategy$/, "").underscore.to_sym
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,143 @@
1
+ # Like `WardenOpenidAuth::Strategy` in
2
+ # `lib/warden_openid_auth/strategy.rb` from the `warden_openid_auth`
3
+ # gem, except done right for a modern, split-backend Web application
4
+ # (in which the browser takes charge of the OAuth2 login dance, and
5
+ # the back-end only checks signatures on the JWT claims).
6
+ #
7
+ # You shoud subclass `WardenOpenidBearer::Strategy` and override the
8
+ # `user_of_claims` protected method if you want `env['warden'].user`
9
+ # to be a “real” user object (instead of just a hash of OIDC claims,
10
+ # which is what happens when using `WardenOpenidBearer::Strategy` directly).
11
+ # If you want your Rails app to support more than one OIDC
12
+ # authentication server, you should also subclass
13
+ # `WardenOpenidBearer::Strategy` and override the `metadata_url` method.
14
+ #
15
+ # This class has a `self.register!` method, which makes things
16
+ # (slightly) easier than calling `Warden::Strategies.add` yourself.
17
+ # See `WardenOpenidBearer::Registerer` for details.
18
+ module WardenOpenidBearer
19
+ class Strategy < Warden::Strategies::Base
20
+ include WardenOpenidBearer::Registerer # Provides self.register!
21
+ include WardenOpenidBearer::CacheMixin
22
+
23
+ def valid?
24
+ return if !token
25
+ # Do the issuer check here, so as to seamlessly support multiple
26
+ # OIDC issuers inside the same app. If a token is not “for us”,
27
+ # we want to defer to the other Warden strategy instances in the
28
+ # stack (one which could typically be another instance of either
29
+ # WardenOpenidBearer::Strategy, or one of its subclasses); therefore, we
30
+ # want to return `false` if issuers don't match.
31
+ untrusted_issuer == config.issuer
32
+ end
33
+
34
+ def authenticate!
35
+ if (c = claims)
36
+ success! user_of_claims(c)
37
+ else
38
+ # Given that `valid?` did return true previously,
39
+ # we know the status with precision:
40
+ fail! "Invalid OIDC bearer token"
41
+ end
42
+ rescue JWT::ExpiredSignature
43
+ fail! "Expired OIDC bearer token"
44
+ end
45
+
46
+ # Overridden to always return false, because we typically *don't*
47
+ # want persistent sessions for an OpenID-Connect resource server —
48
+ # Everything we need to know is in the JWT token.
49
+ def store?
50
+ false
51
+ end
52
+
53
+ # Made public so that one may tune the `strategy.config.cache_timeout`:
54
+ def config
55
+ return @config if @config
56
+ @config = WardenOpenidBearer::DiscoveredConfig.new(metadata_url)
57
+ @config.cache_timeout = cache_timeout
58
+ @config
59
+ end
60
+
61
+ protected
62
+
63
+ # Dummy implementation for applications that don't really care
64
+ # about `env['warden'].user` being an object (or at all). Override
65
+ # in a subclass if you do care.
66
+ def user_of_claims(claims)
67
+ claims
68
+ end
69
+
70
+ # Returns the URL of the OIDC metadata for the authentication server,
71
+ # which typically ends with `/.well-known/openid-configuration`
72
+ #
73
+ # The default implementation obeys the `.openid_metadata_url`
74
+ # setting, as set in a `WardenOpenidBearer.configure` block. Override
75
+ # in a subclass if you don't want all your OIDC claims to be
76
+ # checked against one and the same authentication server. (If you
77
+ # want to support two authentication servers, for instance, you
78
+ # should have two subclasses.)
79
+ def metadata_url
80
+ WardenOpenidBearer.config.openid_metadata_url
81
+ end
82
+
83
+ # Returns the cache timeout for the security data obtained from
84
+ # the authentication server.
85
+ #
86
+ # The default implementation uses a global configuration. Like
87
+ # `metadata_url`, you should override this in multiple subclasses
88
+ # if you want multiple OpenID authentication servers.
89
+ def cache_timeout
90
+ WardenOpenidBearer.config.cache_timeout
91
+ end
92
+
93
+ # Returns the JWT token from `request.headers['Authorization']`
94
+ # (which may or may not be valid)
95
+ def token
96
+ # We call this one quite a lot, so we want some caching. Also,
97
+ # it so happens that Warden only constructs a single instance of
98
+ # this class and re-uses it across requests (see
99
+ # `_fetch_strategy` in `lib/warden/proxy.rb`).
100
+ cached_by(request) do
101
+ puts request.headers
102
+ strategy, token = (request.headers["Authorization"] || "").split(" ")
103
+ token if (strategy || "").downcase == "bearer"
104
+ end
105
+ end
106
+
107
+ # Returns the JWT claims, only if the cryptographic signature and
108
+ # other security requirements (in particular, the expiration
109
+ # timestamp) check out.
110
+ def claims
111
+ JWT.decode(token, nil, true, jwt_decode_opts).first
112
+ end
113
+
114
+ def jwt_decode_opts
115
+ # Note: issuer check was already done in `valid?`, see
116
+ # explanations there; skip it here.
117
+ {
118
+ algorithm: algorithm,
119
+ verify_expiration: true,
120
+ verify_not_before: true,
121
+ verify_iat: true,
122
+ jwks: config.jwks
123
+ }
124
+ end
125
+
126
+ def algorithm
127
+ return untrusted_algorithm if
128
+ config.authorization_algs.member? untrusted_algorithm
129
+ end
130
+
131
+ def untrusted_fields
132
+ JWT.decode(token, nil, false)
133
+ end
134
+
135
+ def untrusted_algorithm
136
+ untrusted_fields.last["alg"]
137
+ end
138
+
139
+ def untrusted_issuer
140
+ untrusted_fields.first["iss"]
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WardenOpenidBearer
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/configurable"
4
+
5
+ require_relative "warden_openid_bearer/version"
6
+ require_relative "warden_openid_bearer/registerer"
7
+ require_relative "warden_openid_bearer/cache_mixin"
8
+ require_relative "warden_openid_bearer/discovered_config"
9
+ require_relative "warden_openid_bearer/strategy"
10
+
11
+ module WardenOpenidBearer
12
+ extend Dry::Configurable
13
+
14
+ setting :openid_metadata_url, constructor: ->(url) { URI(url) }
15
+ setting :cache_timeout, default: 900
16
+ end
@@ -0,0 +1,4 @@
1
+ module WardenOpenidBearer
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
@@ -0,0 +1,45 @@
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
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: warden_openid_bearer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dominique Quatravaux
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-10-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: warden
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: dry-configurable
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.15.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.15.0
41
+ description: |2+
42
+
43
+ This gem is like the `warden_openid_auth` gem, except that it only
44
+ provides support for the very last step of the OAuth code flow, i.e.
45
+ when the resource server / relying party (your Ruby Web app)
46
+ validates and decodes the JWT token.
47
+
48
+ Use this gem if your client-side Web (or mobile) app will be taking
49
+ care of the rest of the OAuth2 motions, such as redirecting (or
50
+ opening a popup window) to the authentication server at login time,
51
+ managing and refreshing tokens, doing all these unspeakable things
52
+ with iframes, etc.
53
+
54
+ email:
55
+ - dominique.quatravaux@epfl.ch
56
+ executables: []
57
+ extensions: []
58
+ extra_rdoc_files: []
59
+ files:
60
+ - ".rspec"
61
+ - ".standard.yml"
62
+ - CHANGELOG.md
63
+ - Gemfile
64
+ - LICENSE.txt
65
+ - README.md
66
+ - Rakefile
67
+ - lib/warden_openid_bearer.rb
68
+ - lib/warden_openid_bearer/cache_mixin.rb
69
+ - lib/warden_openid_bearer/discovered_config.rb
70
+ - lib/warden_openid_bearer/registerer.rb
71
+ - lib/warden_openid_bearer/strategy.rb
72
+ - lib/warden_openid_bearer/version.rb
73
+ - sig/warden_openid_bearer.rbs
74
+ - warden_openid_bearer.gemspec
75
+ homepage: https://github.com/epfl-si/warden_openid_bearer
76
+ licenses:
77
+ - MIT
78
+ metadata:
79
+ homepage_uri: https://github.com/epfl-si/warden_openid_bearer
80
+ source_code_uri: https://github.com/epfl-si/warden_openid_bearer
81
+ changelog_uri: https://github.com/epfl-si/warden_openid_bearer/blob/master/CHANGELOG.md
82
+ my_side_project_has_a_side_project: https://github.com/epfl-si/rails.starterkit
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: 2.6.0
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubygems_version: 3.3.11
99
+ signing_key:
100
+ specification_version: 4
101
+ summary: Warden strategy to validate OpenID-Connect bearer tokens
102
+ test_files: []
103
+ ...