omniauth_oidc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ead4b54097f07fbdf676bb450ce3ba1b4d477b47f07b61e30ecd445706ced854
4
+ data.tar.gz: fe6e02df8acd530cdd2c4d6ec649e7914248eb42ec47b3573d4775f48449b7dd
5
+ SHA512:
6
+ metadata.gz: 6c4e2b5d1aee856a8703bfb228e42e45d620d08e7dd8151ea39d484e6a1576f05433038fa357c933ca0a792e52d85b772e95d5660ff116ca0c0c41c04215785b
7
+ data.tar.gz: dc0dab4d599d6717589f0c160fee8755d28523a494c165bee1fba7b154a9ff873309e0d00b4d06653ac96bdbbd0e3b36efec92a258a8a0f0349dc545dac53321
data/.rubocop.yml ADDED
@@ -0,0 +1,28 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.7
3
+
4
+ Style/StringLiterals:
5
+ Enabled: true
6
+ EnforcedStyle: double_quotes
7
+
8
+ Style/StringLiteralsInInterpolation:
9
+ Enabled: true
10
+ EnforcedStyle: double_quotes
11
+
12
+ Layout/LineLength:
13
+ Max: 120
14
+
15
+ Metrics/ClassLength:
16
+ Max: 200
17
+
18
+ Metrics/MethodLength:
19
+ Max: 20
20
+
21
+ Metrics/AbcSize:
22
+ Max: 35
23
+
24
+ Metrics/Metrics/CyclomaticComplexity:
25
+ Max: 10
26
+
27
+ Metrics/PerceivedComplexity:
28
+ Max: 10
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ ## [Released]
2
+
3
+ ## [0.1.0] - 2024-06-13
4
+ - Initial release
@@ -0,0 +1,84 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or
22
+ advances of any kind
23
+ * Trolling, insulting or derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information, such as a physical or email
26
+ address, without their explicit permission
27
+ * Other conduct which could reasonably be considered inappropriate in a
28
+ professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at slmusayev@gmail.com. All complaints will be reviewed and investigated promptly and fairly.
43
+
44
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
+
46
+ ## Enforcement Guidelines
47
+
48
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
+
50
+ ### 1. Correction
51
+
52
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
+
54
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
+
56
+ ### 2. Warning
57
+
58
+ **Community Impact**: A violation through a single incident or series of actions.
59
+
60
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
+
62
+ ### 3. Temporary Ban
63
+
64
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
+
66
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
+
68
+ ### 4. Permanent Ban
69
+
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
+
72
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
73
+
74
+ ## Attribution
75
+
76
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
+ available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
+
81
+ [homepage]: https://www.contributor-covenant.org
82
+
83
+ For answers to common questions about this code of conduct, see the FAQ at
84
+ https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Suleyman Musayev
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,216 @@
1
+ # OmniAuth::Oidc
2
+
3
+ This gem provides an OmniAuth strategy for integrating OpenID Connect (OIDC) authentication into your Ruby on Rails application. It allows seamless login using various OIDC providers.
4
+
5
+ Developed with reference to [omniauth-openid-connect](https://github.com/jjbohn/omniauth-openid-connect) and [omniauth_openid_connect](https://github.dev/omniauth/omniauth_openid_connect).
6
+
7
+ ## Installation
8
+
9
+ To install the gem run the following command in the terminal:
10
+
11
+ $ bundle add omniauth_oidc
12
+
13
+ If bundler is not being used to manage dependencies, install the gem by executing:
14
+
15
+ $ gem install omniauth_oidc
16
+
17
+
18
+ ## Usage
19
+
20
+ To use the OmniAuth OIDC strategy, you need to configure your Rails application and set up the necessary environment variables for OIDC client credentials.
21
+
22
+ ### Configuration
23
+ You have to provide Client ID, Client Secret and url for the OIDC configuration endpoint as a bare minimum for the `omniauth_oidc` to work properly.
24
+ Create an initializer file at `config/initializers/omniauth.rb`
25
+
26
+ ```ruby
27
+ # config/initializers/omniauth.rb
28
+ Rails.application.config.middleware.use OmniAuth::Builder do
29
+ provider :oidc, {
30
+ name: :simple_provider, # used for dynamic routing
31
+ client_options: {
32
+ identifier: '23575f4602bebbd9a17dbc38d85bd1a77',
33
+ secret: ENV['SIMPLE_PROVIDER_CLIENT_SECRET'],
34
+ config_endpoint: 'https://simpleprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/.well-known/openid-configuration'
35
+ }
36
+ }
37
+ end
38
+ ```
39
+
40
+ With Devise
41
+
42
+ ```ruby
43
+ Devise.setup do |config|
44
+ config.omniauth :oidc, {
45
+ name: :simple_provider,
46
+ scope: [:openid, :email, :profile, :address],
47
+ response_type: :code,
48
+ uid_field: "preferred_username",
49
+ client_options: {
50
+ identifier: '23575f4602bebbd9a17dbc38d85bd1a77',
51
+ secret: ENV['SIMPLE_PROVIDER_CLIENT_SECRET'],
52
+ config_endpoint: 'https://simpleprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/.well-known/openid-configuration'
53
+ }
54
+ }
55
+ end
56
+ ```
57
+
58
+ The gem also supports a wide range of optional parameters for higher degree of configurability.
59
+
60
+ ```ruby
61
+ # config/initializers/omniauth.rb
62
+ Rails.application.config.middleware.use OmniAuth::Builder do
63
+ provider :oidc, {
64
+ name: :complex_provider, # used for dynamic routing
65
+ issuer: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77',
66
+ scope: [:openid],
67
+ response_type: 'id_token',
68
+ require_state: true,
69
+ response_mode: :query,
70
+ prompt: :login,
71
+ send_nonce: false,
72
+ uid_field: "sub",
73
+ pkce: false,
74
+ client_options: {
75
+ identifier: '23575f4602bebbd9a17dbc38d85bd1a77',
76
+ secret: ENV['COMPLEX_PROVIDER_CLIENT_SECRET'],
77
+ config_endpoint: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/.well-known/openid-configuration',
78
+ host: 'complexprovider.com'
79
+ scheme: "https",
80
+ port: 443,
81
+ authorization_endpoint: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/authorization',
82
+ token_endpoint: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/token',
83
+ userinfo_endpoint: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/userinfo',
84
+ jwks_uri: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/jwks',
85
+ end_session_endpoint: '/signout'
86
+ }
87
+ }
88
+ end
89
+ ```
90
+
91
+ Ensure to replace identifier, secret, configuration endpoint url and others with credentials received from your OIDC provider.
92
+
93
+ ### Redirecting for Authentication
94
+
95
+ Buttons and links to initialize the authentication request can be placed on relevant pages as below:
96
+
97
+ ```ruby
98
+ <%= button_to "Login with Simple Provider", "/auth/simple_provider" %>
99
+ ```
100
+
101
+ ### Handling Callbacks
102
+
103
+ The gem uses dyanmic routes to handle different phases, and while you can use same routes in your Rails application, for
104
+ better experience you should have a controller to process the authenticated user. Create a CallbacksController:
105
+
106
+ ```ruby
107
+ # app/controllers/callbacks_controller.rb
108
+ class CallbacksController < ApplicationController
109
+ def omniauth
110
+ # user info received from OIDC provider will be available in `request.env['omniauth.auth']`
111
+ auth = request.env['omniauth.auth']
112
+
113
+ user = User.find_or_create_by(uid: auth['uid']) do |user|
114
+ user.name = auth['info']['name']
115
+ user.email = auth['info']['email']
116
+ end
117
+
118
+ session[:user_id] = user.id
119
+ redirect_to root_path, notice: 'Successfully logged in!'
120
+ end
121
+ end
122
+ ```
123
+
124
+ ### Routes
125
+
126
+ The gem uses dynamic routes when making requests to the OIDC provider endpoints. These routes follow the naming pattern
127
+ of `https://your_app.com/auth/<simple_provider>/callback`, where `<simple_provider>` is the provider name defined
128
+ within the configuration of the `omniauth.rb` initializer.
129
+
130
+ Dynamic routes are used to process responses and perform intermediary steps by the middleware, e.g. request phase,
131
+ token verification. While you can define and use same routes within your Rails app, you can modify your `routes.rb`
132
+ to perform a dynamic redirect to a another controller method. In an example below, all OIDC responses are ultimately
133
+ redirected to the `omniauth` method of the `callbacks_controller`, which is a universal method to handle authentication
134
+ with various omniauth providers:
135
+
136
+ ```ruby
137
+ # config/routes.rb
138
+ Rails.application.routes.draw do
139
+ match 'auth/:provider/callback', via: :get, to: "callbacks#omniauth"
140
+ end
141
+ ```
142
+
143
+ Alternatively, you can specify separate redirects for some of your OIDC providers, in case you need to handle responses
144
+ differently:
145
+
146
+ ```ruby
147
+ # config/routes.rb
148
+ Rails.application.routes.draw do
149
+ match 'auth/simple_provider/callback', via: :get, to: "callbacks#simple_provider"
150
+ match 'auth/complex_provider/callback', via: :get, to: "callbacks#complex_provider"
151
+
152
+ # you can add the line below if you would like the rest of the providers to be redirected to a universal `omniauth` method
153
+ match 'auth/:provider/callback', via: :get, to: "callbacks#omniauth"
154
+ end
155
+ ```
156
+
157
+ **Please note that you should register `https://your_app.com/auth/<simple_provider>/callback` with your OIDC provider
158
+ as a callback redirect url.**
159
+
160
+
161
+ ### Advanced Configuration
162
+ You can customize the OIDC strategy further by adding additional configuration options:
163
+
164
+ | Field | Description | Required | Default Value | Example/Notes |
165
+ |------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------|-------------------------------------|-------------------------------------------------------|
166
+ | name | Arbitrary string to identify OIDC provider and segregate it from other OIDC providers | no | `"oidc"` | `:simple_provider` |
167
+ | issuer | Root url for the OIDC authorization server | no | retrived from config_endpoint | `"https://simpleprovider.com"` |
168
+ | client_auth_method | Authentication method to be used with the OIDC authorization server | no | `:basic` | `"basic"`, `"jwks"` |
169
+ | scope | OIDC scopes to be included in the server's response | `[:openid]` is required | all scopes offered by OIDC provider | `[:openid, :profile, :email]` |
170
+ | response_type | OAuth2 response type expected from OIDC provider during authorization | no | `"code"` | `"code"` or `"id_token"` |
171
+ | state | Value to be used for the OAuth2 state parameter on the authorization request. Can be a proc that generates a string | no | Random 16 character string | `Proc.new { SecureRandom.hex(32) }` |
172
+ | require_state | Boolean to indicate if state param should be verified. This is a recommendation by OIDC spec | no | `true` | `true` or `false` |
173
+ | response_mode | The response mode per [OIDC spec](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html) | no | `nil` | `:query`, `:fragment`, `:form_post` or `:web_message` |
174
+ | display | Specifies how OIDC authorization server should display the authentication and consent UI pages to the end user | no | `nil` | `:page`, `:popup`, `:touch` or `:wap` |
175
+ | prompt | Specifies whether the OIDC authorization server prompts the end user for reauthentication and consent | no | `nil` | `:none`, `:login`, `:consent` or `:select_account` |
176
+ | send_scope_to_token_endpoint | Should the scope parameter be sent to the authorization token endpoint | no | `true` | `true` or `false` |
177
+ | post_logout_redirect_uri | Logout redirect uri to use per the [session management draft](https://openid.net/specs/openid-connect-session-1_0.html) | no | `nil` | `"https://your_app.com/logout/callback"` |
178
+ | uid_field | Field of the user info response to be used as a unique ID | no | `'sub'` | `"sub"` or `"preferred_username"` |
179
+ | extra_authorize_params | Hash of extra fixed parameters that will be merged to the authorization request | no | `{}` | `{"tenant" => "common"}` |
180
+ | allow_authorize_params | List of allowed dynamic parameters that will be merged to the authorization request | no | `[]` | `[:screen_name]` |
181
+ | pkce | Enable [PKCE flow](https://oauth.net/2/pkce/) | no | `false` | `true` or `false` |
182
+ | pkce_verifier | Specify custom PKCE verifier code | no | Random 128-character string | `Proc.new { SecureRandom.hex(64) }` |
183
+ | pkce_options | Specify custom implementation of the PKCE code challenge/method | no | SHA256(code_challenge) in hex | Proc to customise the code challenge generation |
184
+ | client_options | Hash of client options detailed below in a separate table | yes | see below | see below |
185
+ | jwt_secret_base64 | Specify the base64-encoded secret used to sign the JWT token for HMAC with SHA2 (e.g. HS256) signing algorithms | no | `client_options.secret` | `"bXlzZWNyZXQ=\n"` |
186
+ | logout_path | Log out is only triggered when the request path ends on this path | no | `'/logout'` | '/sign_out' |
187
+ | acr_values | Authentication Class Reference (ACR) values to be passed to the authorize_uri to enforce a specific level, see [RFC9470](https://www.rfc-editor.org/rfc/rfc9470.html) | no | `nil` | `"c1 c2"` å|
188
+
189
+
190
+ Below are options for the `client_options` hash of the configuration:
191
+
192
+ | Field | Description | Required | Default value |
193
+ |------------------------|-------------------------------------------------------------|----------|-------------------------------|
194
+ | identifier | OAuth2 client_id | yes | `nil` |
195
+ | secret | OAuth2 client secret | yes | `nil` |
196
+ | config_endpoint | OIDC configuration endpoint | yes | `nil` |
197
+ | scheme | http scheme to use | no | https |
198
+ | host | host of the authorization server | no | nil |
199
+ | port | port for the authorization server | no | 443 |
200
+ | authorization_endpoint | authorize endpoint on the authorization server | no | retrived from config_endpoint |
201
+ | token_endpoint | token endpoint on the authorization server | no | retrived from config_endpoint |
202
+ | userinfo_endpoint | user info endpoint on the authorization server | no | retrived from config_endpoint |
203
+ | jwks_uri | jwks_uri on the authorization server | no | retrived from config_endpoint |
204
+ | end_session_endpoint | url to call to log the user out at the authorization server | no | `nil` |
205
+
206
+ ## Contributing
207
+
208
+ Bug reports and pull requests are welcome on GitHub at https://github.com/msuliq/omniauth_oidc. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/msuliq/omniauth_oidc/blob/main/CODE_OF_CONDUCT.md).
209
+
210
+ ## License
211
+
212
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
213
+
214
+ ## Code of Conduct
215
+
216
+ Everyone interacting in the OmniauthOidc project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/msuliq/omniauth_oidc/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniauthOidc
4
+ class Error < RuntimeError; end
5
+
6
+ class MissingCodeError < Error; end
7
+
8
+ class MissingIdTokenError < Error; end
9
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniauthOidc
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAuth
4
+ module Strategies
5
+ class Oidc
6
+ # Callback phase
7
+ module Callback
8
+ def callback_phase # rubocop:disable Metrics
9
+ error = params["error_reason"] || params["error"]
10
+ error_description = params["error_description"] || params["error_reason"]
11
+ invalid_state = (options.require_state && params["state"].to_s.empty?) || params["state"] != stored_state
12
+
13
+ raise CallbackError, error: params["error"], reason: error_description, uri: params["error_uri"] if error
14
+ raise CallbackError, error: :csrf_detected, reason: "Invalid 'state' parameter" if invalid_state
15
+
16
+ return unless valid_response_type?
17
+
18
+ options.issuer = issuer if options.issuer.nil? || options.issuer.empty?
19
+
20
+ verify_id_token!(params["id_token"]) if configured_response_type == "id_token"
21
+
22
+ client.redirect_uri = redirect_uri
23
+
24
+ return id_token_callback_phase if configured_response_type == "id_token"
25
+
26
+ client.authorization_code = authorization_code
27
+
28
+ access_token
29
+ super
30
+ rescue CallbackError => e
31
+ fail!(e.error, e)
32
+ rescue ::Rack::OAuth2::Client::Error => e
33
+ fail!(e.response[:error], e)
34
+ rescue ::Timeout::Error, ::Errno::ETIMEDOUT => e
35
+ fail!(:timeout, e)
36
+ rescue ::SocketError => e
37
+ fail!(:failed_to_connect, e)
38
+ end
39
+
40
+ private
41
+
42
+ def access_token
43
+ return @access_token if @access_token
44
+
45
+ token_request_params = {
46
+ scope: (scope if options.send_scope_to_token_endpoint),
47
+ client_auth_method: options.client_auth_method
48
+ }
49
+
50
+ if options.pkce
51
+ token_request_params[:code_verifier] =
52
+ params["code_verifier"] || session.delete("omniauth.pkce.verifier")
53
+ end
54
+
55
+ set_client_options_for_callback_phase
56
+
57
+ @access_token = client.access_token!(token_request_params)
58
+
59
+ verify_id_token!(@access_token.id_token) if configured_response_type == "code"
60
+
61
+ user_info_from_access_token
62
+ end
63
+
64
+ def id_token_callback_phase
65
+ user_data = decode_id_token(params["id_token"]).raw_attributes
66
+
67
+ define_user_info(user_data)
68
+ end
69
+
70
+ def valid_response_type?
71
+ return true if params.key?(configured_response_type)
72
+
73
+ error_attrs = RESPONSE_TYPE_EXCEPTIONS[configured_response_type]
74
+ fail!(error_attrs[:key], error_attrs[:exception_class].new(params["error"]))
75
+
76
+ false
77
+ end
78
+
79
+ def user_info_from_access_token
80
+ user_data = HTTParty.get(
81
+ config.userinfo_endpoint, {
82
+ headers: {
83
+ "Authorization" => "Bearer #{@access_token}",
84
+ "Content-Type" => "application/json"
85
+ }
86
+ }
87
+ )
88
+
89
+ define_user_info(user_data.parsed_response)
90
+ end
91
+
92
+ def define_user_info(user_data)
93
+ env["omniauth.auth"] = AuthHash.new(
94
+ provider: name,
95
+ uid: user_data["sub"],
96
+ info: { name: user_data["name"], email: user_data["email"] },
97
+ extra: { raw_info: user_data },
98
+ credentials: {
99
+ id_token: @access_token.id_token,
100
+ token: @access_token.access_token,
101
+ refresh_token: @access_token.refresh_token,
102
+ expires_in: @access_token.expires_in,
103
+ scope: @access_token.scope
104
+ }
105
+ )
106
+ call_app!
107
+ end
108
+
109
+ def configured_response_type
110
+ @configured_response_type ||= options.response_type.to_s
111
+ end
112
+
113
+ # Parse response from OIDC endpoint and set client options for callback phase
114
+ def set_client_options_for_callback_phase
115
+ client.host = host
116
+ client.redirect_uri = redirect_uri
117
+ client.authorization_endpoint = resolve_endpoint_from_host(host, config.authorization_endpoint)
118
+ client.token_endpoint = resolve_endpoint_from_host(host, config.token_endpoint)
119
+ client.userinfo_endpoint = resolve_endpoint_from_host(host, config.userinfo_endpoint)
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAuth
4
+ module Strategies
5
+ class Oidc
6
+ # Code request phase
7
+ module Request
8
+ def request_phase
9
+ @identifier = client_options.identifier
10
+ @secret = secret
11
+
12
+ set_client_options_for_request_phase
13
+ redirect authorize_uri
14
+ end
15
+
16
+ def authorize_uri # rubocop:disable Metrics/AbcSize
17
+ client.redirect_uri = redirect_uri
18
+ opts = request_options
19
+
20
+ opts.merge!(options.extra_authorize_params) unless options.extra_authorize_params.empty?
21
+
22
+ options.allow_authorize_params.each do |key|
23
+ opts[key] = request.params[key.to_s] unless opts.key?(key)
24
+ end
25
+
26
+ if options.pkce
27
+ verifier = options.pkce_verifier ? options.pkce_verifier.call : SecureRandom.hex(64)
28
+
29
+ opts.merge!(pkce_authorize_params(verifier))
30
+ session["omniauth.pkce.verifier"] = verifier
31
+ end
32
+
33
+ client.authorization_uri(opts.reject { |_k, v| v.nil? })
34
+ end
35
+
36
+ private
37
+
38
+ def request_options
39
+ {
40
+ response_type: options.response_type,
41
+ response_mode: options.response_mode,
42
+ scope: scope,
43
+ state: new_state,
44
+ login_hint: params["login_hint"],
45
+ ui_locales: params["ui_locales"],
46
+ claims_locales: params["claims_locales"],
47
+ prompt: options.prompt,
48
+ nonce: (new_nonce if options.send_nonce),
49
+ hd: options.hd,
50
+ acr_values: options.acr_values
51
+ }
52
+ end
53
+
54
+ def new_state
55
+ state = if options.state.respond_to?(:call)
56
+ if options.state.arity == 1
57
+ options.state.call(env)
58
+ else
59
+ options.state.call
60
+ end
61
+ end
62
+ session["omniauth.state"] = state || SecureRandom.hex(16)
63
+ end
64
+
65
+ # Parse response from OIDC endpoint and set client options for request phase
66
+ def set_client_options_for_request_phase # rubocop:disable Metrics/AbcSize
67
+ client_options.host = host
68
+ client_options.authorization_endpoint = resolve_endpoint_from_host(host, config.authorization_endpoint)
69
+ client_options.token_endpoint = resolve_endpoint_from_host(host, config.token_endpoint)
70
+ client_options.userinfo_endpoint = resolve_endpoint_from_host(host, config.userinfo_endpoint)
71
+ client_options.jwks_uri = resolve_endpoint_from_host(host, config.jwks_uri)
72
+
73
+ return unless config.respond_to?(:end_session_endpoint)
74
+
75
+ client_options.end_session_endpoint = resolve_endpoint_from_host(host,
76
+ config.end_session_endpoint)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAuth
4
+ module Strategies
5
+ class Oidc
6
+ # Token verification phase
7
+ module Verify # rubocop:disable Metrics/ModuleLength
8
+ def secret
9
+ base64_decoded_jwt_secret || client_options.secret
10
+ end
11
+
12
+ # https://tools.ietf.org/html/rfc7636#appendix-A
13
+ def pkce_authorize_params(verifier)
14
+ {
15
+ code_challenge: options.pkce_options[:code_challenge].call(verifier),
16
+ code_challenge_method: options.pkce_options[:code_challenge_method]
17
+ }
18
+ end
19
+
20
+ # Looks for key defined in omniauth initializer, if none is defined
21
+ # falls back to using jwks_uri returned by OIDC config_endpoint
22
+ def public_key
23
+ @public_key ||= if configured_public_key
24
+ configured_public_key
25
+ elsif config.jwks_uri
26
+ fetch_key
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def fetch_key
33
+ @fetch_key ||= parse_jwk_key(::OpenIDConnect.http_client.get(config.jwks_uri).body)
34
+ end
35
+
36
+ def base64_decoded_jwt_secret
37
+ return unless options.jwt_secret_base64
38
+
39
+ Base64.decode64(options.jwt_secret_base64)
40
+ end
41
+
42
+ def verify_id_token!(id_token)
43
+ return unless id_token
44
+
45
+ decode_id_token(id_token).verify!(issuer: config.issuer,
46
+ client_id: client_options.identifier,
47
+ nonce: params["nonce"].presence || stored_nonce)
48
+ end
49
+
50
+ # Workaround for https://github.com/nov/openid_connect/issues/61
51
+ def decode_id_token(id_token)
52
+ decoded = JSON::JWT.decode(id_token, :skip_verification)
53
+ algorithm = decoded.algorithm.to_sym
54
+
55
+ validate_client_algorithm!(algorithm)
56
+
57
+ keyset =
58
+ case algorithm
59
+ when :HS256, :HS384, :HS512
60
+ secret
61
+ else
62
+ public_key
63
+ end
64
+
65
+ decoded.verify!(keyset)
66
+ ::OpenIDConnect::ResponseObject::IdToken.new(decoded)
67
+ rescue JSON::JWK::Set::KidNotFound
68
+ # Workaround for https://github.com/nov/json-jwt/pull/92#issuecomment-824654949
69
+ raise if decoded&.header&.key?("kid")
70
+
71
+ decoded = decode_with_each_key!(id_token, keyset)
72
+
73
+ raise unless decoded
74
+
75
+ decoded
76
+ end
77
+
78
+ # Check for jwt to match defined client_signing_alg
79
+ def validate_client_algorithm!(algorithm)
80
+ client_signing_alg = options.client_signing_alg&.to_sym
81
+
82
+ return unless client_signing_alg
83
+ return if algorithm == client_signing_alg
84
+
85
+ reason = "Received JWT is signed with #{algorithm}, but client_singing_alg is \
86
+ configured for #{client_signing_alg}"
87
+ raise CallbackError, error: :invalid_jwt_algorithm, reason: reason, uri: params["error_uri"]
88
+ end
89
+
90
+ def decode!(id_token, key)
91
+ ::OpenIDConnect::ResponseObject::IdToken.decode(id_token, key)
92
+ end
93
+
94
+ def decode_with_each_key!(id_token, keyset)
95
+ return unless keyset.is_a?(JSON::JWK::Set)
96
+
97
+ keyset.each do |key|
98
+ begin
99
+ decoded = decode!(id_token, key)
100
+ rescue JSON::JWS::VerificationFailed, JSON::JWS::UnexpectedAlgorithm, JSON::JWK::UnknownAlgorithm
101
+ next
102
+ end
103
+
104
+ return decoded if decoded
105
+ end
106
+
107
+ nil
108
+ end
109
+
110
+ def stored_nonce
111
+ session.delete("omniauth.nonce")
112
+ end
113
+
114
+ def configured_public_key
115
+ @configured_public_key ||= if options.client_jwk_signing_key
116
+ parse_jwk_key(options.client_jwk_signing_key)
117
+ elsif options.client_x509_signing_key
118
+ parse_x509_key(options.client_x509_signing_key)
119
+ end
120
+ end
121
+
122
+ def parse_x509_key(key)
123
+ OpenSSL::X509::Certificate.new(key).public_key
124
+ end
125
+
126
+ def parse_jwk_key(key)
127
+ json = key.is_a?(String) ? JSON.parse(key) : key
128
+ return JSON::JWK::Set.new(json["keys"]) if json.key?("keys")
129
+
130
+ JSON::JWK.new(json)
131
+ end
132
+
133
+ def decode(str)
134
+ UrlSafeBase64.decode64(str).unpack1("B*").to_i(2).to_s
135
+ end
136
+
137
+ def user_info
138
+ return @user_info if @user_info
139
+
140
+ if access_token.id_token
141
+ decoded = decode_id_token(access_token.id_token).raw_attributes
142
+
143
+ @user_info = ::OpenIDConnect::ResponseObject::UserInfo.new(
144
+ access_token.userinfo!.raw_attributes.merge(decoded)
145
+ )
146
+ else
147
+ @user_info = access_token.userinfo!
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "timeout"
5
+ require "net/http"
6
+ require "open-uri"
7
+ require "omniauth"
8
+ require "openid_connect"
9
+ require "openid_config_parser"
10
+ require "forwardable"
11
+ require "httparty"
12
+
13
+ Dir[File.join(File.dirname(__FILE__), "oidc", "*.rb")].sort.each { |file| require_relative file }
14
+
15
+ module OmniAuth
16
+ module Strategies
17
+ # OIDC strategy for omniauth
18
+ class Oidc
19
+ include OmniAuth::Strategy
20
+ include Request
21
+ include Callback
22
+ include Verify
23
+
24
+ extend Forwardable
25
+
26
+ RESPONSE_TYPE_EXCEPTIONS = {
27
+ "id_token" => { exception_class: OmniauthOidc::MissingIdTokenError, key: :missing_id_token }.freeze,
28
+ "code" => { exception_class: OmniauthOidc::MissingCodeError, key: :missing_code }.freeze
29
+ }.freeze
30
+
31
+ def_delegator :request, :params
32
+
33
+ option :name, "oidc" # to separate each oidc provider available in the app
34
+ option(:client_options, identifier: nil, # client id, required
35
+ secret: nil, # client secret, required
36
+ host: nil, # oidc provider host, optional
37
+ scheme: "https", # connection scheme, optional
38
+ port: 443, # connection port, optional
39
+ config_endpoint: nil, # all data will be fetched from here, required
40
+ authorization_endpoint: nil, # optional
41
+ token_endpoint: nil, # optional
42
+ userinfo_endpoint: nil, # optional
43
+ jwks_uri: nil, # optional
44
+ end_session_endpoint: nil) # optional
45
+
46
+ option :issuer
47
+ option :client_signing_alg
48
+ option :jwt_secret_base64
49
+ option :client_jwk_signing_key
50
+ option :client_x509_signing_key
51
+ option :scope, [:openid]
52
+ option :response_type, "code" # ['code', 'id_token']
53
+ option :require_state, true
54
+ option :state
55
+ option :response_mode # [:query, :fragment, :form_post, :web_message]
56
+ option :display, nil # [:page, :popup, :touch, :wap]
57
+ option :prompt, nil # [:none, :login, :consent, :select_account]
58
+ option :hd, nil
59
+ option :max_age
60
+ option :ui_locales
61
+ option :id_token_hint
62
+ option :acr_values
63
+ option :send_nonce, true
64
+ option :send_scope_to_token_endpoint, true
65
+ option :client_auth_method
66
+ option :post_logout_redirect_uri
67
+ option :extra_authorize_params, {}
68
+ option :allow_authorize_params, []
69
+ option :uid_field, "sub"
70
+ option :pkce, false
71
+ option :pkce_verifier, nil
72
+ option :pkce_options, {
73
+ code_challenge: proc { |verifier|
74
+ Base64.urlsafe_encode64(Digest::SHA2.digest(verifier), padding: false)
75
+ },
76
+ code_challenge_method: "S256"
77
+ }
78
+
79
+ option :logout_path, "/logout"
80
+
81
+ def uid
82
+ user_info.raw_attributes[options.uid_field.to_sym] || user_info.sub
83
+ end
84
+
85
+ info do
86
+ {
87
+ name: user_info.name,
88
+ email: user_info.email,
89
+ email_verified: user_info.email_verified,
90
+ nickname: user_info.preferred_username,
91
+ first_name: user_info.given_name,
92
+ last_name: user_info.family_name,
93
+ gender: user_info.gender,
94
+ image: user_info.picture,
95
+ phone: user_info.phone_number,
96
+ urls: { website: user_info.website }
97
+ }
98
+ end
99
+
100
+ extra do
101
+ { raw_info: user_info.raw_attributes }
102
+ end
103
+
104
+ credentials do
105
+ {
106
+ id_token: access_token.id_token,
107
+ token: access_token.access_token,
108
+ refresh_token: access_token.refresh_token,
109
+ expires_in: access_token.expires_in,
110
+ scope: access_token.scope
111
+ }
112
+ end
113
+
114
+ # Initialize OpenIDConnect Client with options
115
+ def client
116
+ @client ||= ::OpenIDConnect::Client.new(client_options)
117
+ end
118
+
119
+ # Config is build from the json response from the OIDC config endpoint
120
+ def config
121
+ unless client_options.config_endpoint || params["config_endpoint"]
122
+ raise Error,
123
+ "Configuration endpoint is missing from options"
124
+ end
125
+
126
+ @config ||= OpenidConfigParser.fetch_openid_configuration(client_options.config_endpoint)
127
+ end
128
+
129
+ def other_phase
130
+ if logout_path_pattern.match?(current_path)
131
+ options.issuer = issuer if options.issuer.to_s.empty?
132
+
133
+ return redirect(end_session_uri) if end_session_uri
134
+ end
135
+ call_app!
136
+ end
137
+
138
+ def end_session_uri
139
+ return unless end_session_endpoint_is_valid?
140
+
141
+ end_session_uri = URI(client_options.end_session_endpoint)
142
+ end_session_uri.query = encoded_post_logout_redirect_uri
143
+ end_session_uri.to_s
144
+ end
145
+
146
+ private
147
+
148
+ def issuer
149
+ @issuer ||= config.issuer
150
+ end
151
+
152
+ def host
153
+ @host ||= URI.parse(config.issuer).host
154
+ end
155
+
156
+ # By default Returns all scopes supported by the OIDC provider
157
+ def scope
158
+ config.scopes_supported || options.scope
159
+ end
160
+
161
+ def authorization_code
162
+ params["code"]
163
+ end
164
+
165
+ def client_options
166
+ options.client_options
167
+ end
168
+
169
+ def stored_state
170
+ session.delete("omniauth.state")
171
+ end
172
+
173
+ def new_nonce
174
+ session["omniauth.nonce"] = SecureRandom.hex(16)
175
+ end
176
+
177
+ def script_name
178
+ return "" if @env.nil?
179
+
180
+ super
181
+ end
182
+
183
+ def session
184
+ return {} if @env.nil?
185
+
186
+ super
187
+ end
188
+
189
+ def redirect_uri
190
+ "#{request.base_url}/auth/#{name}/callback"
191
+ end
192
+
193
+ def encoded_post_logout_redirect_uri
194
+ return unless options.post_logout_redirect_uri
195
+
196
+ URI.encode_www_form(
197
+ post_logout_redirect_uri: options.post_logout_redirect_uri
198
+ )
199
+ end
200
+
201
+ def end_session_endpoint_is_valid?
202
+ client_options.end_session_endpoint &&
203
+ client_options.end_session_endpoint =~ URI::DEFAULT_PARSER.make_regexp
204
+ end
205
+
206
+ def logout_path_pattern
207
+ @logout_path_pattern ||= /\A#{Regexp.quote(request_path)}#{options.logout_path}/
208
+ end
209
+
210
+ # Strips port and host from strings with OIDC endpoints
211
+ def resolve_endpoint_from_host(host, endpoint)
212
+ start_index = endpoint.index(host) + host.length
213
+ endpoint = endpoint[start_index..]
214
+ endpoint = "/#{endpoint}" unless endpoint.start_with?("/")
215
+ endpoint
216
+ end
217
+
218
+ # Override for the CallbackError class
219
+ class CallbackError < StandardError
220
+ attr_accessor :error, :error_reason, :error_uri
221
+
222
+ def initialize(data)
223
+ super
224
+ self.error = data[:error]
225
+ self.error_reason = data[:reason]
226
+ self.error_uri = data[:uri]
227
+ end
228
+
229
+ def message
230
+ [error, error_reason, error_uri].compact.join(" | ")
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
236
+
237
+ OmniAuth.config.add_camelization "OmniauthOidc", "OmniAuthOidc"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "omniauth/oidc/version"
4
+ require_relative "omniauth/oidc/errors"
5
+ require_relative "omniauth/strategies/oidc"
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/omniauth/oidc/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "omniauth_oidc"
7
+ spec.version = OmniauthOidc::VERSION
8
+ spec.authors = ["Suleyman Musayev"]
9
+ spec.email = ["slmusayev@gmail.com"]
10
+
11
+ spec.summary = "Omniauth strategy to authenticate and retrieve user data using OpenID Connect (OIDC)"
12
+ spec.description = "Omniauth strategy to authenticate and retrieve user data as a client using OpenID Connect (OIDC)
13
+ suited for multiple OIDC providers."
14
+ spec.homepage = "https://github.com/msuliq/omniauth_oidc"
15
+ spec.license = "MIT"
16
+ spec.required_ruby_version = ">= 2.7.0"
17
+
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = "https://github.com/msuliq/omniauth_oidc"
20
+ spec.metadata["changelog_uri"] = "https://github.com/msuliq/omniauth_oidc/blob/main/CHANGELOG.md"
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(__dir__) do
25
+ `git ls-files -z`.split("\x0").reject do |f|
26
+ (File.expand_path(f) == __FILE__) ||
27
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
28
+ end
29
+ end
30
+ spec.bindir = "exe"
31
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ["lib"]
33
+
34
+ # Uncomment to register a new dependency of your gem
35
+ spec.add_dependency "httparty"
36
+ spec.add_dependency "omniauth"
37
+ spec.add_dependency "openid_config_parser"
38
+ spec.add_dependency "openid_connect"
39
+
40
+ # For more information and examples about making a new gem, check out our
41
+ # guide at: https://bundler.io/guides/creating_gem.html
42
+ end
@@ -0,0 +1,4 @@
1
+ module OmniauthOidc
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,120 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omniauth_oidc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Suleyman Musayev
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-06-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: httparty
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: omniauth
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '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'
41
+ - !ruby/object:Gem::Dependency
42
+ name: openid_config_parser
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: openid_connect
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: |-
70
+ Omniauth strategy to authenticate and retrieve user data as a client using OpenID Connect (OIDC)
71
+ suited for multiple OIDC providers.
72
+ email:
73
+ - slmusayev@gmail.com
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - ".rubocop.yml"
79
+ - CHANGELOG.md
80
+ - CODE_OF_CONDUCT.md
81
+ - LICENSE.txt
82
+ - README.md
83
+ - Rakefile
84
+ - lib/omniauth/oidc/errors.rb
85
+ - lib/omniauth/oidc/version.rb
86
+ - lib/omniauth/strategies/oidc.rb
87
+ - lib/omniauth/strategies/oidc/callback.rb
88
+ - lib/omniauth/strategies/oidc/request.rb
89
+ - lib/omniauth/strategies/oidc/verify.rb
90
+ - lib/omniauth_oidc.rb
91
+ - omniauth_oidc.gemspec
92
+ - sig/omniauth_oidc.rbs
93
+ homepage: https://github.com/msuliq/omniauth_oidc
94
+ licenses:
95
+ - MIT
96
+ metadata:
97
+ homepage_uri: https://github.com/msuliq/omniauth_oidc
98
+ source_code_uri: https://github.com/msuliq/omniauth_oidc
99
+ changelog_uri: https://github.com/msuliq/omniauth_oidc/blob/main/CHANGELOG.md
100
+ post_install_message:
101
+ rdoc_options: []
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: 2.7.0
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ requirements: []
115
+ rubygems_version: 3.1.6
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: Omniauth strategy to authenticate and retrieve user data using OpenID Connect
119
+ (OIDC)
120
+ test_files: []