omniauth_oidc 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rubocop.yml +28 -0
- data/CHANGELOG.md +4 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +216 -0
- data/Rakefile +12 -0
- data/lib/omniauth/oidc/errors.rb +9 -0
- data/lib/omniauth/oidc/version.rb +5 -0
- data/lib/omniauth/strategies/oidc/callback.rb +124 -0
- data/lib/omniauth/strategies/oidc/request.rb +81 -0
- data/lib/omniauth/strategies/oidc/verify.rb +153 -0
- data/lib/omniauth/strategies/oidc.rb +237 -0
- data/lib/omniauth_oidc.rb +5 -0
- data/omniauth_oidc.gemspec +42 -0
- data/sig/omniauth_oidc.rbs +4 -0
- metadata +120 -0
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
data/CODE_OF_CONDUCT.md
ADDED
@@ -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,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,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
|
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: []
|