better_auth-sso 0.2.0 → 0.5.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/lib/better_auth/plugins/sso.rb +305 -73
- data/lib/better_auth/sso/client.rb +31 -0
- data/lib/better_auth/sso/constants.rb +20 -0
- data/lib/better_auth/sso/domain_verification.rb +17 -0
- data/lib/better_auth/sso/linking/org_assignment.rb +118 -0
- data/lib/better_auth/sso/linking/types.rb +52 -0
- data/lib/better_auth/sso/linking.rb +24 -0
- data/lib/better_auth/sso/oidc/discovery.rb +259 -0
- data/lib/better_auth/sso/oidc/errors.rb +27 -0
- data/lib/better_auth/sso/oidc/types.rb +29 -0
- data/lib/better_auth/sso/oidc.rb +20 -0
- data/lib/better_auth/sso/routes/domain_verification.rb +19 -0
- data/lib/better_auth/sso/routes/helpers.rb +19 -0
- data/lib/better_auth/sso/routes/providers.rb +19 -0
- data/lib/better_auth/sso/routes/saml_pipeline.rb +19 -0
- data/lib/better_auth/sso/routes/schemas.rb +77 -0
- data/lib/better_auth/sso/routes/sso.rb +43 -0
- data/lib/better_auth/sso/saml/algorithms.rb +96 -0
- data/lib/better_auth/sso/saml/assertions.rb +21 -0
- data/lib/better_auth/sso/saml/error_codes.rb +24 -0
- data/lib/better_auth/sso/saml/parser.rb +19 -0
- data/lib/better_auth/sso/saml/timestamp.rb +19 -0
- data/lib/better_auth/sso/saml.rb +24 -4
- data/lib/better_auth/sso/saml_state.rb +30 -0
- data/lib/better_auth/sso/types.rb +20 -0
- data/lib/better_auth/sso/utils.rb +55 -0
- data/lib/better_auth/sso/version.rb +2 -1
- data/lib/better_auth/sso.rb +24 -0
- metadata +45 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ebd55984df929b46f41984bb799d40ae89018e02189619a60485316d6c1943b4
|
|
4
|
+
data.tar.gz: e5f7aa9680058c33c2413f08385fcf73ab3c10ece70c06e999dda40a24676366
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6cad2fc9ab83b28280f93f2d2a521ec9dcc12854f0000f5797674fdcdae285f28d7435bc6dbaee92bb9e1f47bf6573ba3e527bf87e3e5a221cf5578eab1f245a
|
|
7
|
+
data.tar.gz: 7f4b8739cac30c78ba7eaffd7e097037d6c062a92ef777f98eee039072f9efa2b098b3b2f35d81c8bda04646ce220d9566c0e5709988bb59c823980ac5d7f435
|
|
@@ -6,6 +6,7 @@ require "json"
|
|
|
6
6
|
require "jwt"
|
|
7
7
|
require "net/http"
|
|
8
8
|
require "openssl"
|
|
9
|
+
require "rexml/document"
|
|
9
10
|
require "resolv"
|
|
10
11
|
require "securerandom"
|
|
11
12
|
require "time"
|
|
@@ -67,6 +68,7 @@ module BetterAuth
|
|
|
67
68
|
].freeze
|
|
68
69
|
SSO_DEFAULT_MAX_SAML_RESPONSE_SIZE = 256 * 1024
|
|
69
70
|
SSO_DEFAULT_MAX_SAML_METADATA_SIZE = 100 * 1024
|
|
71
|
+
SSO_SAML_RELAY_STATE_KEY_PREFIX = "saml-relay-state:"
|
|
70
72
|
SSO_SAML_AUTHN_REQUEST_KEY_PREFIX = "saml-authn-request:"
|
|
71
73
|
SSO_DEFAULT_AUTHN_REQUEST_TTL_MS = 5 * 60 * 1000
|
|
72
74
|
SSO_SAML_USED_ASSERTION_KEY_PREFIX = "saml-used-assertion:"
|
|
@@ -83,29 +85,11 @@ module BetterAuth
|
|
|
83
85
|
if defined?(BetterAuth::SSO::SAML) && defined?(BetterAuth::SSO::SAMLHooks)
|
|
84
86
|
config = BetterAuth::SSO::SAMLHooks.merge_options(BetterAuth::SSO::SAML.sso_options, config)
|
|
85
87
|
end
|
|
86
|
-
endpoints =
|
|
87
|
-
sp_metadata: sso_sp_metadata_endpoint(config),
|
|
88
|
-
register_sso_provider: sso_register_provider_endpoint(config),
|
|
89
|
-
sign_in_sso: sso_sign_in_endpoint(config),
|
|
90
|
-
callback_sso: sso_oidc_callback_endpoint(config),
|
|
91
|
-
callback_sso_shared: sso_oidc_shared_callback_endpoint(config),
|
|
92
|
-
callback_sso_saml: sso_saml_callback_endpoint(config),
|
|
93
|
-
acs_endpoint: sso_saml_acs_endpoint(config),
|
|
94
|
-
slo_endpoint: sso_saml_slo_endpoint(config),
|
|
95
|
-
initiate_slo: sso_initiate_slo_endpoint(config),
|
|
96
|
-
list_sso_providers: sso_list_providers_endpoint,
|
|
97
|
-
get_sso_provider: sso_get_provider_endpoint,
|
|
98
|
-
update_sso_provider: sso_update_provider_endpoint,
|
|
99
|
-
delete_sso_provider: sso_delete_provider_endpoint
|
|
100
|
-
}
|
|
101
|
-
if config.dig(:domain_verification, :enabled)
|
|
102
|
-
endpoints[:request_domain_verification] = sso_request_domain_verification_endpoint(config)
|
|
103
|
-
endpoints[:verify_domain] = sso_verify_domain_endpoint(config)
|
|
104
|
-
end
|
|
88
|
+
endpoints = BetterAuth::SSO::Routes::SSO.endpoints(config)
|
|
105
89
|
Plugin.new(
|
|
106
90
|
id: "sso",
|
|
107
91
|
init: ->(_ctx) { {options: {advanced: {disable_origin_check: ["/sso/saml2/callback", "/sso/saml2/sp/acs", "/sso/saml2/sp/slo"]}}} },
|
|
108
|
-
schema:
|
|
92
|
+
schema: BetterAuth::SSO::Routes::Schemas.plugin_schema(config),
|
|
109
93
|
endpoints: endpoints,
|
|
110
94
|
error_codes: SSO_ERROR_CODES,
|
|
111
95
|
options: config
|
|
@@ -113,24 +97,7 @@ module BetterAuth
|
|
|
113
97
|
end
|
|
114
98
|
|
|
115
99
|
def sso_schema(config = {})
|
|
116
|
-
|
|
117
|
-
issuer: {type: "string", required: true},
|
|
118
|
-
oidcConfig: {type: "string", required: false},
|
|
119
|
-
samlConfig: {type: "string", required: false},
|
|
120
|
-
userId: {type: "string", required: true},
|
|
121
|
-
providerId: {type: "string", required: true, unique: true},
|
|
122
|
-
domain: {type: "string", required: true},
|
|
123
|
-
organizationId: {type: "string", required: false}
|
|
124
|
-
}
|
|
125
|
-
if config.dig(:domain_verification, :enabled)
|
|
126
|
-
fields[:domainVerified] = {type: "boolean", required: false, default_value: false}
|
|
127
|
-
end
|
|
128
|
-
{
|
|
129
|
-
ssoProvider: {
|
|
130
|
-
model_name: config[:model_name] || "ssoProviders",
|
|
131
|
-
fields: fields
|
|
132
|
-
}
|
|
133
|
-
}
|
|
100
|
+
BetterAuth::SSO::Routes::Schemas.plugin_schema(config)
|
|
134
101
|
end
|
|
135
102
|
|
|
136
103
|
def sso_discover_oidc_config(issuer:, fetch: nil, existing_config: nil, discovery_endpoint: nil, trusted_origin: nil, timeout: nil)
|
|
@@ -242,8 +209,18 @@ module BetterAuth
|
|
|
242
209
|
domainVerified: false
|
|
243
210
|
}
|
|
244
211
|
)
|
|
212
|
+
domain_verification_token = nil
|
|
213
|
+
if config.dig(:domain_verification, :enabled)
|
|
214
|
+
domain_verification_token = BetterAuth::Crypto.random_string(24)
|
|
215
|
+
ctx.context.internal_adapter.create_verification_value(
|
|
216
|
+
identifier: sso_domain_verification_identifier(config, provider.fetch("providerId")),
|
|
217
|
+
value: domain_verification_token,
|
|
218
|
+
expiresAt: Time.now + (7 * 24 * 60 * 60)
|
|
219
|
+
)
|
|
220
|
+
end
|
|
245
221
|
response = sso_sanitize_provider(provider, ctx.context)
|
|
246
222
|
response[:redirectURI] = sso_oidc_redirect_uri(ctx.context, provider.fetch("providerId"))
|
|
223
|
+
response[:domainVerificationToken] = domain_verification_token if domain_verification_token
|
|
247
224
|
ctx.json(response)
|
|
248
225
|
end
|
|
249
226
|
end
|
|
@@ -284,13 +261,13 @@ module BetterAuth
|
|
|
284
261
|
update[:domain] = body[:domain].to_s.downcase if body.key?(:domain)
|
|
285
262
|
update[:domainVerified] = false if body.key?(:domain) && body[:domain].to_s.downcase != provider["domain"].to_s
|
|
286
263
|
if body.key?(:oidc_config)
|
|
287
|
-
current =
|
|
264
|
+
current = sso_provider_config_hash(provider["oidcConfig"])
|
|
288
265
|
raise APIError.new("BAD_REQUEST", message: "Cannot update OIDC config for a provider that doesn't have OIDC configured") if current.empty?
|
|
289
266
|
|
|
290
267
|
update[:oidcConfig] = current.merge(normalize_hash(body[:oidc_config]))
|
|
291
268
|
end
|
|
292
269
|
if body.key?(:saml_config)
|
|
293
|
-
current =
|
|
270
|
+
current = sso_provider_config_hash(provider["samlConfig"])
|
|
294
271
|
raise APIError.new("BAD_REQUEST", message: "Cannot update SAML config for a provider that doesn't have SAML configured") if current.empty?
|
|
295
272
|
|
|
296
273
|
update[:samlConfig] = current.merge(normalize_hash(body[:saml_config]))
|
|
@@ -339,7 +316,7 @@ module BetterAuth
|
|
|
339
316
|
state = BetterAuth::Crypto.sign_jwt(state_data.merge(sso_oidc_pkce_state(provider)), ctx.context.secret, expires_in: 600)
|
|
340
317
|
url = sso_oidc_authorization_url(provider, ctx, state, config, body)
|
|
341
318
|
elsif provider["samlConfig"]
|
|
342
|
-
relay_state =
|
|
319
|
+
relay_state = sso_generate_saml_relay_state(ctx, state_data)
|
|
343
320
|
url = sso_saml_authorization_url(provider, relay_state, ctx, config)
|
|
344
321
|
sso_store_saml_authn_request(ctx, provider, url, config)
|
|
345
322
|
else
|
|
@@ -459,12 +436,14 @@ module BetterAuth
|
|
|
459
436
|
end
|
|
460
437
|
|
|
461
438
|
sso_process_saml_logout_request(ctx, provider, sso_fetch(ctx.body, :saml_request) || sso_fetch(ctx.query, :saml_request))
|
|
462
|
-
response = Base64.strict_encode64("<samlp:LogoutResponse xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\"_#{
|
|
439
|
+
response = Base64.strict_encode64("<samlp:LogoutResponse xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\"_#{BetterAuth::Crypto.random_string(32)}\" Version=\"2.0\" IssueInstant=\"#{Time.now.utc.iso8601}\" Destination=\"#{sso_saml_logout_destination(provider)}\"><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/></samlp:Status></samlp:LogoutResponse>")
|
|
463
440
|
if sso_fetch(ctx.body, :saml_request)
|
|
464
441
|
next sso_saml_post_form(sso_saml_logout_destination(provider), "SAMLResponse", response, relay_state)
|
|
465
442
|
end
|
|
466
443
|
|
|
467
|
-
|
|
444
|
+
query = {SAMLResponse: response, RelayState: relay_state}
|
|
445
|
+
query = sso_signed_saml_redirect_query(provider, query) if config.dig(:saml, :want_logout_response_signed)
|
|
446
|
+
sso_redirect(ctx, "#{sso_saml_logout_destination(provider)}?#{URI.encode_www_form(query)}")
|
|
468
447
|
end
|
|
469
448
|
end
|
|
470
449
|
|
|
@@ -488,7 +467,7 @@ module BetterAuth
|
|
|
488
467
|
name_id = saml_record["nameId"] || user_email
|
|
489
468
|
session_index = saml_record["sessionIndex"]
|
|
490
469
|
|
|
491
|
-
request_id = "_#{
|
|
470
|
+
request_id = "_#{BetterAuth::Crypto.random_string(32)}"
|
|
492
471
|
session_index_xml = session_index.to_s.empty? ? "" : "<samlp:SessionIndex>#{CGI.escapeHTML(session_index.to_s)}</samlp:SessionIndex>"
|
|
493
472
|
request = Base64.strict_encode64("<samlp:LogoutRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"#{request_id}\" Version=\"2.0\" IssueInstant=\"#{Time.now.utc.iso8601}\" Destination=\"#{CGI.escapeHTML(destination.to_s)}\"><saml:NameID>#{CGI.escapeHTML(name_id.to_s)}</saml:NameID>#{session_index_xml}</samlp:LogoutRequest>")
|
|
494
473
|
sso_store_saml_logout_request(ctx, provider, request_id, config)
|
|
@@ -496,7 +475,9 @@ module BetterAuth
|
|
|
496
475
|
ctx.context.internal_adapter.delete_verification_by_identifier("#{SSO_SAML_SESSION_BY_ID_KEY_PREFIX}#{session_token}")
|
|
497
476
|
ctx.context.internal_adapter.delete_session(session_token)
|
|
498
477
|
Cookies.delete_session_cookie(ctx)
|
|
499
|
-
|
|
478
|
+
query = {SAMLRequest: request, RelayState: relay_state}
|
|
479
|
+
query = sso_signed_saml_redirect_query(provider, query) if config.dig(:saml, :want_logout_request_signed)
|
|
480
|
+
sso_redirect(ctx, "#{destination}?#{URI.encode_www_form(query)}")
|
|
500
481
|
end
|
|
501
482
|
end
|
|
502
483
|
|
|
@@ -515,7 +496,7 @@ module BetterAuth
|
|
|
515
496
|
next ctx.json({domainVerificationToken: active.fetch("value")}, status: 201)
|
|
516
497
|
end
|
|
517
498
|
|
|
518
|
-
token =
|
|
499
|
+
token = BetterAuth::Crypto.random_string(24)
|
|
519
500
|
ctx.context.internal_adapter.create_verification_value(identifier: identifier, value: token, expiresAt: Time.now + (7 * 24 * 60 * 60))
|
|
520
501
|
config.dig(:domain_verification, :request)&.call(provider: provider, token: token, context: ctx)
|
|
521
502
|
ctx.json({domainVerificationToken: token}, status: 201)
|
|
@@ -559,8 +540,16 @@ module BetterAuth
|
|
|
559
540
|
def sso_handle_saml_response(ctx, config = {})
|
|
560
541
|
provider = sso_find_provider!(ctx, sso_fetch(ctx.params, :provider_id))
|
|
561
542
|
relay_state = sso_fetch(ctx.body, :relay_state) || sso_fetch(ctx.query, :relay_state)
|
|
562
|
-
state =
|
|
563
|
-
raw_response = sso_fetch(ctx.body, :saml_response)
|
|
543
|
+
state = sso_parse_saml_relay_state(ctx, relay_state) || {}
|
|
544
|
+
raw_response = sso_fetch(ctx.body, :saml_response) || sso_fetch(ctx.query, :saml_response)
|
|
545
|
+
if ctx.method == "GET" && raw_response.to_s.empty?
|
|
546
|
+
session = Routes.current_session(ctx, allow_nil: true)
|
|
547
|
+
unless session
|
|
548
|
+
return sso_redirect(ctx, sso_append_error("#{ctx.context.base_url}/error", "invalid_request"))
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
return sso_redirect(ctx, sso_safe_saml_callback_url(ctx, relay_state || sso_saml_callback_url(provider) || "/", provider.fetch("providerId")))
|
|
552
|
+
end
|
|
564
553
|
max_response_size = config.dig(:saml, :max_response_size) || SSO_DEFAULT_MAX_SAML_RESPONSE_SIZE
|
|
565
554
|
if raw_response.to_s.bytesize > max_response_size
|
|
566
555
|
raise APIError.new("BAD_REQUEST", message: "SAML response exceeds maximum allowed size (#{max_response_size} bytes)")
|
|
@@ -579,14 +568,15 @@ module BetterAuth
|
|
|
579
568
|
end
|
|
580
569
|
ctx.context.internal_adapter.create_verification_value(identifier: replay_key, value: "used", expiresAt: sso_saml_assertion_replay_expires_at(assertion, config))
|
|
581
570
|
|
|
582
|
-
callback_url = state["callbackURL"] || "/"
|
|
583
|
-
callback_url = "/" unless ctx.context.trusted_origin?(callback_url, allow_relative_paths: true)
|
|
571
|
+
callback_url = sso_safe_saml_callback_url(ctx, state["callbackURL"] || sso_saml_callback_url(provider) || "/", provider.fetch("providerId"))
|
|
584
572
|
email = (assertion[:email] || assertion["email"]).to_s.downcase
|
|
585
573
|
if config[:disable_implicit_sign_up] && !state["requestSignUp"] && !ctx.context.internal_adapter.find_user_by_email(email)
|
|
586
574
|
return sso_redirect(ctx, sso_append_error(callback_url, "signup disabled"))
|
|
587
575
|
end
|
|
588
576
|
|
|
589
577
|
result = sso_find_or_create_user_result(ctx, provider, assertion, config)
|
|
578
|
+
return sso_redirect(ctx, sso_append_error(callback_url, result.fetch(:error))) if result[:error]
|
|
579
|
+
|
|
590
580
|
user = result.fetch(:user)
|
|
591
581
|
if config[:provision_user].respond_to?(:call) && (result.fetch(:created) || config[:provision_user_on_every_login])
|
|
592
582
|
config[:provision_user].call(user: user, userInfo: assertion, provider: provider)
|
|
@@ -604,9 +594,32 @@ module BetterAuth
|
|
|
604
594
|
def sso_find_or_create_user_result(ctx, provider, user_info, config = {})
|
|
605
595
|
user_info = normalize_hash(user_info)
|
|
606
596
|
email = user_info[:email].to_s.downcase
|
|
607
|
-
|
|
608
|
-
|
|
597
|
+
account_id = (user_info[:id] || user_info["id"]).to_s
|
|
598
|
+
provider_id = provider.fetch("providerId")
|
|
599
|
+
storage_provider_id = provider["samlConfig"] ? provider_id : "sso:#{provider_id}"
|
|
600
|
+
existing_account = account_id.empty? ? nil : (
|
|
601
|
+
ctx.context.internal_adapter.find_account_by_provider_id(account_id, provider_id) ||
|
|
602
|
+
ctx.context.internal_adapter.find_account_by_provider_id(account_id, "sso:#{provider_id}")
|
|
603
|
+
)
|
|
604
|
+
if existing_account
|
|
605
|
+
user = ctx.context.internal_adapter.find_user_by_id(existing_account.fetch("userId"))
|
|
606
|
+
created = false
|
|
607
|
+
elsif (found = ctx.context.internal_adapter.find_user_by_email(email, include_accounts: true))
|
|
608
|
+
already_linked_provider = Array(found[:accounts]).any? do |account|
|
|
609
|
+
[provider_id, "sso:#{provider_id}"].include?(account["providerId"])
|
|
610
|
+
end
|
|
611
|
+
if provider["samlConfig"]
|
|
612
|
+
return {error: "account_not_linked"} unless already_linked_provider || sso_saml_trusted_provider?(ctx, provider, email)
|
|
613
|
+
end
|
|
614
|
+
|
|
609
615
|
user = found[:user]
|
|
616
|
+
unless account_id.empty?
|
|
617
|
+
ctx.context.internal_adapter.create_account(
|
|
618
|
+
accountId: account_id,
|
|
619
|
+
providerId: storage_provider_id,
|
|
620
|
+
userId: user.fetch("id")
|
|
621
|
+
)
|
|
622
|
+
end
|
|
610
623
|
oidc_config = normalize_hash(provider["oidcConfig"] || {})
|
|
611
624
|
if oidc_config[:override_user_info] || config[:default_override_user_info]
|
|
612
625
|
update = {}
|
|
@@ -624,8 +637,8 @@ module BetterAuth
|
|
|
624
637
|
image: user_info[:image]
|
|
625
638
|
)
|
|
626
639
|
ctx.context.internal_adapter.create_account(
|
|
627
|
-
accountId:
|
|
628
|
-
providerId:
|
|
640
|
+
accountId: account_id.empty? ? created.fetch("id") : account_id,
|
|
641
|
+
providerId: storage_provider_id,
|
|
629
642
|
userId: created.fetch("id")
|
|
630
643
|
)
|
|
631
644
|
user = created
|
|
@@ -635,6 +648,15 @@ module BetterAuth
|
|
|
635
648
|
{user: user, created: created}
|
|
636
649
|
end
|
|
637
650
|
|
|
651
|
+
def sso_saml_trusted_provider?(ctx, provider, email)
|
|
652
|
+
provider_id = provider.fetch("providerId")
|
|
653
|
+
linking = ctx.context.options.account[:account_linking] || {}
|
|
654
|
+
return false if linking[:enabled] == false
|
|
655
|
+
|
|
656
|
+
trusted = Array(linking[:trusted_providers]).map(&:to_s).include?(provider_id.to_s)
|
|
657
|
+
trusted || (provider["domainVerified"] && sso_email_domain_matches?(email, provider["domain"]))
|
|
658
|
+
end
|
|
659
|
+
|
|
638
660
|
def sso_validate_saml_response!(config, assertion, provider, ctx)
|
|
639
661
|
validator = config.dig(:saml, :validate_response)
|
|
640
662
|
return unless validator.respond_to?(:call)
|
|
@@ -653,6 +675,10 @@ module BetterAuth
|
|
|
653
675
|
if saml_config[:entry_point].to_s.empty? && saml_config[:single_sign_on_service].to_s.empty? && metadata.to_s.empty?
|
|
654
676
|
raise APIError.new("BAD_REQUEST", message: "SAML config must include entryPoint, singleSignOnService, or IdP metadata")
|
|
655
677
|
end
|
|
678
|
+
sso_validate_url!(saml_config[:entry_point], "SAML entryPoint must be a valid URL") unless saml_config[:entry_point].to_s.empty?
|
|
679
|
+
unless saml_config[:single_sign_on_service].to_s.empty?
|
|
680
|
+
sso_validate_url!(saml_config[:single_sign_on_service], "SAML singleLogoutService must be a valid URL")
|
|
681
|
+
end
|
|
656
682
|
|
|
657
683
|
sso_validate_saml_algorithms!(
|
|
658
684
|
metadata.to_s,
|
|
@@ -667,16 +693,112 @@ module BetterAuth
|
|
|
667
693
|
def sso_sp_metadata_xml(ctx, provider, config = {})
|
|
668
694
|
provider_id = provider.fetch("providerId")
|
|
669
695
|
saml_config = normalize_hash(provider["samlConfig"] || {})
|
|
670
|
-
|
|
671
|
-
|
|
696
|
+
explicit_metadata = saml_config.dig(:sp_metadata, :metadata)
|
|
697
|
+
return explicit_metadata unless explicit_metadata.to_s.empty?
|
|
698
|
+
|
|
699
|
+
entity_id = saml_config.dig(:sp_metadata, :entity_id) || saml_config[:audience] || provider["issuer"] || "#{ctx.context.base_url}/sso/saml2/sp/metadata?providerId=#{URI.encode_www_form_component(provider_id)}"
|
|
700
|
+
acs_url = sso_saml_acs_url(ctx, provider)
|
|
672
701
|
authn_requests_signed = !!saml_config[:authn_requests_signed]
|
|
673
702
|
want_assertions_signed = saml_config.key?(:want_assertions_signed) ? !!saml_config[:want_assertions_signed] : true
|
|
703
|
+
name_id_format = saml_config[:identifier_format].to_s.empty? ? "" : "<NameIDFormat>#{saml_config[:identifier_format]}</NameIDFormat>"
|
|
674
704
|
slo = if config.dig(:saml, :enable_single_logout)
|
|
675
705
|
location = "#{ctx.context.base_url}/sso/saml2/sp/slo/#{URI.encode_www_form_component(provider_id)}"
|
|
676
706
|
"<SingleLogoutService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"#{location}\" /><SingleLogoutService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"#{location}\" />"
|
|
677
707
|
end
|
|
678
708
|
|
|
679
|
-
"<EntityDescriptor entityID=\"#{entity_id}\"><SPSSODescriptor AuthnRequestsSigned=\"#{authn_requests_signed}\" WantAssertionsSigned=\"#{want_assertions_signed}\">#{slo}<AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"#{acs_url}\" index=\"0\" /></SPSSODescriptor></EntityDescriptor>"
|
|
709
|
+
"<EntityDescriptor entityID=\"#{entity_id}\"><SPSSODescriptor AuthnRequestsSigned=\"#{authn_requests_signed}\" WantAssertionsSigned=\"#{want_assertions_signed}\">#{slo}#{name_id_format}<AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"#{acs_url}\" index=\"0\" /></SPSSODescriptor></EntityDescriptor>"
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
def sso_saml_acs_url(ctx, provider)
|
|
713
|
+
provider_id = provider.fetch("providerId")
|
|
714
|
+
base_url = ctx.context.base_url
|
|
715
|
+
configured = normalize_hash(provider["samlConfig"] || {})[:callback_url].to_s
|
|
716
|
+
return configured if sso_saml_acs_url?(configured)
|
|
717
|
+
|
|
718
|
+
"#{base_url}/sso/saml2/sp/acs/#{URI.encode_www_form_component(provider_id)}"
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
def sso_saml_acs_url?(url)
|
|
722
|
+
return false if url.to_s.empty?
|
|
723
|
+
|
|
724
|
+
URI.parse(url.to_s).path.include?("/sso/saml2/sp/acs")
|
|
725
|
+
rescue
|
|
726
|
+
false
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
def sso_saml_idp_metadata(provider_or_config)
|
|
730
|
+
saml_config = if provider_or_config.respond_to?(:key?) && (provider_or_config.key?("samlConfig") || provider_or_config.key?(:samlConfig))
|
|
731
|
+
normalize_hash(provider_or_config["samlConfig"] || provider_or_config[:samlConfig] || {})
|
|
732
|
+
else
|
|
733
|
+
normalize_hash(provider_or_config || {})
|
|
734
|
+
end
|
|
735
|
+
idp_metadata = normalize_hash(saml_config[:idp_metadata] || {})
|
|
736
|
+
xml = idp_metadata[:metadata] || saml_config[:metadata] || saml_config[:idp_metadata_xml]
|
|
737
|
+
parsed = xml.to_s.strip.empty? ? {} : sso_parse_saml_metadata_xml(xml)
|
|
738
|
+
parsed[:entity_id] ||= idp_metadata[:entity_id] || idp_metadata[:entityID] || saml_config[:issuer]
|
|
739
|
+
parsed[:cert] ||= idp_metadata[:cert] || saml_config[:cert]
|
|
740
|
+
parsed[:single_sign_on_service] = sso_saml_metadata_services_from_config(idp_metadata[:single_sign_on_service] || saml_config[:single_sign_on_service]) if parsed[:single_sign_on_service].to_a.empty?
|
|
741
|
+
parsed[:single_logout_service] = sso_saml_metadata_services_from_config(idp_metadata[:single_logout_service] || saml_config[:single_logout_service]) if parsed[:single_logout_service].to_a.empty?
|
|
742
|
+
parsed
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
def sso_parse_saml_metadata_xml(xml)
|
|
746
|
+
doc = REXML::Document.new(xml.to_s)
|
|
747
|
+
root = doc.root
|
|
748
|
+
{
|
|
749
|
+
entity_id: root&.attributes&.[]("entityID"),
|
|
750
|
+
cert: sso_saml_normalize_certificate(sso_saml_metadata_first_text(doc, "X509Certificate")),
|
|
751
|
+
single_sign_on_service: sso_saml_metadata_services(doc, "SingleSignOnService"),
|
|
752
|
+
single_logout_service: sso_saml_metadata_services(doc, "SingleLogoutService")
|
|
753
|
+
}.compact
|
|
754
|
+
rescue
|
|
755
|
+
{}
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
def sso_saml_metadata_services(doc, element_name)
|
|
759
|
+
services = []
|
|
760
|
+
REXML::XPath.each(doc, "//*") do |element|
|
|
761
|
+
next unless element.name == element_name
|
|
762
|
+
|
|
763
|
+
services << {
|
|
764
|
+
binding: element.attributes["Binding"],
|
|
765
|
+
location: element.attributes["Location"]
|
|
766
|
+
}.compact
|
|
767
|
+
end
|
|
768
|
+
services
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
def sso_saml_metadata_first_text(doc, element_name)
|
|
772
|
+
REXML::XPath.each(doc, "//*") do |element|
|
|
773
|
+
return element.text.to_s.strip if element.name == element_name && !element.text.to_s.strip.empty?
|
|
774
|
+
end
|
|
775
|
+
nil
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
def sso_saml_metadata_services_from_config(value)
|
|
779
|
+
Array(value).filter_map do |entry|
|
|
780
|
+
data = normalize_hash(entry || {})
|
|
781
|
+
next if data[:location].to_s.empty?
|
|
782
|
+
|
|
783
|
+
{binding: data[:binding] || data[:Binding], location: data[:location] || data[:Location]}.compact
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
def sso_saml_preferred_service(services)
|
|
788
|
+
Array(services).find { |service| normalize_hash(service)[:binding].to_s.include?("HTTP-Redirect") } || Array(services).first
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
def sso_saml_normalize_certificate(value)
|
|
792
|
+
cert = value.to_s.strip
|
|
793
|
+
return nil if cert.empty?
|
|
794
|
+
return cert if cert.include?("BEGIN CERTIFICATE")
|
|
795
|
+
|
|
796
|
+
"-----BEGIN CERTIFICATE-----\n#{cert.scan(/.{1,64}/).join("\n")}\n-----END CERTIFICATE-----"
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
def sso_saml_callback_url(provider)
|
|
800
|
+
saml_config = normalize_hash(provider["samlConfig"] || {})
|
|
801
|
+
saml_config[:callback_url]
|
|
680
802
|
end
|
|
681
803
|
|
|
682
804
|
def sso_saml_logout_destination(provider)
|
|
@@ -687,14 +809,8 @@ module BetterAuth
|
|
|
687
809
|
saml_config[:logout_url]
|
|
688
810
|
return direct unless direct.to_s.empty?
|
|
689
811
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
structured = structured.first if structured.is_a?(Array)
|
|
693
|
-
structured = normalize_hash(structured) if structured.is_a?(Hash)
|
|
694
|
-
return structured[:location] if structured.is_a?(Hash) && !structured[:location].to_s.empty?
|
|
695
|
-
|
|
696
|
-
metadata = idp_metadata[:metadata] || saml_config[:metadata] || saml_config[:idp_metadata_xml]
|
|
697
|
-
metadata.to_s[/<[^>]*SingleLogoutService\b[^>]*\bLocation=['"]([^'"]+)['"]/, 1]
|
|
812
|
+
service = sso_saml_preferred_service(sso_saml_idp_metadata(saml_config)[:single_logout_service])
|
|
813
|
+
normalize_hash(service || {})[:location]
|
|
698
814
|
end
|
|
699
815
|
|
|
700
816
|
def sso_store_saml_session(ctx, provider, assertion, session)
|
|
@@ -806,6 +922,55 @@ module BetterAuth
|
|
|
806
922
|
app_origin
|
|
807
923
|
end
|
|
808
924
|
|
|
925
|
+
def sso_safe_saml_callback_url(ctx, url, provider_id)
|
|
926
|
+
app_origin = ctx.context.base_url
|
|
927
|
+
callback_path = URI.parse("#{ctx.context.base_url}/sso/saml2/callback/#{URI.encode_www_form_component(provider_id)}").path
|
|
928
|
+
acs_path = URI.parse("#{ctx.context.base_url}/sso/saml2/sp/acs/#{URI.encode_www_form_component(provider_id)}").path
|
|
929
|
+
value = url.to_s
|
|
930
|
+
return app_origin if value.empty?
|
|
931
|
+
|
|
932
|
+
if value.start_with?("/") && !value.start_with?("//")
|
|
933
|
+
parsed = URI.parse(value)
|
|
934
|
+
return app_origin if [callback_path, acs_path].include?(parsed.path)
|
|
935
|
+
return value
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
return app_origin unless ctx.context.trusted_origin?(value, allow_relative_paths: false)
|
|
939
|
+
|
|
940
|
+
parsed = URI.parse(value)
|
|
941
|
+
return app_origin if [callback_path, acs_path].include?(parsed.path)
|
|
942
|
+
|
|
943
|
+
value
|
|
944
|
+
rescue
|
|
945
|
+
app_origin
|
|
946
|
+
end
|
|
947
|
+
|
|
948
|
+
def sso_signed_saml_redirect_query(provider, query)
|
|
949
|
+
saml_config = normalize_hash(provider["samlConfig"] || {})
|
|
950
|
+
private_key = saml_config.dig(:sp_metadata, :private_key) || saml_config[:private_key] || saml_config[:sp_private_key]
|
|
951
|
+
raise APIError.new("BAD_REQUEST", message: "SAML Redirect signing requires privateKey") if private_key.to_s.empty?
|
|
952
|
+
|
|
953
|
+
sig_alg = saml_config[:signature_algorithm] ? sso_normalize_saml_signature_algorithm(saml_config[:signature_algorithm]) : XMLSecurity::Document::RSA_SHA256
|
|
954
|
+
signed = query.compact.merge(SigAlg: sig_alg)
|
|
955
|
+
signed_payload = signed.keys.map(&:to_s).select { |key| %w[SAMLRequest SAMLResponse RelayState SigAlg].include?(key) }.map { |key| [key, signed[key.to_sym] || signed[key]] }.reject { |_key, value| value.nil? }
|
|
956
|
+
signature_input = URI.encode_www_form(signed_payload)
|
|
957
|
+
signed[:Signature] = Base64.strict_encode64(OpenSSL::PKey.read(private_key).sign(sso_saml_signature_digest(sig_alg), signature_input))
|
|
958
|
+
signed
|
|
959
|
+
end
|
|
960
|
+
|
|
961
|
+
def sso_saml_signature_digest(signature_algorithm)
|
|
962
|
+
case signature_algorithm.to_s
|
|
963
|
+
when /sha512/i
|
|
964
|
+
OpenSSL::Digest.new("SHA512")
|
|
965
|
+
when /sha384/i
|
|
966
|
+
OpenSSL::Digest.new("SHA384")
|
|
967
|
+
when /sha1/i
|
|
968
|
+
OpenSSL::Digest.new("SHA1")
|
|
969
|
+
else
|
|
970
|
+
OpenSSL::Digest.new("SHA256")
|
|
971
|
+
end
|
|
972
|
+
end
|
|
973
|
+
|
|
809
974
|
def sso_saml_post_form(action, saml_param, saml_value, relay_state = nil)
|
|
810
975
|
relay_input = relay_state.to_s.empty? ? "" : "<input type=\"hidden\" name=\"RelayState\" value=\"#{CGI.escapeHTML(relay_state.to_s)}\" />"
|
|
811
976
|
html = "<!DOCTYPE html><html><body onload=\"document.forms[0].submit();\"><form method=\"POST\" action=\"#{CGI.escapeHTML(action.to_s)}\"><input type=\"hidden\" name=\"#{CGI.escapeHTML(saml_param.to_s)}\" value=\"#{CGI.escapeHTML(saml_value.to_s)}\" />#{relay_input}<noscript><input type=\"submit\" value=\"Continue\" /></noscript></form></body></html>"
|
|
@@ -970,6 +1135,39 @@ module BetterAuth
|
|
|
970
1135
|
end
|
|
971
1136
|
end
|
|
972
1137
|
|
|
1138
|
+
def sso_generate_saml_relay_state(ctx, state_data)
|
|
1139
|
+
ttl_ms = 10 * 60 * 1000
|
|
1140
|
+
relay_state = BetterAuth::Crypto.random_string(32)
|
|
1141
|
+
now_ms = (Time.now.to_f * 1000).to_i
|
|
1142
|
+
stored = state_data.each_with_object({}) { |(key, value), result| result[key.to_s] = value }.merge(
|
|
1143
|
+
"codeVerifier" => BetterAuth::Crypto.random_string(128),
|
|
1144
|
+
"expiresAt" => now_ms + ttl_ms
|
|
1145
|
+
)
|
|
1146
|
+
ctx.context.internal_adapter.create_verification_value(
|
|
1147
|
+
identifier: "#{SSO_SAML_RELAY_STATE_KEY_PREFIX}#{relay_state}",
|
|
1148
|
+
value: JSON.generate(stored),
|
|
1149
|
+
expiresAt: Time.at((now_ms + ttl_ms) / 1000.0)
|
|
1150
|
+
)
|
|
1151
|
+
ctx.set_signed_cookie("relay_state", relay_state, ctx.context.secret, path: "/", max_age: ttl_ms / 1000, http_only: true, same_site: "lax")
|
|
1152
|
+
relay_state
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1155
|
+
def sso_parse_saml_relay_state(ctx, relay_state)
|
|
1156
|
+
state = sso_verify_state(relay_state, ctx.context.secret)
|
|
1157
|
+
return state if state
|
|
1158
|
+
|
|
1159
|
+
verification = ctx.context.internal_adapter.find_verification_value("#{SSO_SAML_RELAY_STATE_KEY_PREFIX}#{relay_state}")
|
|
1160
|
+
return nil unless verification
|
|
1161
|
+
return nil unless sso_future_time?(verification.fetch("expiresAt"))
|
|
1162
|
+
|
|
1163
|
+
parsed = JSON.parse(verification.fetch("value"))
|
|
1164
|
+
return nil if parsed["expiresAt"].to_i <= (Time.now.to_f * 1000).to_i
|
|
1165
|
+
|
|
1166
|
+
parsed
|
|
1167
|
+
rescue
|
|
1168
|
+
nil
|
|
1169
|
+
end
|
|
1170
|
+
|
|
973
1171
|
def sso_verify_state(value, secret)
|
|
974
1172
|
BetterAuth::Crypto.verify_jwt(value.to_s, secret)
|
|
975
1173
|
rescue
|
|
@@ -1006,11 +1204,13 @@ module BetterAuth
|
|
|
1006
1204
|
end
|
|
1007
1205
|
|
|
1008
1206
|
config = normalize_hash(provider["samlConfig"] || {})
|
|
1207
|
+
metadata = sso_saml_idp_metadata(config)
|
|
1208
|
+
entry_point = config[:entry_point] || normalize_hash(sso_saml_preferred_service(metadata[:single_sign_on_service]) || {})[:location]
|
|
1009
1209
|
query = {
|
|
1010
1210
|
SAMLRequest: Base64.strict_encode64(JSON.generate({providerId: provider.fetch("providerId")})),
|
|
1011
1211
|
RelayState: relay_state
|
|
1012
1212
|
}
|
|
1013
|
-
"#{
|
|
1213
|
+
"#{entry_point}?#{URI.encode_www_form(query)}"
|
|
1014
1214
|
end
|
|
1015
1215
|
|
|
1016
1216
|
def sso_store_saml_authn_request(ctx, provider, url, config)
|
|
@@ -1300,7 +1500,7 @@ module BetterAuth
|
|
|
1300
1500
|
def sso_oidc_pkce_state(provider)
|
|
1301
1501
|
return {} unless normalize_hash(provider["oidcConfig"] || {})[:pkce]
|
|
1302
1502
|
|
|
1303
|
-
{codeVerifier:
|
|
1503
|
+
{codeVerifier: BetterAuth::Crypto.random_string(128)}
|
|
1304
1504
|
end
|
|
1305
1505
|
|
|
1306
1506
|
def sso_decode_state(state, secret)
|
|
@@ -1400,7 +1600,7 @@ module BetterAuth
|
|
|
1400
1600
|
|
|
1401
1601
|
def sso_find_provider!(ctx, provider_id)
|
|
1402
1602
|
provider = ctx.context.adapter.find_one(model: "ssoProvider", where: [{field: "providerId", value: provider_id.to_s}])
|
|
1403
|
-
raise APIError.new("NOT_FOUND", message:
|
|
1603
|
+
raise APIError.new("NOT_FOUND", message: "Provider not found", code: "PROVIDER_NOT_FOUND") unless provider
|
|
1404
1604
|
|
|
1405
1605
|
provider
|
|
1406
1606
|
end
|
|
@@ -1466,8 +1666,8 @@ module BetterAuth
|
|
|
1466
1666
|
|
|
1467
1667
|
def sso_sanitize_provider(provider, context)
|
|
1468
1668
|
data = provider.dup
|
|
1469
|
-
oidc_config =
|
|
1470
|
-
saml_config =
|
|
1669
|
+
oidc_config = sso_provider_config_hash(data["oidcConfig"])
|
|
1670
|
+
saml_config = sso_provider_config_hash(data["samlConfig"])
|
|
1471
1671
|
data["type"] = saml_config.empty? ? "oidc" : "saml"
|
|
1472
1672
|
data["organizationId"] ||= nil
|
|
1473
1673
|
data["domainVerified"] = !!data["domainVerified"]
|
|
@@ -1478,6 +1678,16 @@ module BetterAuth
|
|
|
1478
1678
|
data.compact
|
|
1479
1679
|
end
|
|
1480
1680
|
|
|
1681
|
+
def sso_provider_config_hash(value)
|
|
1682
|
+
return normalize_hash(value) if value.is_a?(Hash)
|
|
1683
|
+
return {} if value.nil? || value.to_s.strip.empty?
|
|
1684
|
+
|
|
1685
|
+
parsed = JSON.parse(value.to_s)
|
|
1686
|
+
normalize_hash(parsed)
|
|
1687
|
+
rescue JSON::ParserError, TypeError
|
|
1688
|
+
{}
|
|
1689
|
+
end
|
|
1690
|
+
|
|
1481
1691
|
def sso_context_domain_verification_enabled?(context)
|
|
1482
1692
|
context.options.plugins.any? do |plugin|
|
|
1483
1693
|
plugin.id == "sso" && plugin.options.dig(:domain_verification, :enabled)
|
|
@@ -1500,7 +1710,8 @@ module BetterAuth
|
|
|
1500
1710
|
"scopes" => config[:scopes],
|
|
1501
1711
|
"tokenEndpointAuthentication" => config[:token_endpoint_authentication],
|
|
1502
1712
|
"pkce" => config[:pkce],
|
|
1503
|
-
"discoveryEndpoint" => config[:discovery_endpoint]
|
|
1713
|
+
"discoveryEndpoint" => config[:discovery_endpoint],
|
|
1714
|
+
"mapping" => config[:mapping] && sso_sanitize_config(config[:mapping])
|
|
1504
1715
|
}.compact
|
|
1505
1716
|
end
|
|
1506
1717
|
|
|
@@ -1514,10 +1725,22 @@ module BetterAuth
|
|
|
1514
1725
|
"identifierFormat" => config[:identifier_format],
|
|
1515
1726
|
"signatureAlgorithm" => config[:signature_algorithm],
|
|
1516
1727
|
"digestAlgorithm" => config[:digest_algorithm],
|
|
1517
|
-
"certificate" => sso_parse_certificate(config[:cert])
|
|
1728
|
+
"certificate" => sso_parse_certificate(config[:cert]),
|
|
1729
|
+
"idpMetadata" => sso_sanitize_saml_metadata_config(config[:idp_metadata]),
|
|
1730
|
+
"spMetadata" => sso_sanitize_saml_metadata_config(config[:sp_metadata]),
|
|
1731
|
+
"mapping" => config[:mapping] && sso_sanitize_config(config[:mapping])
|
|
1518
1732
|
}.compact
|
|
1519
1733
|
end
|
|
1520
1734
|
|
|
1735
|
+
def sso_sanitize_saml_metadata_config(metadata)
|
|
1736
|
+
data = normalize_hash(metadata || {})
|
|
1737
|
+
return nil if data.empty?
|
|
1738
|
+
|
|
1739
|
+
data.except(:private_key, :private_key_pass, :enc_private_key, :enc_private_key_pass, :decryption_pvk).each_with_object({}) do |(key, value), result|
|
|
1740
|
+
result[(key == :entity_id) ? "entityID" : Schema.storage_key(key)] = value
|
|
1741
|
+
end
|
|
1742
|
+
end
|
|
1743
|
+
|
|
1521
1744
|
def sso_mask_client_id(client_id)
|
|
1522
1745
|
value = client_id.to_s
|
|
1523
1746
|
return "****" if value.length <= 4
|
|
@@ -1533,13 +1756,22 @@ module BetterAuth
|
|
|
1533
1756
|
end
|
|
1534
1757
|
|
|
1535
1758
|
def sso_fetch(data, key)
|
|
1759
|
+
return nil unless data.respond_to?(:[])
|
|
1760
|
+
|
|
1536
1761
|
compact = key.to_s.delete("_").downcase
|
|
1537
|
-
data[key] ||
|
|
1762
|
+
direct = data[key] ||
|
|
1538
1763
|
data[key.to_s] ||
|
|
1539
1764
|
data[Schema.storage_key(key)] ||
|
|
1540
1765
|
data[Schema.storage_key(key).to_sym] ||
|
|
1541
1766
|
data[compact] ||
|
|
1542
1767
|
data[compact.to_sym]
|
|
1768
|
+
return direct unless direct.nil?
|
|
1769
|
+
|
|
1770
|
+
data.each do |candidate, value|
|
|
1771
|
+
normalized = candidate.to_s.delete("_").downcase
|
|
1772
|
+
return value if normalized == compact
|
|
1773
|
+
end
|
|
1774
|
+
nil
|
|
1543
1775
|
end
|
|
1544
1776
|
|
|
1545
1777
|
def sso_redirect(ctx, location)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module SSO
|
|
5
|
+
module Client
|
|
6
|
+
ID = "sso-client"
|
|
7
|
+
PATH_METHODS = {
|
|
8
|
+
"/sso/providers" => "GET",
|
|
9
|
+
"/sso/get-provider" => "GET"
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def sso_client(options = {})
|
|
15
|
+
domain_verification = options[:domain_verification] || options["domainVerification"] || options["domain_verification"] || {}
|
|
16
|
+
enabled = domain_verification[:enabled] || domain_verification["enabled"] || false
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
id: ID,
|
|
20
|
+
version: VERSION,
|
|
21
|
+
infer_server_plugin: {
|
|
22
|
+
domain_verification: {
|
|
23
|
+
enabled: !!enabled
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
path_methods: PATH_METHODS
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|