omniauth_oidc 0.2.7 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -1
- data/CHANGELOG.md +70 -0
- data/LICENSE.txt +1 -1
- data/README.md +125 -63
- data/lib/omniauth/oidc/client.rb +86 -0
- data/lib/omniauth/oidc/config_fetcher.rb +76 -0
- data/lib/omniauth/oidc/errors.rb +20 -1
- data/lib/omniauth/oidc/http_client.rb +81 -0
- data/lib/omniauth/oidc/jwk_handler.rb +48 -0
- data/lib/omniauth/oidc/jwks_cache.rb +89 -0
- data/lib/omniauth/oidc/logging.rb +76 -0
- data/lib/omniauth/oidc/response_objects.rb +176 -0
- data/lib/omniauth/oidc/version.rb +1 -1
- data/lib/omniauth/strategies/oidc/callback.rb +119 -82
- data/lib/omniauth/strategies/oidc/request.rb +20 -16
- data/lib/omniauth/strategies/oidc/verify.rb +118 -68
- data/lib/omniauth/strategies/oidc.rb +70 -31
- data/lib/omniauth_oidc.rb +7 -0
- data/omniauth_oidc.gemspec +7 -5
- data/sig/omniauth_oidc.rbs +192 -1
- metadata +19 -39
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 235b9ddce05f36feb1cff322acfd22e06969de1ae49673b607fbb6779b85d448
|
|
4
|
+
data.tar.gz: 4cfec340b6c5cc585091829b3e4b6b8bd3db913773687481a05fc52fc3e27ea0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ebae17e00270c2aeefe3937993de033f7241d1f98cfbc630e9acd2163fbbe5011808da0189cef14fa20d9f94833b5ef014fbfc99652f6e16448048c25ee7e6fe
|
|
7
|
+
data.tar.gz: 6d908f0af3cd3e341f3ef3fdfa8c2f6c36fb15f4c1d94a17cb72d1ea1c28eb6f92b368696c32daf11dd89a72a78e76f409c28be3668ba42ab262677bacd9c14d
|
data/.rubocop.yml
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
AllCops:
|
|
2
2
|
TargetRubyVersion: 2.7
|
|
3
|
+
NewCops: enable
|
|
3
4
|
|
|
4
5
|
Style/StringLiterals:
|
|
5
6
|
Enabled: true
|
|
@@ -21,7 +22,7 @@ Metrics/MethodLength:
|
|
|
21
22
|
Metrics/AbcSize:
|
|
22
23
|
Max: 35
|
|
23
24
|
|
|
24
|
-
Metrics/
|
|
25
|
+
Metrics/CyclomaticComplexity:
|
|
25
26
|
Max: 10
|
|
26
27
|
|
|
27
28
|
Metrics/PerceivedComplexity:
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,75 @@
|
|
|
1
1
|
## [Released]
|
|
2
2
|
|
|
3
|
+
## [1.0.0] - 2026-03-02
|
|
4
|
+
|
|
5
|
+
### BREAKING CHANGES
|
|
6
|
+
- **Dependency Replacement**: Removed `openid_connect`, `openid_config_parser`, `json-jwt`, and `httparty` runtime dependencies. Replaced with custom `Net::HTTP`-based implementation and the standard `jwt` gem. Any code that relied on these gems being transitively available will need to add them directly.
|
|
7
|
+
- **Internal Object Types Changed**: Response objects are now custom classes instead of OpenIDConnect library types:
|
|
8
|
+
- `OpenIDConnect::Client` replaced by `OmniauthOidc::Client`
|
|
9
|
+
- `OpenIDConnect::ResponseObject::IdToken` replaced by `OmniauthOidc::ResponseObjects::IdToken`
|
|
10
|
+
- `OpenIDConnect::ResponseObject::UserInfo` replaced by `OmniauthOidc::ResponseObjects::UserInfo`
|
|
11
|
+
- `Rack::OAuth2::AccessToken` replaced by `OmniauthOidc::ResponseObjects::AccessToken`
|
|
12
|
+
- **Session Keys Namespaced**: Session keys now include the provider name (e.g. `omniauth.my_provider.state` instead of `omniauth.state`). This enables multiple OIDC providers but breaks code that manually accesses session keys with the old format. Users mid-authentication during upgrade will see state mismatch errors.
|
|
13
|
+
- **Scope Return Type**: `scope` now returns a space-delimited String (e.g. `"openid profile email"`) instead of an Array. Code that called `.each`, `.include?`, or other Array methods on the return value will break.
|
|
14
|
+
- **AuthHash Construction**: Now uses OmniAuth DSL blocks (`info`, `credentials`, `extra`) consistently instead of manually building `env["omniauth.auth"]`. The `info` hash now includes all standard OIDC UserInfo fields (given_name, family_name, gender, picture, phone, website).
|
|
15
|
+
- **Error Classes Restructured**: Error handling uses new specific error classes under `OmniauthOidc::` namespace. `Rack::OAuth2::Client::Error` is no longer caught. `validate_client_algorithm!` now raises `OmniauthOidc::InvalidAlgorithmError` instead of `CallbackError`.
|
|
16
|
+
- **CallbackError Parameter Names**: `CallbackError` now accepts `error_reason:` and `error_uri:` keyword arguments (previously `reason:` and `uri:`). The old keys are still accepted for backward compatibility.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **Custom HTTP Client** (`OmniauthOidc::HttpClient`): Net::HTTP-based client for all HTTP requests, removing external HTTP client dependencies entirely
|
|
20
|
+
- GET requests follow up to 5 redirects (301, 302, 307, 308), including relative redirects
|
|
21
|
+
- POST requests reject redirects to prevent credential leakage
|
|
22
|
+
- **Custom OIDC Client** (`OmniauthOidc::Client`): Handles authorization URI construction, token exchange, and userinfo fetching
|
|
23
|
+
- **Response Objects** (`OmniauthOidc::ResponseObjects`): `IdToken`, `UserInfo`, and `AccessToken` classes with method-style access to standard OIDC claims
|
|
24
|
+
- **Configuration Fetcher** (`OmniauthOidc::ConfigFetcher`): Fetches and parses `.well-known/openid-configuration` with retry support
|
|
25
|
+
- **JWK Handler** (`OmniauthOidc::JwkHandler`): JWKS parsing with proper `kid`-based key selection for providers that publish multiple signing keys (e.g. Google, Microsoft Entra ID, Auth0)
|
|
26
|
+
- **JWKS Caching** (`OmniauthOidc::JwksCache`): Thread-safe JWKS cache with configurable TTL (default 1 hour)
|
|
27
|
+
- Automatic cache invalidation and retry on signature verification failure
|
|
28
|
+
- Manual cache control via `OmniauthOidc::JwksCache.clear!` and `.invalidate(uri)`
|
|
29
|
+
- Configurable via `jwks_cache_ttl` option
|
|
30
|
+
- **Logging & Instrumentation** (`OmniauthOidc::Logging`): Logging via Ruby Logger with optional ActiveSupport::Notifications integration
|
|
31
|
+
- Configurable log levels (default: WARN)
|
|
32
|
+
- Automatic sanitization of sensitive data (tokens, secrets, code verifiers)
|
|
33
|
+
- Event instrumentation for config fetch, token exchange, userinfo fetch, JWKS fetch, request phase, and callback phase
|
|
34
|
+
- **Error Hierarchy**: Specific error classes for all failure modes
|
|
35
|
+
- `TokenVerificationError`, `TokenExpiredError`, `InvalidAlgorithmError`, `InvalidSignatureError`
|
|
36
|
+
- `InvalidIssuerError`, `InvalidAudienceError`, `InvalidNonceError`
|
|
37
|
+
- `JwksFetchError`, `KeyNotFoundError`
|
|
38
|
+
- `ConfigurationError`, `MissingConfigurationError`
|
|
39
|
+
- **Configuration Validation**: Required options (`identifier`, `secret`, `config_endpoint`) are validated on first request with a clear error message listing all missing fields
|
|
40
|
+
- **RBS Type Signatures** (`sig/omniauth_oidc.rbs`): Full type declarations for all public API classes
|
|
41
|
+
- **Comprehensive Test Suite**: Unit tests for HTTP client, JWK handler, JWKS cache, logging, errors, and strategy configuration
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
- **JWK kid Selection**: `keyset_for_algorithm` now selects the correct signing key by `kid` from JWKS. Previously, with multiple keys in the JWKS, verification could fail or use the wrong key.
|
|
45
|
+
- **Dead Code Removed**: Removed `decode` method in Verify module that referenced undefined `UrlSafeBase64` constant
|
|
46
|
+
- **Typo**: Fixed `client_singing_alg` to `client_signing_alg` in error messages
|
|
47
|
+
- **Scope Default**: Fixed default scope fallback from `[:open_id]` (typo) to `[:openid]`
|
|
48
|
+
- **AuthHash Consistency**: Consolidated duplicate user info fetching and AuthHash construction into OmniAuth DSL blocks
|
|
49
|
+
- **Removed `resolve_endpoint_from_host`**: Endpoints are now used as full URLs from the discovery document instead of being stripped to relative paths
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
- **Module Loading**: Replaced `Dir.glob` with explicit `require_relative` for clarity and predictable load order
|
|
53
|
+
- **Dependency Constraints**: Runtime dependencies reduced to `omniauth ~> 2.1` and `jwt ~> 2.7`
|
|
54
|
+
- **CI Updates**: Added Ruby 4.0.1 to test matrix, updated `actions/checkout` to v6, RuboCop runs on Ruby 3.3
|
|
55
|
+
- **RuboCop Config**: Fixed invalid cop name `Metrics/Metrics/CyclomaticComplexity`, added `NewCops: enable`
|
|
56
|
+
|
|
57
|
+
### Migration Guide from 0.x to 1.0.0
|
|
58
|
+
|
|
59
|
+
1. **Dependencies**: Remove gems that were only used by this gem, then run `bundle install`:
|
|
60
|
+
- `httparty`
|
|
61
|
+
- `openid_connect`
|
|
62
|
+
- `openid_config_parser`
|
|
63
|
+
- `json-jwt`
|
|
64
|
+
2. **Session Keys**: If you manually access session keys, update to the new namespaced format: `omniauth.{provider_name}.state`, `omniauth.{provider_name}.nonce`.
|
|
65
|
+
3. **Scope Handling**: If your code treats `scope` as an Array, update it. `scope` now returns a space-delimited String.
|
|
66
|
+
4. **Error Handling**: Update rescue clauses to catch the new error classes if needed. For example:
|
|
67
|
+
- `Rack::OAuth2::Client::Error` -> `OmniauthOidc::TokenError`
|
|
68
|
+
- Algorithm mismatches now raise `OmniauthOidc::InvalidAlgorithmError` instead of `CallbackError`
|
|
69
|
+
5. **Response Objects**: If you access `access_token` or `user_info` objects directly, note they are now `OmniauthOidc::ResponseObjects::AccessToken` and `OmniauthOidc::ResponseObjects::UserInfo` respectively.
|
|
70
|
+
6. **Logging** (optional): Configure logging level: `OmniauthOidc::Logging.log_level = Logger::INFO`
|
|
71
|
+
7. **Ruby 4.0+**: Add `gem "ostruct"` to your Gemfile (removed from default gems in Ruby 4.0).
|
|
72
|
+
|
|
3
73
|
## [0.2.7] - 2024-10-11
|
|
4
74
|
- Dependencies update
|
|
5
75
|
|
data/LICENSE.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
The MIT License (MIT)
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2024 Suleyman Musayev
|
|
3
|
+
Copyright (c) 2024-present Suleyman Musayev
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
data/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# OmniAuth::Oidc
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
OmniAuth strategy for OpenID Connect (OIDC) authentication. Supports multiple OIDC providers, PKCE, JWKS key rotation, and both `code` and `id_token` response types.
|
|
4
|
+
|
|
5
|
+
Minimal dependencies: only `omniauth` and `jwt` gems required. All HTTP requests use Ruby's built-in `Net::HTTP` — no Faraday, HTTParty, or other external HTTP clients.
|
|
4
6
|
|
|
5
7
|
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
8
|
|
|
@@ -16,14 +18,15 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
|
16
18
|
|
|
17
19
|
$ gem install omniauth_oidc
|
|
18
20
|
|
|
21
|
+
**Ruby 4.0+**: Add `gem "ostruct"` to your Gemfile (`ostruct` was removed from default gems in Ruby 4.0).
|
|
19
22
|
|
|
20
23
|
## Usage
|
|
21
24
|
|
|
22
25
|
To use the OmniAuth OIDC strategy, you need to configure your Rails application and set up the necessary environment variables for OIDC client credentials.
|
|
23
26
|
|
|
24
27
|
### Configuration
|
|
25
|
-
|
|
26
|
-
Create an initializer file at `config/initializers/omniauth.rb
|
|
28
|
+
|
|
29
|
+
You must provide Client ID, Client Secret, and the URL for the OIDC configuration endpoint as a minimum for `omniauth_oidc` to work. Create an initializer file at `config/initializers/omniauth.rb`:
|
|
27
30
|
|
|
28
31
|
```ruby
|
|
29
32
|
# config/initializers/omniauth.rb
|
|
@@ -39,13 +42,13 @@ Rails.application.config.middleware.use OmniAuth::Builder do
|
|
|
39
42
|
end
|
|
40
43
|
```
|
|
41
44
|
|
|
42
|
-
With Devise
|
|
45
|
+
With Devise:
|
|
43
46
|
|
|
44
47
|
```ruby
|
|
45
48
|
Devise.setup do |config|
|
|
46
49
|
config.omniauth :oidc, {
|
|
47
50
|
name: :simple_provider,
|
|
48
|
-
scope: [
|
|
51
|
+
scope: %w[openid email profile address],
|
|
49
52
|
response_type: :code,
|
|
50
53
|
uid_field: "preferred_username",
|
|
51
54
|
client_options: {
|
|
@@ -57,7 +60,7 @@ Devise.setup do |config|
|
|
|
57
60
|
end
|
|
58
61
|
```
|
|
59
62
|
|
|
60
|
-
The gem also supports a wide range of optional parameters for higher degree of configurability
|
|
63
|
+
The gem also supports a wide range of optional parameters for a higher degree of configurability:
|
|
61
64
|
|
|
62
65
|
```ruby
|
|
63
66
|
# config/initializers/omniauth.rb
|
|
@@ -65,7 +68,7 @@ Rails.application.config.middleware.use OmniAuth::Builder do
|
|
|
65
68
|
provider :oidc, {
|
|
66
69
|
name: :complex_provider, # used for dynamic routing
|
|
67
70
|
issuer: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77',
|
|
68
|
-
scope: [
|
|
71
|
+
scope: %w[openid],
|
|
69
72
|
response_type: 'id_token',
|
|
70
73
|
require_state: true,
|
|
71
74
|
response_mode: :query,
|
|
@@ -73,24 +76,25 @@ Rails.application.config.middleware.use OmniAuth::Builder do
|
|
|
73
76
|
send_nonce: false,
|
|
74
77
|
uid_field: "sub",
|
|
75
78
|
pkce: false,
|
|
79
|
+
jwks_cache_ttl: 3600, # JWKS cache TTL in seconds (default: 3600)
|
|
76
80
|
client_options: {
|
|
77
81
|
identifier: '23575f4602bebbd9a17dbc38d85bd1a77',
|
|
78
82
|
secret: ENV['COMPLEX_PROVIDER_CLIENT_SECRET'],
|
|
79
83
|
config_endpoint: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/.well-known/openid-configuration',
|
|
80
|
-
host: 'complexprovider.com'
|
|
84
|
+
host: 'complexprovider.com',
|
|
81
85
|
scheme: "https",
|
|
82
86
|
port: 443,
|
|
83
87
|
authorization_endpoint: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/authorization',
|
|
84
88
|
token_endpoint: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/token',
|
|
85
89
|
userinfo_endpoint: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/userinfo',
|
|
86
90
|
jwks_uri: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/jwks',
|
|
87
|
-
end_session_endpoint: '/signout'
|
|
91
|
+
end_session_endpoint: 'https://complexprovider.com/signout'
|
|
88
92
|
}
|
|
89
93
|
}
|
|
90
94
|
end
|
|
91
95
|
```
|
|
92
96
|
|
|
93
|
-
Ensure to replace identifier, secret, configuration endpoint
|
|
97
|
+
Ensure to replace identifier, secret, configuration endpoint URL and others with credentials received from your OIDC provider.
|
|
94
98
|
Please note that the gem does not accept `redirect_uri` as a configurable option. For details please see section Routes.
|
|
95
99
|
|
|
96
100
|
### Redirecting for Authentication
|
|
@@ -103,7 +107,7 @@ Buttons and links to initialize the authentication request can be placed on rele
|
|
|
103
107
|
|
|
104
108
|
### Handling Callbacks
|
|
105
109
|
|
|
106
|
-
The gem uses
|
|
110
|
+
The gem uses dynamic routes to handle different phases, and while you can use same routes in your Rails application, for
|
|
107
111
|
better experience you should have a controller to process the authenticated user. Create a CallbacksController:
|
|
108
112
|
|
|
109
113
|
```ruby
|
|
@@ -113,9 +117,9 @@ class CallbacksController < ApplicationController
|
|
|
113
117
|
# user info received from OIDC provider will be available in `request.env['omniauth.auth']`
|
|
114
118
|
auth = request.env['omniauth.auth']
|
|
115
119
|
|
|
116
|
-
user = User.find_or_create_by(uid: auth['uid']) do |
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
user = User.find_or_create_by(uid: auth['uid']) do |u|
|
|
121
|
+
u.name = auth['info']['name']
|
|
122
|
+
u.email = auth['info']['email']
|
|
119
123
|
end
|
|
120
124
|
|
|
121
125
|
session[:user_id] = user.id
|
|
@@ -124,17 +128,48 @@ class CallbacksController < ApplicationController
|
|
|
124
128
|
end
|
|
125
129
|
```
|
|
126
130
|
|
|
131
|
+
The `omniauth.auth` hash includes:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
{
|
|
135
|
+
provider: :simple_provider,
|
|
136
|
+
uid: "user123",
|
|
137
|
+
info: {
|
|
138
|
+
name: "Test User",
|
|
139
|
+
email: "test@example.com",
|
|
140
|
+
email_verified: true,
|
|
141
|
+
nickname: "testuser", # preferred_username
|
|
142
|
+
first_name: "Test", # given_name
|
|
143
|
+
last_name: "User", # family_name
|
|
144
|
+
gender: "male",
|
|
145
|
+
image: "https://example.com/avatar.jpg", # picture
|
|
146
|
+
phone: "+1234567890", # phone_number
|
|
147
|
+
urls: { website: "https://testuser.com" }
|
|
148
|
+
},
|
|
149
|
+
credentials: {
|
|
150
|
+
id_token: "eyJ...",
|
|
151
|
+
token: "access_token_value",
|
|
152
|
+
refresh_token: "refresh_token_value",
|
|
153
|
+
expires_in: 3600,
|
|
154
|
+
scope: "openid profile email"
|
|
155
|
+
},
|
|
156
|
+
extra: {
|
|
157
|
+
raw_info: { ... } # full userinfo response attributes
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
127
162
|
### Routes
|
|
128
163
|
|
|
129
164
|
The gem uses dynamic routes when making requests to the OIDC provider endpoints, so called `redirect_uri` which is a
|
|
130
|
-
non-configurable value that follows the naming pattern of `https://your_app.com/auth/<simple_provider>/callback`,
|
|
165
|
+
non-configurable value that follows the naming pattern of `https://your_app.com/auth/<simple_provider>/callback`,
|
|
131
166
|
where `<simple_provider>` is the provider name defined within the configuration of the `omniauth.rb` initializer.
|
|
132
167
|
This represents the `redirect_uri` that will be passed with the authorization request to your OIDC provider and that
|
|
133
168
|
has to be registered with your OIDC provider as permitted `redirect_uri`.
|
|
134
169
|
|
|
135
170
|
Dynamic routes are used to process responses and perform intermediary steps by the middleware, e.g. request phase,
|
|
136
|
-
token verification. While you can define and use same routes within your Rails app, it is highly recommended to modify
|
|
137
|
-
your `routes.rb` to perform a dynamic redirect to
|
|
171
|
+
token verification. While you can define and use same routes within your Rails app, it is highly recommended to modify
|
|
172
|
+
your `routes.rb` to perform a dynamic redirect to another controller method so this does not cause any conflicts with
|
|
138
173
|
the middleware or the authorization flow.
|
|
139
174
|
|
|
140
175
|
In an example below, `auth/:provider/callback` is generalized `redirect_uri` value that is passed in the authorization
|
|
@@ -163,11 +198,11 @@ end
|
|
|
163
198
|
```
|
|
164
199
|
|
|
165
200
|
**Please note that you should register `https://your_app.com/auth/<simple_provider>/callback` with your OIDC provider
|
|
166
|
-
as a callback redirect
|
|
201
|
+
as a callback redirect URL.**
|
|
167
202
|
|
|
168
203
|
### Using Access Token Without User Info
|
|
169
204
|
|
|
170
|
-
In case your app
|
|
205
|
+
In case your app requires only an access token and not the user information, then you can specify an optional
|
|
171
206
|
configuration in the omniauth initializer:
|
|
172
207
|
|
|
173
208
|
```ruby
|
|
@@ -185,38 +220,17 @@ Rails.application.config.middleware.use OmniAuth::Builder do
|
|
|
185
220
|
end
|
|
186
221
|
```
|
|
187
222
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
```ruby
|
|
191
|
-
# app/controllers/callbacks_controller.rb
|
|
192
|
-
class CallbacksController < ApplicationController
|
|
193
|
-
def omniauth
|
|
194
|
-
# access token parameters received from OIDC provider will be available in `request.env['omniauth.auth']`
|
|
195
|
-
omniauth_params = request.env['omniauth.auth']
|
|
196
|
-
|
|
197
|
-
# omniauth_params will contain similar data as shown below
|
|
198
|
-
# {"provider"=>:simple_provider_access_token_only,
|
|
199
|
-
# "credentials"=>
|
|
200
|
-
# {"id_token"=> "id token value",
|
|
201
|
-
# "token"=> "token value",
|
|
202
|
-
# "refresh_token"=>"refresh token value",
|
|
203
|
-
# "expires_in"=>300,
|
|
204
|
-
# "scope"=>nil
|
|
205
|
-
# }
|
|
206
|
-
# }
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
```
|
|
223
|
+
When `fetch_user_info` is `false`, user info is extracted from the ID token claims instead of calling the userinfo endpoint. If the userinfo endpoint call fails, the gem also falls back to ID token claims automatically.
|
|
210
224
|
|
|
211
225
|
### Ending Session
|
|
212
226
|
|
|
213
227
|
The gem provides two configuration options to allow ending a session simultaneously with your client application and the
|
|
214
228
|
OIDC provider.
|
|
215
229
|
|
|
216
|
-
To use this feature, you need to provide a `logout_path` in the options and an `end_session_endpoint` in the client
|
|
217
|
-
options. Here
|
|
230
|
+
To use this feature, you need to provide a `logout_path` in the options and an `end_session_endpoint` in the client
|
|
231
|
+
options. Here's a sample setup:
|
|
218
232
|
|
|
219
|
-
```
|
|
233
|
+
```ruby
|
|
220
234
|
provider :oidc, {
|
|
221
235
|
name: :simple_provider,
|
|
222
236
|
client_options: {
|
|
@@ -236,32 +250,76 @@ options. Here’s a sample setup:
|
|
|
236
250
|
Using these two configurations, you can ensure that when a user logs out from your application, they are also logged out
|
|
237
251
|
from the OIDC provider, providing a seamless logout across multiple services.
|
|
238
252
|
|
|
239
|
-
This works by calling `other_phase` on every controller request in your application. The method checks if the requested
|
|
240
|
-
URL matches the defined `logout_path`. If it does (i.e. current user has requested to log out from your application)
|
|
241
|
-
`other_phase` performs a redirect to the `end_session_endpoint` to terminate the user's session with the OIDC provider
|
|
253
|
+
This works by calling `other_phase` on every controller request in your application. The method checks if the requested
|
|
254
|
+
URL matches the defined `logout_path`. If it does (i.e. current user has requested to log out from your application)
|
|
255
|
+
`other_phase` performs a redirect to the `end_session_endpoint` to terminate the user's session with the OIDC provider
|
|
242
256
|
and then it returns back to your application and concludes the request to end the current user's session.
|
|
243
257
|
|
|
244
258
|
For additional details please refer to the [OIDC specification](https://openid.net/specs/openid-connect-session-1_0-17.html#:~:text=%C2%A0TOC-,5.%C2%A0%20RP%2DInitiated%20Logout,-An%20RP%20can).
|
|
245
259
|
|
|
260
|
+
### Logging
|
|
261
|
+
|
|
262
|
+
The gem includes built-in logging with automatic sensitive data sanitization. By default, log level is set to WARN.
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
# Set log level
|
|
266
|
+
OmniauthOidc::Logging.log_level = Logger::INFO
|
|
267
|
+
|
|
268
|
+
# Use a custom logger
|
|
269
|
+
OmniauthOidc::Logging.logger = Rails.logger
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
If ActiveSupport::Notifications is available (e.g. in Rails), the gem publishes instrumentation events:
|
|
273
|
+
|
|
274
|
+
- `config.fetch.omniauth_oidc` — OIDC discovery document fetch
|
|
275
|
+
- `token.exchange.omniauth_oidc` — authorization code to token exchange
|
|
276
|
+
- `userinfo.fetch.omniauth_oidc` — userinfo endpoint call
|
|
277
|
+
- `jwks.fetch.omniauth_oidc` — JWKS fetch
|
|
278
|
+
- `request_phase.start.omniauth_oidc` — authorization redirect
|
|
279
|
+
- `callback_phase.start.omniauth_oidc` — callback processing
|
|
280
|
+
- `id_token.verify.omniauth_oidc` — ID token verification
|
|
281
|
+
|
|
282
|
+
### Error Handling
|
|
283
|
+
|
|
284
|
+
The gem raises specific error classes that you can rescue in your callback handling:
|
|
285
|
+
|
|
286
|
+
| Error Class | When Raised |
|
|
287
|
+
|---|---|
|
|
288
|
+
| `OmniauthOidc::MissingConfigurationError` | Required options (`identifier`, `secret`, `config_endpoint`) are missing |
|
|
289
|
+
| `OmniauthOidc::ConfigurationError` | OIDC discovery endpoint fetch fails |
|
|
290
|
+
| `OmniauthOidc::TokenError` | Token exchange fails |
|
|
291
|
+
| `OmniauthOidc::TokenVerificationError` | JWT format is invalid |
|
|
292
|
+
| `OmniauthOidc::TokenExpiredError` | ID token `exp` claim is in the past |
|
|
293
|
+
| `OmniauthOidc::InvalidAlgorithmError` | JWT algorithm does not match `client_signing_alg` |
|
|
294
|
+
| `OmniauthOidc::InvalidSignatureError` | JWT signature verification fails |
|
|
295
|
+
| `OmniauthOidc::InvalidIssuerError` | ID token `iss` does not match expected issuer |
|
|
296
|
+
| `OmniauthOidc::InvalidAudienceError` | ID token `aud` does not match client identifier |
|
|
297
|
+
| `OmniauthOidc::InvalidNonceError` | ID token `nonce` does not match stored nonce |
|
|
298
|
+
| `OmniauthOidc::JwksFetchError` | JWKS endpoint fetch fails |
|
|
299
|
+
|
|
300
|
+
All error classes inherit from `OmniauthOidc::Error < RuntimeError`.
|
|
301
|
+
|
|
246
302
|
### Advanced Configuration
|
|
303
|
+
|
|
247
304
|
You can customize the OIDC strategy further by adding additional configuration options:
|
|
248
305
|
|
|
249
306
|
| Field | Description | Required | Default Value | Example/Notes |
|
|
250
307
|
|------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------|-------------------------------------|-------------------------------------------------------|
|
|
251
308
|
| name | Arbitrary string to identify OIDC provider and segregate it from other OIDC providers | no | `"oidc"` | `:simple_provider` |
|
|
252
|
-
| issuer | Root url for the OIDC authorization server | no |
|
|
253
|
-
| fetch_user_info | Fetches user information from
|
|
254
|
-
|
|
|
255
|
-
| scope | OIDC scopes to
|
|
309
|
+
| issuer | Root url for the OIDC authorization server | no | retrieved from config_endpoint | `"https://simpleprovider.com"` |
|
|
310
|
+
| fetch_user_info | Fetches user information from userinfo endpoint using the access token. If false, user info is extracted from ID token claims | no | `true` | `fetch_user_info: false` |
|
|
311
|
+
| client_signing_alg | Expected JWT signing algorithm. If set, tokens signed with a different algorithm are rejected | no | `nil` (any algorithm accepted) | `"RS256"`, `"HS256"` |
|
|
312
|
+
| scope | OIDC scopes to request. Accepts Array or String; always sent as a space-delimited string | `openid` is required | all scopes offered by OIDC provider | `%w[openid profile email]` |
|
|
256
313
|
| response_type | OAuth2 response type expected from OIDC provider during authorization | no | `"code"` | `"code"` or `"id_token"` |
|
|
257
314
|
| 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) }` |
|
|
258
315
|
| require_state | Boolean to indicate if state param should be verified. This is a recommendation by OIDC spec | no | `true` | `true` or `false` |
|
|
259
316
|
| 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` |
|
|
260
317
|
| 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` |
|
|
261
318
|
| prompt | Specifies whether the OIDC authorization server prompts the end user for reauthentication and consent | no | `nil` | `:none`, `:login`, `:consent` or `:select_account` |
|
|
319
|
+
| send_nonce | Include a nonce in the authorization request and verify it in the ID token | no | `true` | `true` or `false` |
|
|
262
320
|
| send_scope_to_token_endpoint | Should the scope parameter be sent to the authorization token endpoint | no | `true` | `true` or `false` |
|
|
263
321
|
| 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"` |
|
|
264
|
-
| uid_field | Field of the user info response to be used as a unique ID | no | `
|
|
322
|
+
| uid_field | Field of the user info response to be used as a unique ID | no | `"sub"` | `"sub"` or `"preferred_username"` |
|
|
265
323
|
| extra_authorize_params | Hash of extra fixed parameters that will be merged to the authorization request | no | `{}` | `{"tenant" => "common"}` |
|
|
266
324
|
| allow_authorize_params | List of allowed dynamic parameters that will be merged to the authorization request | no | `[]` | `[:screen_name]` |
|
|
267
325
|
| pkce | Enable [PKCE flow](https://oauth.net/2/pkce/) | no | `false` | `true` or `false` |
|
|
@@ -269,9 +327,13 @@ You can customize the OIDC strategy further by adding additional configuration o
|
|
|
269
327
|
| pkce_options | Specify custom implementation of the PKCE code challenge/method | no | SHA256(code_challenge) in hex | Proc to customise the code challenge generation |
|
|
270
328
|
| client_options | Hash of client options detailed below in a separate table | yes | see below | see below |
|
|
271
329
|
| 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"` |
|
|
272
|
-
|
|
|
273
|
-
|
|
|
274
|
-
|
|
330
|
+
| client_jwk_signing_key | JWK or JWKS (as JSON string or Hash) for local signature verification without fetching from `jwks_uri` | no | `nil` | `'{"kty":"RSA","n":"...","e":"AQAB"}'` |
|
|
331
|
+
| client_x509_signing_key | X.509 certificate (PEM format) for local signature verification | no | `nil` | PEM string |
|
|
332
|
+
| logout_path | Log out is only triggered when the request path ends on this path | no | `"/logout"` | `"/sign_out"` |
|
|
333
|
+
| jwks_cache_ttl | JWKS cache time-to-live in seconds | no | `3600` (1 hour) | `7200` |
|
|
334
|
+
| hd | Google-specific: hosted domain parameter | no | `nil` | `"example.com"` |
|
|
335
|
+
| max_age | Maximum authentication age in seconds | no | `nil` | `3600` |
|
|
336
|
+
| acr_values | Authentication Class Reference (ACR) values per [RFC9470](https://www.rfc-editor.org/rfc/rfc9470.html) | no | `nil` | `"c1 c2"` |
|
|
275
337
|
|
|
276
338
|
Below are options for the `client_options` hash of the configuration:
|
|
277
339
|
|
|
@@ -280,14 +342,14 @@ Below are options for the `client_options` hash of the configuration:
|
|
|
280
342
|
| identifier | OAuth2 client_id | yes | `nil` |
|
|
281
343
|
| secret | OAuth2 client secret | yes | `nil` |
|
|
282
344
|
| config_endpoint | OIDC configuration endpoint | yes | `nil` |
|
|
283
|
-
| scheme |
|
|
284
|
-
| host |
|
|
285
|
-
| port |
|
|
286
|
-
| authorization_endpoint |
|
|
287
|
-
| token_endpoint |
|
|
288
|
-
| userinfo_endpoint |
|
|
289
|
-
| jwks_uri |
|
|
290
|
-
| end_session_endpoint |
|
|
345
|
+
| scheme | HTTP scheme to use | no | `"https"` |
|
|
346
|
+
| host | Host of the authorization server | no | `nil` |
|
|
347
|
+
| port | Port for the authorization server | no | `443` |
|
|
348
|
+
| authorization_endpoint | Authorize endpoint on the authorization server | no | retrieved from config_endpoint |
|
|
349
|
+
| token_endpoint | Token endpoint on the authorization server | no | retrieved from config_endpoint |
|
|
350
|
+
| userinfo_endpoint | User info endpoint on the authorization server | no | retrieved from config_endpoint |
|
|
351
|
+
| jwks_uri | JWKS URI on the authorization server | no | retrieved from config_endpoint |
|
|
352
|
+
| end_session_endpoint | URL to call to log the user out at the authorization server | no | `nil` |
|
|
291
353
|
|
|
292
354
|
## Contributing
|
|
293
355
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
|
|
5
|
+
module OmniauthOidc
|
|
6
|
+
# Custom OIDC client using Net::HTTP
|
|
7
|
+
class Client
|
|
8
|
+
attr_accessor :identifier, :secret, :authorization_endpoint, :token_endpoint, :userinfo_endpoint,
|
|
9
|
+
:host, :redirect_uri
|
|
10
|
+
|
|
11
|
+
def initialize(options = {})
|
|
12
|
+
@identifier = options[:identifier] || options["identifier"]
|
|
13
|
+
@secret = options[:secret] || options["secret"]
|
|
14
|
+
@authorization_endpoint = options[:authorization_endpoint] || options["authorization_endpoint"]
|
|
15
|
+
@token_endpoint = options[:token_endpoint] || options["token_endpoint"]
|
|
16
|
+
@userinfo_endpoint = options[:userinfo_endpoint] || options["userinfo_endpoint"]
|
|
17
|
+
@redirect_uri = options[:redirect_uri] || options["redirect_uri"]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def authorization_uri(params = {})
|
|
21
|
+
uri = URI.parse(authorization_endpoint)
|
|
22
|
+
query_params = {
|
|
23
|
+
client_id: identifier,
|
|
24
|
+
response_type: params[:response_type] || "code",
|
|
25
|
+
scope: params[:scope] || "openid profile email",
|
|
26
|
+
redirect_uri: params[:redirect_uri] || @redirect_uri,
|
|
27
|
+
state: params[:state],
|
|
28
|
+
nonce: params[:nonce]
|
|
29
|
+
}.compact
|
|
30
|
+
|
|
31
|
+
# Add PKCE parameters if provided
|
|
32
|
+
query_params[:code_challenge] = params[:code_challenge] if params[:code_challenge]
|
|
33
|
+
query_params[:code_challenge_method] = params[:code_challenge_method] if params[:code_challenge_method]
|
|
34
|
+
|
|
35
|
+
# Add any additional params
|
|
36
|
+
query_params.merge!(params[:extra_params]) if params[:extra_params]
|
|
37
|
+
|
|
38
|
+
uri.query = URI.encode_www_form(query_params)
|
|
39
|
+
uri.to_s
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def access_token!(params = {}) # rubocop:disable Metrics/MethodLength
|
|
43
|
+
body_params = {
|
|
44
|
+
grant_type: "authorization_code",
|
|
45
|
+
code: params[:code],
|
|
46
|
+
redirect_uri: params[:redirect_uri] || @redirect_uri,
|
|
47
|
+
client_id: identifier,
|
|
48
|
+
client_secret: secret
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Add PKCE verifier if provided
|
|
52
|
+
body_params[:code_verifier] = params[:code_verifier] if params[:code_verifier]
|
|
53
|
+
|
|
54
|
+
OmniauthOidc::Logging.instrument("token.exchange", code: "[FILTERED]") do
|
|
55
|
+
response = HttpClient.post(
|
|
56
|
+
token_endpoint,
|
|
57
|
+
body: URI.encode_www_form(body_params),
|
|
58
|
+
headers: {
|
|
59
|
+
"Content-Type" => "application/x-www-form-urlencoded",
|
|
60
|
+
"Accept" => "application/json"
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
ResponseObjects::AccessToken.new(response)
|
|
65
|
+
end
|
|
66
|
+
rescue HttpClient::HttpError => e
|
|
67
|
+
raise OmniauthOidc::TokenError, "Token exchange failed: #{e.message}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def userinfo!(access_token)
|
|
71
|
+
OmniauthOidc::Logging.instrument("userinfo.fetch", endpoint: userinfo_endpoint) do
|
|
72
|
+
response = HttpClient.get(
|
|
73
|
+
userinfo_endpoint,
|
|
74
|
+
headers: {
|
|
75
|
+
"Authorization" => "Bearer #{access_token}",
|
|
76
|
+
"Accept" => "application/json"
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
ResponseObjects::UserInfo.new(response)
|
|
81
|
+
end
|
|
82
|
+
rescue HttpClient::HttpError => e
|
|
83
|
+
raise OmniauthOidc::TokenError, "Failed to fetch user info: #{e.message}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OmniauthOidc
|
|
4
|
+
# Fetches and parses OpenID Connect configuration from .well-known endpoint
|
|
5
|
+
class ConfigFetcher
|
|
6
|
+
# Config object with dynamic attribute access for OIDC discovery fields
|
|
7
|
+
class Config
|
|
8
|
+
def initialize(attributes = {})
|
|
9
|
+
@attributes = attributes
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def [](key)
|
|
13
|
+
@attributes[key.to_sym] || @attributes[key.to_s]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def []=(key, value)
|
|
17
|
+
@attributes[key.to_sym] = value
|
|
18
|
+
@attributes[key.to_s] = value
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
22
|
+
setter = method_name.to_s.end_with?("=")
|
|
23
|
+
key = setter ? method_name.to_s.chomp("=") : method_name.to_s
|
|
24
|
+
setter || @attributes.key?(key.to_sym) || @attributes.key?(key) || super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def method_missing(method_name, *args)
|
|
30
|
+
name = method_name.to_s
|
|
31
|
+
|
|
32
|
+
if name.end_with?("=")
|
|
33
|
+
key = name.chomp("=")
|
|
34
|
+
@attributes[key.to_sym] = args.first
|
|
35
|
+
@attributes[key] = args.first
|
|
36
|
+
elsif @attributes.key?(name.to_sym)
|
|
37
|
+
@attributes[name.to_sym]
|
|
38
|
+
elsif @attributes.key?(name)
|
|
39
|
+
@attributes[name]
|
|
40
|
+
else
|
|
41
|
+
super
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class << self
|
|
47
|
+
def fetch(endpoint_url, max_retries: 3)
|
|
48
|
+
retries = 0
|
|
49
|
+
begin
|
|
50
|
+
response = OmniauthOidc::HttpClient.get(endpoint_url)
|
|
51
|
+
symbolized_config = deep_symbolize_keys(response)
|
|
52
|
+
Config.new(symbolized_config)
|
|
53
|
+
rescue OmniauthOidc::HttpClient::HttpError => e
|
|
54
|
+
retries += 1
|
|
55
|
+
retry if retries < max_retries
|
|
56
|
+
raise OmniauthOidc::ConfigurationError, "Failed to fetch OIDC configuration: #{e.message}"
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
raise OmniauthOidc::ConfigurationError, "Failed to fetch OIDC configuration: #{e.message}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# Recursively converts keys of a hash to symbols while retaining the original string keys
|
|
65
|
+
def deep_symbolize_keys(hash)
|
|
66
|
+
result = {}
|
|
67
|
+
hash.each do |key, value|
|
|
68
|
+
sym_key = key.to_sym
|
|
69
|
+
result[sym_key] = value.is_a?(Hash) ? deep_symbolize_keys(value) : value
|
|
70
|
+
result[key] = result[sym_key] # Add the string key as well
|
|
71
|
+
end
|
|
72
|
+
result
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|