omniauth-oidc-strategy 0.3.2
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 +7 -0
- data/.rubocop.yml +5 -0
- data/CHANGELOG.md +19 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +235 -0
- data/Rakefile +12 -0
- data/lib/omniauth/oidc/config.rb +48 -0
- data/lib/omniauth/oidc/errors.rb +9 -0
- data/lib/omniauth/oidc/user_agent.rb +7 -0
- data/lib/omniauth/oidc/version.rb +5 -0
- data/lib/omniauth/strategies/oidc/callback.rb +126 -0
- data/lib/omniauth/strategies/oidc/request.rb +58 -0
- data/lib/omniauth/strategies/oidc/serializer.rb +71 -0
- data/lib/omniauth/strategies/oidc/transport.rb +47 -0
- data/lib/omniauth/strategies/oidc/verify.rb +171 -0
- data/lib/omniauth/strategies/oidc.rb +271 -0
- data/lib/omniauth-oidc-strategy.rb +7 -0
- data/omniauth-oidc-strategy.gemspec +43 -0
- data/sig/omniauth_oidc.rbs +4 -0
- metadata +152 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OmniAuth
|
|
4
|
+
module Strategies
|
|
5
|
+
class Oidc
|
|
6
|
+
module Serializer
|
|
7
|
+
def serialized_access_token_auth_hash
|
|
8
|
+
{
|
|
9
|
+
provider: name,
|
|
10
|
+
credentials: serialized_credentials
|
|
11
|
+
}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def serialized_credentials
|
|
15
|
+
{
|
|
16
|
+
id_token: access_token.id_token,
|
|
17
|
+
token: access_token.access_token,
|
|
18
|
+
refresh_token: access_token.refresh_token,
|
|
19
|
+
expires_in: access_token.expires_in,
|
|
20
|
+
scope: access_token.scope
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def serialized_extra
|
|
25
|
+
{
|
|
26
|
+
claims: id_token_raw_attributes,
|
|
27
|
+
scope: scope
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def serialized_request_options
|
|
32
|
+
{
|
|
33
|
+
response_type: options.response_type,
|
|
34
|
+
response_mode: options.response_mode,
|
|
35
|
+
scope: scope,
|
|
36
|
+
state: new_state,
|
|
37
|
+
login_hint: params["login_hint"],
|
|
38
|
+
ui_locales: params["ui_locales"],
|
|
39
|
+
claims_locales: params["claims_locales"],
|
|
40
|
+
prompt: options.prompt,
|
|
41
|
+
nonce: (new_nonce if options.send_nonce),
|
|
42
|
+
hd: options.hd,
|
|
43
|
+
acr_values: options.acr_values
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def serialized_user_info
|
|
48
|
+
{
|
|
49
|
+
name: user_info.name,
|
|
50
|
+
email: user_info.email,
|
|
51
|
+
email_verified: user_info.email_verified,
|
|
52
|
+
first_name: user_info.given_name,
|
|
53
|
+
last_name: user_info.family_name,
|
|
54
|
+
phone: user_info.phone_number,
|
|
55
|
+
address: user_info.address
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def serialized_user_info_auth_hash
|
|
60
|
+
{
|
|
61
|
+
provider: name,
|
|
62
|
+
uid: user_info.sub,
|
|
63
|
+
info: serialized_user_info,
|
|
64
|
+
extra: serialized_extra,
|
|
65
|
+
credentials: serialized_credentials
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "faraday/net_http_persistent"
|
|
5
|
+
require "faraday/retry"
|
|
6
|
+
|
|
7
|
+
module OmniAuth
|
|
8
|
+
module Strategies
|
|
9
|
+
class Oidc
|
|
10
|
+
# HTTP transport layer using Faraday
|
|
11
|
+
module Transport
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def connection
|
|
15
|
+
@connection ||= Faraday.new do |f|
|
|
16
|
+
f.request :retry, max: 2, interval: 0.5, backoff_factor: 2
|
|
17
|
+
f.headers["User-Agent"] = OmniauthOidc::USER_AGENT
|
|
18
|
+
f.ssl.min_version = OpenSSL::SSL::TLS1_2_VERSION
|
|
19
|
+
f.adapter :net_http_persistent
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def get(url, headers: {})
|
|
24
|
+
connection.get(url) do |req|
|
|
25
|
+
req.headers.merge!(headers)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def post(url, headers: {}, body: nil)
|
|
30
|
+
connection.post(url) do |req|
|
|
31
|
+
req.headers.merge!(headers)
|
|
32
|
+
req.body = body
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def fetch_json(url, headers: {})
|
|
37
|
+
response = get(url, headers: headers)
|
|
38
|
+
JSON.parse(response.body)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def reset!
|
|
42
|
+
@connection = nil
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
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
|
+
attr_reader :decoded_id_token
|
|
33
|
+
|
|
34
|
+
def fetch_key
|
|
35
|
+
parse_jwk_key(jwks_key)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def jwks_key
|
|
39
|
+
@jwks_key ||= Transport.fetch_json(config.jwks_uri)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def base64_decoded_jwt_secret
|
|
43
|
+
return unless options.jwt_secret_base64
|
|
44
|
+
|
|
45
|
+
Base64.decode64(options.jwt_secret_base64)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def verify_id_token!(id_token)
|
|
49
|
+
return unless id_token
|
|
50
|
+
decode_id_token(id_token).verify!(issuer: config.issuer,
|
|
51
|
+
client_id: client_options.identifier,
|
|
52
|
+
nonce: stored_nonce)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def decode_id_token(id_token)
|
|
56
|
+
decoded = JSON::JWT.decode(id_token, :skip_verification)
|
|
57
|
+
algorithm = decoded.algorithm.to_sym
|
|
58
|
+
|
|
59
|
+
validate_client_algorithm!(algorithm)
|
|
60
|
+
|
|
61
|
+
keyset =
|
|
62
|
+
case algorithm
|
|
63
|
+
when :HS256, :HS384, :HS512
|
|
64
|
+
secret
|
|
65
|
+
else
|
|
66
|
+
public_key
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
decoded.verify!(keyset)
|
|
70
|
+
@decoded_id_token = ::OpenIDConnect::ResponseObject::IdToken.new(decoded)
|
|
71
|
+
rescue JSON::JWK::Set::KidNotFound
|
|
72
|
+
# Workaround for https://github.com/nov/json-jwt/pull/92#issuecomment-824654949
|
|
73
|
+
raise if decoded&.header&.key?("kid")
|
|
74
|
+
|
|
75
|
+
decoded = decode_with_each_key!(id_token, keyset)
|
|
76
|
+
|
|
77
|
+
raise unless decoded
|
|
78
|
+
|
|
79
|
+
@decoded_id_token = decoded
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Check for jwt to match defined client_signing_alg
|
|
83
|
+
def validate_client_algorithm!(algorithm)
|
|
84
|
+
client_signing_alg = options.client_signing_alg&.to_sym
|
|
85
|
+
|
|
86
|
+
return unless client_signing_alg
|
|
87
|
+
return if algorithm == client_signing_alg
|
|
88
|
+
|
|
89
|
+
reason = "Received JWT is signed with #{algorithm}, but client_signing_alg is \
|
|
90
|
+
configured for #{client_signing_alg}"
|
|
91
|
+
raise CallbackError, error: :invalid_jwt_algorithm, reason: reason, uri: params["error_uri"]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def decode!(id_token, key)
|
|
95
|
+
::OpenIDConnect::ResponseObject::IdToken.decode(id_token, key)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def decode_with_each_key!(id_token, keyset)
|
|
99
|
+
return unless keyset.is_a?(JSON::JWK::Set)
|
|
100
|
+
|
|
101
|
+
keyset.each do |key|
|
|
102
|
+
begin
|
|
103
|
+
decoded = decode!(id_token, key)
|
|
104
|
+
rescue JSON::JWS::VerificationFailed, JSON::JWS::UnexpectedAlgorithm, JSON::JWK::UnknownAlgorithm
|
|
105
|
+
next
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
return decoded if decoded
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def stored_nonce
|
|
115
|
+
session.delete("omniauth.nonce")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def configured_public_key
|
|
119
|
+
@configured_public_key ||= if options.client_jwk_signing_key
|
|
120
|
+
parse_jwk_key(options.client_jwk_signing_key)
|
|
121
|
+
elsif options.client_x509_signing_key
|
|
122
|
+
parse_x509_key(options.client_x509_signing_key)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def parse_x509_key(key)
|
|
127
|
+
OpenSSL::X509::Certificate.new(key).public_key
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def parse_jwk_key(key)
|
|
131
|
+
json = key.is_a?(String) ? JSON.parse(key) : key
|
|
132
|
+
return JSON::JWK::Set.new(json["keys"]) if json.key?("keys")
|
|
133
|
+
|
|
134
|
+
JSON::JWK.new(json)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def decode(str)
|
|
138
|
+
UrlSafeBase64.decode64(str).unpack1("B*").to_i(2).to_s
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Converts camelCase keys to snake_case symbols. Handles standard OIDC
|
|
142
|
+
# claim names (e.g. "givenName" → :given_name). Does not handle acronym
|
|
143
|
+
# runs like "HTTPSEnabled" — not expected in OIDC responses.
|
|
144
|
+
def deep_underscore_keys(hash)
|
|
145
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
146
|
+
new_key = key.to_s.gsub(/([A-Z])/, '_\1').sub(/^_/, "").downcase.to_sym
|
|
147
|
+
result[new_key] = value.is_a?(Hash) ? deep_underscore_keys(value) : value
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def id_token_raw_attributes
|
|
152
|
+
decoded_id_token.raw_attributes
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def user_info
|
|
156
|
+
return @user_info if @user_info
|
|
157
|
+
|
|
158
|
+
if id_token_raw_attributes
|
|
159
|
+
merged_user_info = access_token.userinfo!.raw_attributes.merge(id_token_raw_attributes)
|
|
160
|
+
|
|
161
|
+
@user_info = ::OpenIDConnect::ResponseObject::UserInfo.new(
|
|
162
|
+
deep_underscore_keys(merged_user_info)
|
|
163
|
+
)
|
|
164
|
+
else
|
|
165
|
+
@user_info = access_token.userinfo!
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "timeout"
|
|
5
|
+
require "oauth2"
|
|
6
|
+
require "omniauth"
|
|
7
|
+
require "openid_connect"
|
|
8
|
+
require "forwardable"
|
|
9
|
+
|
|
10
|
+
require_relative "oidc/callback"
|
|
11
|
+
require_relative "oidc/request"
|
|
12
|
+
require_relative "oidc/serializer"
|
|
13
|
+
require_relative "oidc/transport"
|
|
14
|
+
require_relative "oidc/verify"
|
|
15
|
+
|
|
16
|
+
module OmniAuth
|
|
17
|
+
module Strategies
|
|
18
|
+
# OIDC strategy for omniauth
|
|
19
|
+
class Oidc
|
|
20
|
+
include OmniAuth::Strategy
|
|
21
|
+
include Callback
|
|
22
|
+
include Request
|
|
23
|
+
include Serializer
|
|
24
|
+
include Verify
|
|
25
|
+
|
|
26
|
+
extend Forwardable
|
|
27
|
+
|
|
28
|
+
RESPONSE_TYPE_EXCEPTIONS = {
|
|
29
|
+
"id_token" => { exception_class: OmniauthOidc::MissingIdTokenError, key: :missing_id_token }.freeze,
|
|
30
|
+
"code" => { exception_class: OmniauthOidc::MissingCodeError, key: :missing_code }.freeze
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
def_delegator :request, :params
|
|
34
|
+
|
|
35
|
+
option :name, :oidc # to separate each oidc provider available in the app
|
|
36
|
+
option(:client_options, identifier: nil, # client id, required
|
|
37
|
+
secret: nil, # client secret, required
|
|
38
|
+
host: nil, # oidc provider host, optional
|
|
39
|
+
scheme: "https", # connection scheme, optional
|
|
40
|
+
port: 443, # connection port, optional
|
|
41
|
+
config_endpoint: nil, # all data will be fetched from here, required
|
|
42
|
+
authorization_endpoint: nil, # optional
|
|
43
|
+
token_endpoint: nil, # optional
|
|
44
|
+
userinfo_endpoint: nil, # optional
|
|
45
|
+
jwks_uri: nil, # optional
|
|
46
|
+
end_session_endpoint: nil, # optional
|
|
47
|
+
environment: nil) # optional
|
|
48
|
+
|
|
49
|
+
option :issuer
|
|
50
|
+
option :client_signing_alg
|
|
51
|
+
option :jwt_secret_base64
|
|
52
|
+
option :client_jwk_signing_key
|
|
53
|
+
option :client_x509_signing_key
|
|
54
|
+
option :scope, [ :openid ]
|
|
55
|
+
option :response_type, "code" # ['code', 'id_token']
|
|
56
|
+
option :require_state, true
|
|
57
|
+
option :state
|
|
58
|
+
option :response_mode # [:query, :fragment, :form_post, :web_message]
|
|
59
|
+
option :display, nil # [:page, :popup, :touch, :wap]
|
|
60
|
+
option :prompt, nil # [:none, :login, :consent, :select_account]
|
|
61
|
+
option :hd, nil
|
|
62
|
+
option :max_age
|
|
63
|
+
option :ui_locales
|
|
64
|
+
option :id_token_hint
|
|
65
|
+
option :acr_values
|
|
66
|
+
option :send_nonce, true
|
|
67
|
+
option :fetch_user_info, true
|
|
68
|
+
option :send_scope_to_token_endpoint, true
|
|
69
|
+
option :client_auth_method
|
|
70
|
+
option :post_logout_redirect_uri
|
|
71
|
+
option :extra_authorize_params, {}
|
|
72
|
+
option :allow_authorize_params, []
|
|
73
|
+
option :uid_field, "sub"
|
|
74
|
+
option :pkce, false
|
|
75
|
+
option :pkce_verifier, nil
|
|
76
|
+
option :pkce_options, {
|
|
77
|
+
code_challenge: proc { |verifier|
|
|
78
|
+
Base64.urlsafe_encode64(Digest::SHA2.digest(verifier), padding: false)
|
|
79
|
+
},
|
|
80
|
+
code_challenge_method: "S256"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
option :logout_path, "/logout"
|
|
84
|
+
|
|
85
|
+
# Cross-module state contract. These methods and instance variables are
|
|
86
|
+
# shared between Callback, Verify, and Serializer modules during the
|
|
87
|
+
# callback phase:
|
|
88
|
+
#
|
|
89
|
+
# Callback provides:
|
|
90
|
+
# access_token — OpenIDConnect access token (lazy-initialized, memoized)
|
|
91
|
+
# store_id_token — persists id_token to session for RP-Initiated Logout
|
|
92
|
+
#
|
|
93
|
+
# Verify provides:
|
|
94
|
+
# user_info — merged UserInfo from access token + id_token claims
|
|
95
|
+
# decoded_id_token — decoded and verified JWT (attr_reader)
|
|
96
|
+
# verify_id_token! — verifies id_token signature, issuer, nonce
|
|
97
|
+
# decode_id_token — decodes JWT, sets @decoded_id_token
|
|
98
|
+
# secret, public_key — signing key resolution
|
|
99
|
+
#
|
|
100
|
+
# Serializer reads:
|
|
101
|
+
# access_token, user_info, decoded_id_token (via id_token_raw_attributes)
|
|
102
|
+
#
|
|
103
|
+
# Oidc (this class) provides to all modules:
|
|
104
|
+
# client, config, client_options, scope, session, params, options,
|
|
105
|
+
# redirect_uri, stored_state, new_nonce, host, issuer
|
|
106
|
+
|
|
107
|
+
SECURITY_HEADERS = {
|
|
108
|
+
"Cache-Control" => "no-cache, no-store, must-revalidate",
|
|
109
|
+
"Pragma" => "no-cache",
|
|
110
|
+
"Referrer-Policy" => "no-referrer"
|
|
111
|
+
}.freeze
|
|
112
|
+
|
|
113
|
+
def redirect(uri)
|
|
114
|
+
response = super
|
|
115
|
+
SECURITY_HEADERS.each { |k, v| response[1][k] = v }
|
|
116
|
+
response
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def uid
|
|
120
|
+
user_info.raw_attributes[options.uid_field.to_sym] || user_info.sub
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
info { serialized_user_info }
|
|
124
|
+
|
|
125
|
+
extra { serialized_extra }
|
|
126
|
+
|
|
127
|
+
credentials { serialized_credentials }
|
|
128
|
+
|
|
129
|
+
# Initialize OpenIDConnect Client with options
|
|
130
|
+
def client
|
|
131
|
+
@client ||= ::OpenIDConnect::Client.new(client_options)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Returns an OAuth2::Client (from the oauth2 gem) configured with the
|
|
135
|
+
# discovered token endpoint. Useful for token refresh flows where
|
|
136
|
+
# OAuth2::AccessToken requires an OAuth2::Client rather than the
|
|
137
|
+
# OpenIDConnect/Rack::OAuth2 client returned by #client.
|
|
138
|
+
def oauth2_client
|
|
139
|
+
@oauth2_client ||= ::OAuth2::Client.new(
|
|
140
|
+
client_options.identifier,
|
|
141
|
+
client_options.secret,
|
|
142
|
+
site: config.issuer,
|
|
143
|
+
token_url: config.token_endpoint
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Config is built from the JSON response from the OIDC config endpoint
|
|
148
|
+
def config
|
|
149
|
+
unless client_options.config_endpoint || params["config_endpoint"]
|
|
150
|
+
raise Error,
|
|
151
|
+
"Configuration endpoint is missing from options"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
@config ||= OmniauthOidc::Config.fetch(client_options.config_endpoint)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Detects if current request is for the logout url and makes a redirect to end session with OIDC provider
|
|
158
|
+
def other_phase
|
|
159
|
+
if logout_path_pattern.match?(request.url)
|
|
160
|
+
options.issuer = issuer if options.issuer.to_s.empty?
|
|
161
|
+
|
|
162
|
+
return redirect(end_session_uri) if end_session_uri
|
|
163
|
+
end
|
|
164
|
+
call_app!
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# URL to end authenticated user's session with OIDC provider
|
|
168
|
+
def end_session_uri
|
|
169
|
+
return unless end_session_endpoint_is_valid?
|
|
170
|
+
|
|
171
|
+
end_session = URI(client_options.end_session_endpoint)
|
|
172
|
+
end_session_params = {}
|
|
173
|
+
end_session_params[:post_logout_redirect_uri] = options.post_logout_redirect_uri if options.post_logout_redirect_uri
|
|
174
|
+
end_session_params[:id_token_hint] = session["omniauth.id_token"] if session["omniauth.id_token"]
|
|
175
|
+
end_session.query = URI.encode_www_form(end_session_params) unless end_session_params.empty?
|
|
176
|
+
end_session.to_s
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
def issuer
|
|
182
|
+
@issuer ||= config.issuer
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def host
|
|
186
|
+
@host ||= URI.parse(config.issuer).host
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# get scope list from options or provider config defaults
|
|
190
|
+
def scope
|
|
191
|
+
options.scope || config.scopes_supported
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def authorization_code
|
|
195
|
+
params["code"]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def client_options
|
|
199
|
+
options.client_options
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def stored_state
|
|
203
|
+
session.delete("omniauth.state")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def new_nonce
|
|
207
|
+
session["omniauth.nonce"] = SecureRandom.hex(16)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def script_name
|
|
211
|
+
return "" if @env.nil?
|
|
212
|
+
|
|
213
|
+
super
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def session
|
|
217
|
+
return {} if @env.nil?
|
|
218
|
+
|
|
219
|
+
super
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def redirect_uri
|
|
223
|
+
options.redirect_uri || full_host + callback_path
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Configure OIDC discovery endpoints on a target object (client_options or client).
|
|
227
|
+
# Called by both Request and Callback phases to avoid duplication.
|
|
228
|
+
def configure_discovery_endpoints(target)
|
|
229
|
+
target.host = host
|
|
230
|
+
target.authorization_endpoint = config.authorization_endpoint
|
|
231
|
+
target.token_endpoint = config.token_endpoint
|
|
232
|
+
target.userinfo_endpoint = config.userinfo_endpoint
|
|
233
|
+
|
|
234
|
+
if target.respond_to?(:jwks_uri=)
|
|
235
|
+
target.jwks_uri = config.jwks_uri
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
if config.end_session_endpoint && target.respond_to?(:end_session_endpoint=)
|
|
239
|
+
target.end_session_endpoint = config.end_session_endpoint
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def end_session_endpoint_is_valid?
|
|
244
|
+
client_options.end_session_endpoint &&
|
|
245
|
+
client_options.end_session_endpoint.match?(URI::RFC2396_PARSER.make_regexp)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def logout_path_pattern
|
|
249
|
+
@logout_path_pattern ||= /\A#{Regexp.quote(request.base_url)}#{options.logout_path}/
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Override for the CallbackError class
|
|
253
|
+
class CallbackError < StandardError
|
|
254
|
+
attr_accessor :error, :error_reason, :error_uri
|
|
255
|
+
|
|
256
|
+
def initialize(data)
|
|
257
|
+
super
|
|
258
|
+
self.error = data[:error]
|
|
259
|
+
self.error_reason = data[:reason]
|
|
260
|
+
self.error_uri = data[:uri]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def message
|
|
264
|
+
[ error, error_reason, error_uri ].compact.join(" | ")
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
OmniAuth.config.add_camelization "OmniauthOidc", "OmniAuthOidc"
|
|
@@ -0,0 +1,43 @@
|
|
|
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-strategy"
|
|
7
|
+
spec.version = OmniauthOidc::VERSION
|
|
8
|
+
spec.authors = [ 'mc' ]
|
|
9
|
+
spec.email = [ 'test@example.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/CanalWestStudio/omniauth-oidc"
|
|
15
|
+
spec.license = "MIT"
|
|
16
|
+
spec.required_ruby_version = ">= 3.1.0"
|
|
17
|
+
|
|
18
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
19
|
+
spec.metadata["source_code_uri"] = "https://github.com/CanalWestStudio/omniauth-oidc"
|
|
20
|
+
spec.metadata["changelog_uri"] = "https://github.com/CanalWestStudio/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
|
+
spec.add_dependency "faraday", "~> 2.0"
|
|
35
|
+
spec.add_dependency "faraday-net_http_persistent", "~> 2.0"
|
|
36
|
+
spec.add_dependency "faraday-retry", "~> 2.0"
|
|
37
|
+
spec.add_dependency "oauth2", ">= 1.4"
|
|
38
|
+
spec.add_dependency "omniauth"
|
|
39
|
+
spec.add_dependency "openid_connect"
|
|
40
|
+
|
|
41
|
+
# For more information and examples about making a new gem, check out our
|
|
42
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
|
43
|
+
end
|