Module: BetterAuth::Plugins
- Defined in:
- lib/better_auth/sso/plugin/saml_core.rb,
lib/better_auth/sso/plugin/saml_response.rb,
lib/better_auth/sso/plugin/saml_metadata_and_logout.rb,
lib/better_auth/sso/plugin/saml_validation_and_state.rb
Constant Summary collapse
- SSO_SAML_SIGNATURE_ALGORITHMS =
{ "rsa-sha1" => "http://www.w3.org/2000/09/xmldsig#rsa-sha1", "rsa-sha256" => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", "rsa-sha384" => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384", "rsa-sha512" => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", "ecdsa-sha256" => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256", "ecdsa-sha384" => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384", "ecdsa-sha512" => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512", "sha1" => "http://www.w3.org/2000/09/xmldsig#rsa-sha1", "sha256" => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", "sha384" => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384", "sha512" => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" }.freeze
- SSO_SAML_DIGEST_ALGORITHMS =
{ "sha1" => "http://www.w3.org/2000/09/xmldsig#sha1", "sha256" => "http://www.w3.org/2001/04/xmlenc#sha256", "sha384" => "http://www.w3.org/2001/04/xmldsig-more#sha384", "sha512" => "http://www.w3.org/2001/04/xmlenc#sha512" }.freeze
- SSO_SAML_SECURE_SIGNATURE_ALGORITHMS =
(SSO_SAML_SIGNATURE_ALGORITHMS.values - ["http://www.w3.org/2000/09/xmldsig#rsa-sha1"]).uniq.freeze
- SSO_SAML_SECURE_DIGEST_ALGORITHMS =
(SSO_SAML_DIGEST_ALGORITHMS.values - ["http://www.w3.org/2000/09/xmldsig#sha1"]).uniq.freeze
- SSO_SAML_SECURE_KEY_ENCRYPTION_ALGORITHMS =
%w[ http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p http://www.w3.org/2009/xmlenc11#rsa-oaep ].freeze
- SSO_SAML_SECURE_DATA_ENCRYPTION_ALGORITHMS =
%w[ http://www.w3.org/2001/04/xmlenc#aes128-cbc http://www.w3.org/2001/04/xmlenc#aes192-cbc http://www.w3.org/2001/04/xmlenc#aes256-cbc http://www.w3.org/2009/xmlenc11#aes128-gcm http://www.w3.org/2009/xmlenc11#aes192-gcm http://www.w3.org/2009/xmlenc11#aes256-gcm ].freeze
- SSO_DEFAULT_MAX_SAML_RESPONSE_SIZE =
256 * 1024
- SSO_DEFAULT_MAX_SAML_METADATA_SIZE =
100 * 1024
- SSO_SAML_RELAY_STATE_KEY_PREFIX =
"saml-relay-state:"- SSO_SAML_AUTHN_REQUEST_KEY_PREFIX =
"saml-authn-request:"- SSO_DEFAULT_AUTHN_REQUEST_TTL_MS =
5 * 60 * 1000
- SSO_SAML_USED_ASSERTION_KEY_PREFIX =
"saml-used-assertion:"- SSO_DEFAULT_ASSERTION_TTL_MS =
15 * 60 * 1000
- SSO_DEFAULT_CLOCK_SKEW_MS =
5 * 60 * 1000
- SSO_SAML_SESSION_KEY_PREFIX =
"saml-session:"- SSO_SAML_SESSION_BY_ID_KEY_PREFIX =
"saml-session-by-id:"- SSO_SAML_LOGOUT_REQUEST_KEY_PREFIX =
"saml-logout-request:"- SSO_SAML_STATUS_SUCCESS =
"urn:oasis:names:tc:SAML:2.0:status:Success"- SSO_DEFAULT_LOGOUT_REQUEST_TTL_MS =
5 * 60 * 1000
Class Method Summary collapse
- .sso_assign_organization_membership(ctx, provider, user, config) ⇒ Object
- .sso_base64_xml?(value) ⇒ Boolean
- .sso_find_or_create_user(ctx, provider, user_info, config = {}) ⇒ Object
- .sso_find_or_create_user_result(ctx, provider, user_info, config = {}) ⇒ Object
- .sso_generate_saml_relay_state(ctx, state_data) ⇒ Object
- .sso_handle_saml_response(ctx, config = {}) ⇒ Object
- .sso_normalize_saml_digest_algorithm(algorithm) ⇒ Object
- .sso_normalize_saml_signature_algorithm(algorithm) ⇒ Object
- .sso_oidc_trusted_provider?(ctx, provider, email) ⇒ Boolean
- .sso_parse_saml_logout_request(raw_request) ⇒ Object
- .sso_parse_saml_logout_response(raw_response) ⇒ Object
- .sso_parse_saml_metadata_xml(xml) ⇒ Object
- .sso_parse_saml_relay_state(ctx, relay_state) ⇒ Object
- .sso_parse_saml_response(value, config = {}, provider = nil, ctx = nil) ⇒ Object
- .sso_parse_saml_timestamp(value, error_message) ⇒ Object
- .sso_process_saml_logout_request(ctx, provider, raw_request) ⇒ Object
- .sso_process_saml_logout_response(ctx, raw_response) ⇒ Object
- .sso_safe_saml_callback_url(ctx, url, provider_id) ⇒ Object
- .sso_safe_slo_redirect_url(ctx, url, provider_id) ⇒ Object
- .sso_saml_acs_url(ctx, provider) ⇒ Object
- .sso_saml_acs_url?(url) ⇒ Boolean
- .sso_saml_callback_url(provider) ⇒ Object
- .sso_saml_idp_metadata(provider_or_config) ⇒ Object
- .sso_saml_logout_destination(provider) ⇒ Object
- .sso_saml_metadata_first_text(doc, element_name) ⇒ Object
- .sso_saml_metadata_services(doc, element_name) ⇒ Object
- .sso_saml_metadata_services_from_config(value) ⇒ Object
- .sso_saml_normalize_certificate(value) ⇒ Object
- .sso_saml_post_form(action, saml_param, saml_value, relay_state = nil) ⇒ Object
- .sso_saml_preferred_service(services) ⇒ Object
- .sso_saml_signature_digest(signature_algorithm) ⇒ Object
- .sso_saml_timestamp_conditions(assertion) ⇒ Object
- .sso_saml_trusted_provider?(ctx, provider, email) ⇒ Boolean
- .sso_signed_saml_redirect_query(provider, query) ⇒ Object
- .sso_sp_metadata_xml(ctx, provider, config = {}) ⇒ Object
- .sso_store_saml_logout_request(ctx, provider, request_id, config) ⇒ Object
- .sso_store_saml_session(ctx, provider, assertion, session) ⇒ Object
- .sso_validate_saml_algorithm_group!(algorithms, allowed:, secure:, deprecated:, on_deprecated:, label:) ⇒ Object
- .sso_validate_saml_algorithms!(xml, options = {}) ⇒ Object
- .sso_validate_saml_config!(saml_config, plugin_config = {}) ⇒ Object
- .sso_validate_saml_redirect_signature(ctx, raw_message, signature, sig_alg) ⇒ Object
- .sso_validate_saml_response!(config, assertion, provider, ctx) ⇒ Object
- .sso_validate_saml_slo_signature!(ctx, raw_message, error_message) ⇒ Object
- .sso_validate_saml_timestamp!(conditions, config = {}, now: Time.now.utc) ⇒ Object
- .sso_validate_single_saml_assertion!(saml_response) ⇒ Object
Class Method Details
.sso_assign_organization_membership(ctx, provider, user, config) ⇒ Object
144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/better_auth/sso/plugin/saml_response.rb', line 144 def sso_assign_organization_membership(ctx, provider, user, config) organization_id = provider["organizationId"] return if organization_id.to_s.empty? return if config.dig(:organization_provisioning, :disabled) return unless ctx.context..plugins.any? { |plugin| plugin.id == "organization" } return if ctx.context.adapter.find_one(model: "member", where: [{field: "organizationId", value: organization_id}, {field: "userId", value: user.fetch("id")}]) role = if config.dig(:organization_provisioning, :get_role).respond_to?(:call) config.dig(:organization_provisioning, :get_role).call(user: user, userInfo: {}, provider: provider) else config.dig(:organization_provisioning, :default_role) || config.dig(:organization_provisioning, :role) || "member" end ctx.context.adapter.create(model: "member", data: {organizationId: organization_id, userId: user.fetch("id"), role: role, createdAt: Time.now}) end |
.sso_base64_xml?(value) ⇒ Boolean
75 76 77 78 79 |
# File 'lib/better_auth/sso/plugin/saml_validation_and_state.rb', line 75 def sso_base64_xml?(value) Base64.decode64(value.to_s).lstrip.start_with?("<") rescue false end |
.sso_find_or_create_user(ctx, provider, user_info, config = {}) ⇒ Object
61 62 63 |
# File 'lib/better_auth/sso/plugin/saml_response.rb', line 61 def sso_find_or_create_user(ctx, provider, user_info, config = {}) sso_find_or_create_user_result(ctx, provider, user_info, config).fetch(:user) end |
.sso_find_or_create_user_result(ctx, provider, user_info, config = {}) ⇒ Object
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/better_auth/sso/plugin/saml_response.rb', line 65 def sso_find_or_create_user_result(ctx, provider, user_info, config = {}) user_info = normalize_hash(user_info) email = user_info[:email].to_s.downcase account_id = (user_info[:id] || user_info["id"]).to_s provider_id = provider.fetch("providerId") storage_provider_id = provider["samlConfig"] ? provider_id : "sso:#{provider_id}" existing_account = account_id.empty? ? nil : ( ctx.context.internal_adapter.find_account_by_provider_id(account_id, provider_id) || ctx.context.internal_adapter.find_account_by_provider_id(account_id, "sso:#{provider_id}") ) if existing_account user = ctx.context.internal_adapter.find_user_by_id(existing_account.fetch("userId")) created = false elsif (found = ctx.context.internal_adapter.find_user_by_email(email, include_accounts: true)) already_linked_provider = Array(found[:accounts]).any? do |account| [provider_id, "sso:#{provider_id}"].include?(account["providerId"]) end if provider["samlConfig"] return {error: "account_not_linked"} unless already_linked_provider || sso_saml_trusted_provider?(ctx, provider, email) elsif !already_linked_provider && !sso_oidc_trusted_provider?(ctx, provider, email) return {error: "account_not_linked"} end user = found[:user] unless account_id.empty? ctx.context.internal_adapter.create_account( accountId: account_id, providerId: storage_provider_id, userId: user.fetch("id") ) end oidc_config = sso_provider_config_hash(provider["oidcConfig"]) if oidc_config[:override_user_info] || config[:default_override_user_info] update = {} update[:name] = user_info[:name] if user_info.key?(:name) update[:image] = user_info[:image] if user_info.key?(:image) update[:emailVerified] = !!user_info[:email_verified] if user_info.key?(:email_verified) user = ctx.context.internal_adapter.update_user(user.fetch("id"), update) if update.any? end created = false else created = ctx.context.internal_adapter.create_user( email: email, name: user_info[:name] || email, emailVerified: user_info.key?(:email_verified) ? user_info[:email_verified] : false, image: user_info[:image] ) ctx.context.internal_adapter.create_account( accountId: account_id.empty? ? created.fetch("id") : account_id, providerId: storage_provider_id, userId: created.fetch("id") ) user = created created = true end sso_assign_organization_membership(ctx, provider, user, config) {user: user, created: created} end |
.sso_generate_saml_relay_state(ctx, state_data) ⇒ Object
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/better_auth/sso/plugin/saml_validation_and_state.rb', line 150 def sso_generate_saml_relay_state(ctx, state_data) ttl_ms = 10 * 60 * 1000 relay_state = BetterAuth::Crypto.random_string(32) now_ms = (Time.now.to_f * 1000).to_i stored = state_data.each_with_object({}) { |(key, value), result| result[key.to_s] = value }.merge( "codeVerifier" => BetterAuth::Crypto.random_string(128), "expiresAt" => now_ms + ttl_ms ) ctx.context.internal_adapter.create_verification_value( identifier: "#{SSO_SAML_RELAY_STATE_KEY_PREFIX}#{relay_state}", value: JSON.generate(stored), expiresAt: Time.at((now_ms + ttl_ms) / 1000.0) ) ctx.("relay_state", relay_state, ctx.context.secret, path: "/", max_age: ttl_ms / 1000, http_only: true, same_site: "lax") relay_state end |
.sso_handle_saml_response(ctx, config = {}) ⇒ Object
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/better_auth/sso/plugin/saml_response.rb', line 7 def sso_handle_saml_response(ctx, config = {}) provider = sso_find_saml_provider!(ctx, sso_fetch(ctx.params, :provider_id), config) relay_state = sso_fetch(ctx.body, :relay_state) || sso_fetch(ctx.query, :relay_state) state = sso_parse_saml_relay_state(ctx, relay_state) || {} raw_response = sso_fetch(ctx.body, :saml_response) || sso_fetch(ctx.query, :saml_response) if ctx.method == "GET" && raw_response.to_s.empty? session = Routes.current_session(ctx, allow_nil: true) unless session return sso_redirect(ctx, sso_append_error("#{ctx.context.base_url}/error", "invalid_request")) end return sso_redirect(ctx, sso_safe_saml_callback_url(ctx, relay_state || sso_saml_callback_url(provider) || "/", provider.fetch("providerId"))) end max_response_size = config.dig(:saml, :max_response_size) || SSO_DEFAULT_MAX_SAML_RESPONSE_SIZE if raw_response.to_s.bytesize > max_response_size raise APIError.new("BAD_REQUEST", message: "SAML response exceeds maximum allowed size (#{max_response_size} bytes)") end in_response_to_result = sso_validate_saml_in_response_to(ctx, config, provider, raw_response, state) return in_response_to_result if in_response_to_result.is_a?(Array) assertion = sso_parse_saml_response(raw_response, config, provider, ctx) assertion[:email_verified] = false unless config[:trust_email_verified] ((assertion), config) sso_validate_saml_response!(config, assertion, provider, ctx) sso_consume_saml_in_response_to(ctx, in_response_to_result) assertion_id = assertion[:id] || assertion["id"] unless assertion_id.to_s.empty? replay_key = "#{SSO_SAML_USED_ASSERTION_KEY_PREFIX}#{assertion_id}" if ctx.context.internal_adapter.find_verification_value(replay_key) callback_url = sso_safe_saml_callback_url(ctx, state["callbackURL"] || sso_saml_callback_url(provider) || "/", provider.fetch("providerId")) return sso_redirect(ctx, sso_append_error(callback_url, "replay_detected", "SAML assertion has already been used")) end ctx.context.internal_adapter.create_verification_value(identifier: replay_key, value: "used", expiresAt: sso_saml_assertion_replay_expires_at(assertion, config)) end callback_url = sso_safe_saml_callback_url(ctx, state["callbackURL"] || sso_saml_callback_url(provider) || "/", provider.fetch("providerId")) email = (assertion[:email] || assertion["email"]).to_s.downcase if config[:disable_implicit_sign_up] && !state["requestSignUp"] && !ctx.context.internal_adapter.find_user_by_email(email) return sso_redirect(ctx, sso_append_error(callback_url, "signup disabled")) end result = sso_find_or_create_user_result(ctx, provider, assertion, config) return sso_redirect(ctx, sso_append_error(callback_url, result.fetch(:error))) if result[:error] user = result.fetch(:user) if config[:provision_user].respond_to?(:call) && (result.fetch(:created) || config[:provision_user_on_every_login]) config[:provision_user].call(user: user, userInfo: assertion, provider: provider) end session = ctx.context.internal_adapter.create_session(user.fetch("id")) sso_store_saml_session(ctx, provider, assertion, session) if config.dig(:saml, :enable_single_logout) Cookies.(ctx, {session: session, user: user}) sso_redirect(ctx, callback_url) end |
.sso_normalize_saml_digest_algorithm(algorithm) ⇒ Object
128 129 130 |
# File 'lib/better_auth/sso/plugin/saml_validation_and_state.rb', line 128 def sso_normalize_saml_digest_algorithm(algorithm) SSO_SAML_DIGEST_ALGORITHMS.fetch(algorithm.to_s.downcase, algorithm.to_s) end |
.sso_normalize_saml_signature_algorithm(algorithm) ⇒ Object
124 125 126 |
# File 'lib/better_auth/sso/plugin/saml_validation_and_state.rb', line 124 def sso_normalize_saml_signature_algorithm(algorithm) SSO_SAML_SIGNATURE_ALGORITHMS.fetch(algorithm.to_s.downcase, algorithm.to_s) end |
.sso_oidc_trusted_provider?(ctx, provider, email) ⇒ Boolean
133 134 135 136 137 138 139 140 141 142 |
# File 'lib/better_auth/sso/plugin/saml_response.rb', line 133 def sso_oidc_trusted_provider?(ctx, provider, email) provider_id = provider.fetch("providerId") linking = ctx.context..account[:account_linking] || {} return false if linking[:enabled] == false trusted_providers = Array(linking[:trusted_providers]).map(&:to_s) trusted_providers.include?(provider_id.to_s) || trusted_providers.include?("sso:#{provider_id}") || (provider["domainVerified"] && sso_email_domain_matches?(email, provider["domain"])) end |
.sso_parse_saml_logout_request(raw_request) ⇒ Object
246 247 248 249 250 251 252 253 254 255 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 246 def sso_parse_saml_logout_request(raw_request) xml = Base64.decode64(raw_request.to_s.gsub(/\s+/, "")) { id: xml[/\bID=['"]([^'"]+)['"]/, 1], name_id: xml[%r{<(?:\w+:)?NameID[^>]*>([^<]+)</(?:\w+:)?NameID>}, 1], session_index: xml[%r{<(?:\w+:)?SessionIndex[^>]*>([^<]+)</(?:\w+:)?SessionIndex>}, 1] } rescue {} end |
.sso_parse_saml_logout_response(raw_response) ⇒ Object
291 292 293 294 295 296 297 298 299 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 291 def sso_parse_saml_logout_response(raw_response) xml = Base64.decode64(raw_response.to_s.gsub(/\s+/, "")) { in_response_to: xml[/\bInResponseTo=['"]([^'"]+)['"]/, 1], status_code: xml[/<(?:\w+:)?StatusCode\b[^>]*\bValue=['"]([^'"]+)['"]/, 1] } rescue {} end |
.sso_parse_saml_metadata_xml(xml) ⇒ Object
107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 107 def (xml) doc = REXML::Document.new(xml.to_s) root = doc.root { entity_id: root&.attributes&.[]("entityID"), cert: sso_saml_normalize_certificate((doc, "X509Certificate")), single_sign_on_service: (doc, "SingleSignOnService"), single_logout_service: (doc, "SingleLogoutService") }.compact rescue {} end |
.sso_parse_saml_relay_state(ctx, relay_state) ⇒ Object
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
# File 'lib/better_auth/sso/plugin/saml_validation_and_state.rb', line 167 def sso_parse_saml_relay_state(ctx, relay_state) state = sso_verify_state(relay_state, ctx.context.secret) return state if state verification = ctx.context.internal_adapter.find_verification_value("#{SSO_SAML_RELAY_STATE_KEY_PREFIX}#{relay_state}") return nil unless verification return nil unless sso_future_time?(verification.fetch("expiresAt")) parsed = JSON.parse(verification.fetch("value")) return nil if parsed["expiresAt"].to_i <= (Time.now.to_f * 1000).to_i parsed rescue nil end |
.sso_parse_saml_response(value, config = {}, provider = nil, ctx = nil) ⇒ Object
7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# File 'lib/better_auth/sso/plugin/saml_validation_and_state.rb', line 7 def sso_parse_saml_response(value, config = {}, provider = nil, ctx = nil) parser = config.dig(:saml, :parse_response) if parser.respond_to?(:call) sso_validate_single_saml_assertion!(value) if sso_base64_xml?(value) parsed = parser.call(raw_response: value.to_s, provider: provider, context: ctx) return normalize_hash(parsed) end JSON.parse(Base64.decode64(value.to_s), symbolize_names: true) rescue APIError raise APIError.new("BAD_REQUEST", message: "Invalid SAML response") rescue raise APIError.new("BAD_REQUEST", message: "Invalid SAML response") end |
.sso_parse_saml_timestamp(value, error_message) ⇒ Object
61 62 63 64 65 |
# File 'lib/better_auth/sso/plugin/saml_validation_and_state.rb', line 61 def (value, ) Time.parse(value.to_s).utc rescue raise APIError.new("BAD_REQUEST", message: ) end |
.sso_process_saml_logout_request(ctx, provider, raw_request) ⇒ Object
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 205 def sso_process_saml_logout_request(ctx, provider, raw_request) data = sso_parse_saml_logout_request(raw_request) return data if data[:name_id].to_s.empty? session_identifier = "#{SSO_SAML_SESSION_KEY_PREFIX}#{provider.fetch("providerId")}:#{data[:name_id]}" verification = ctx.context.internal_adapter.find_verification_value(session_identifier) return data unless verification record = JSON.parse(verification.fetch("value")) session_token = record["sessionToken"] session_index_matches = data[:session_index].to_s.empty? || record["sessionIndex"].to_s.empty? || data[:session_index].to_s == record["sessionIndex"].to_s ctx.context.internal_adapter.delete_session(session_token) if session_token && session_index_matches ctx.context.internal_adapter.delete_verification_by_identifier(session_identifier) ctx.context.internal_adapter.delete_verification_by_identifier("#{SSO_SAML_SESSION_BY_ID_KEY_PREFIX}#{session_token}") if session_token data rescue {} end |
.sso_process_saml_logout_response(ctx, raw_response) ⇒ Object
233 234 235 236 237 238 239 240 241 242 243 244 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 233 def sso_process_saml_logout_response(ctx, raw_response) data = sso_parse_saml_logout_response(raw_response) status_code = data[:status_code] if status_code && status_code != SSO_SAML_STATUS_SUCCESS raise APIError.new("BAD_REQUEST", message: "Logout failed at IdP") end in_response_to = data[:in_response_to] return if in_response_to.to_s.empty? ctx.context.internal_adapter.delete_verification_by_identifier("#{SSO_SAML_LOGOUT_REQUEST_KEY_PREFIX}#{in_response_to}") end |
.sso_safe_saml_callback_url(ctx, url, provider_id) ⇒ Object
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 323 def sso_safe_saml_callback_url(ctx, url, provider_id) app_origin = ctx.context.base_url callback_path = URI.parse("#{ctx.context.base_url}/sso/saml2/callback/#{URI.encode_www_form_component(provider_id)}").path acs_path = URI.parse("#{ctx.context.base_url}/sso/saml2/sp/acs/#{URI.encode_www_form_component(provider_id)}").path value = url.to_s return app_origin if value.empty? if value.start_with?("/") && !value.start_with?("//") parsed = URI.parse(value) return app_origin if [callback_path, acs_path].include?(parsed.path) return value end return app_origin unless ctx.context.trusted_origin?(value, allow_relative_paths: false) parsed = URI.parse(value) return app_origin if [callback_path, acs_path].include?(parsed.path) value rescue app_origin end |
.sso_safe_slo_redirect_url(ctx, url, provider_id) ⇒ Object
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 301 def sso_safe_slo_redirect_url(ctx, url, provider_id) app_origin = ctx.context.base_url callback_path = URI.parse("#{ctx.context.base_url}/sso/saml2/sp/slo/#{URI.encode_www_form_component(provider_id)}").path value = url.to_s return app_origin if value.empty? if value.start_with?("/") && !value.start_with?("//") parsed = URI.parse(value) return app_origin if parsed.path == callback_path return value end return app_origin unless ctx.context.trusted_origin?(value, allow_relative_paths: false) parsed = URI.parse(value) return app_origin if parsed.path == callback_path value rescue app_origin end |
.sso_saml_acs_url(ctx, provider) ⇒ Object
74 75 76 77 78 79 80 81 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 74 def sso_saml_acs_url(ctx, provider) provider_id = provider.fetch("providerId") base_url = ctx.context.base_url configured = sso_provider_config_hash(provider["samlConfig"])[:callback_url].to_s return configured if sso_saml_acs_url?(configured) "#{base_url}/sso/saml2/sp/acs/#{URI.encode_www_form_component(provider_id)}" end |
.sso_saml_acs_url?(url) ⇒ Boolean
83 84 85 86 87 88 89 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 83 def sso_saml_acs_url?(url) return false if url.to_s.empty? URI.parse(url.to_s).path.include?("/sso/saml2/sp/acs") rescue false end |
.sso_saml_callback_url(provider) ⇒ Object
161 162 163 164 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 161 def sso_saml_callback_url(provider) saml_config = sso_provider_config_hash(provider["samlConfig"]) saml_config[:callback_url] end |
.sso_saml_idp_metadata(provider_or_config) ⇒ Object
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 91 def (provider_or_config) saml_config = if provider_or_config.respond_to?(:key?) && (provider_or_config.key?("samlConfig") || provider_or_config.key?(:samlConfig)) normalize_hash(provider_or_config["samlConfig"] || provider_or_config[:samlConfig] || {}) else normalize_hash(provider_or_config || {}) end = normalize_hash(saml_config[:idp_metadata] || {}) xml = [:metadata] || saml_config[:metadata] || saml_config[:idp_metadata_xml] parsed = xml.to_s.strip.empty? ? {} : (xml) parsed[:entity_id] ||= [:entity_id] || [:entityID] || saml_config[:issuer] parsed[:cert] ||= [:cert] || saml_config[:cert] parsed[:single_sign_on_service] = ([:single_sign_on_service] || saml_config[:single_sign_on_service]) if parsed[:single_sign_on_service].to_a.empty? parsed[:single_logout_service] = ([:single_logout_service] || saml_config[:single_logout_service]) if parsed[:single_logout_service].to_a.empty? parsed end |
.sso_saml_logout_destination(provider) ⇒ Object
166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 166 def sso_saml_logout_destination(provider) saml_config = sso_provider_config_hash(provider["samlConfig"]) direct = saml_config[:single_logout_service] || saml_config[:single_logout_service_url] || saml_config[:idp_slo_service_url] || saml_config[:logout_url] return direct unless direct.to_s.empty? service = sso_saml_preferred_service((saml_config)[:single_logout_service]) normalize_hash(service || {})[:location] end |
.sso_saml_metadata_first_text(doc, element_name) ⇒ Object
133 134 135 136 137 138 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 133 def (doc, element_name) REXML::XPath.each(doc, "//*") do |element| return element.text.to_s.strip if element.name == element_name && !element.text.to_s.strip.empty? end nil end |
.sso_saml_metadata_services(doc, element_name) ⇒ Object
120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 120 def (doc, element_name) services = [] REXML::XPath.each(doc, "//*") do |element| next unless element.name == element_name services << { binding: element.attributes["Binding"], location: element.attributes["Location"] }.compact end services end |
.sso_saml_metadata_services_from_config(value) ⇒ Object
140 141 142 143 144 145 146 147 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 140 def (value) Array(value).filter_map do |entry| data = normalize_hash(entry || {}) next if data[:location].to_s.empty? {binding: data[:binding] || data[:Binding], location: data[:location] || data[:Location]}.compact end end |
.sso_saml_normalize_certificate(value) ⇒ Object
153 154 155 156 157 158 159 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 153 def sso_saml_normalize_certificate(value) cert = value.to_s.strip return nil if cert.empty? return cert if cert.include?("BEGIN CERTIFICATE") "-----BEGIN CERTIFICATE-----\n#{cert.scan(/.{1,64}/).join("\n")}\n-----END CERTIFICATE-----" end |
.sso_saml_post_form(action, saml_param, saml_value, relay_state = nil) ⇒ Object
372 373 374 375 376 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 372 def sso_saml_post_form(action, saml_param, saml_value, relay_state = nil) relay_input = relay_state.to_s.empty? ? "" : "<input type=\"hidden\" name=\"RelayState\" value=\"#{CGI.escapeHTML(relay_state.to_s)}\" />" 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>" [200, {"content-type" => "text/html"}, [html]] end |
.sso_saml_preferred_service(services) ⇒ Object
149 150 151 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 149 def sso_saml_preferred_service(services) Array(services).find { |service| normalize_hash(service)[:binding].to_s.include?("HTTP-Redirect") } || Array(services).first end |
.sso_saml_signature_digest(signature_algorithm) ⇒ Object
359 360 361 362 363 364 365 366 367 368 369 370 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 359 def sso_saml_signature_digest(signature_algorithm) case signature_algorithm.to_s when /sha512/i OpenSSL::Digest.new("SHA512") when /sha384/i OpenSSL::Digest.new("SHA384") when /sha1/i OpenSSL::Digest.new("SHA1") else OpenSSL::Digest.new("SHA256") end end |
.sso_saml_timestamp_conditions(assertion) ⇒ Object
67 68 69 70 71 72 73 |
# File 'lib/better_auth/sso/plugin/saml_validation_and_state.rb', line 67 def (assertion) assertion = normalize_hash(assertion || {}) conditions = normalize_hash(assertion[:conditions] || {}) conditions[:not_before] ||= assertion[:not_before] || assertion[:notBefore] conditions[:not_on_or_after] ||= assertion[:not_on_or_after] || assertion[:notOnOrAfter] conditions end |
.sso_saml_trusted_provider?(ctx, provider, email) ⇒ Boolean
124 125 126 127 128 129 130 131 |
# File 'lib/better_auth/sso/plugin/saml_response.rb', line 124 def sso_saml_trusted_provider?(ctx, provider, email) provider_id = provider.fetch("providerId") linking = ctx.context..account[:account_linking] || {} return false if linking[:enabled] == false trusted = Array(linking[:trusted_providers]).map(&:to_s).include?(provider_id.to_s) trusted || (provider["domainVerified"] && sso_email_domain_matches?(email, provider["domain"])) end |
.sso_signed_saml_redirect_query(provider, query) ⇒ Object
346 347 348 349 350 351 352 353 354 355 356 357 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 346 def sso_signed_saml_redirect_query(provider, query) saml_config = sso_provider_config_hash(provider["samlConfig"]) private_key = saml_config.dig(:sp_metadata, :private_key) || saml_config[:private_key] || saml_config[:sp_private_key] raise APIError.new("BAD_REQUEST", message: "SAML Redirect signing requires privateKey") if private_key.to_s.empty? sig_alg = saml_config[:signature_algorithm] ? sso_normalize_saml_signature_algorithm(saml_config[:signature_algorithm]) : XMLSecurity::Document::RSA_SHA256 signed = query.compact.merge(SigAlg: sig_alg) 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? } signature_input = URI.encode_www_form(signed_payload) signed[:Signature] = Base64.strict_encode64(OpenSSL::PKey.read(private_key).sign(sso_saml_signature_digest(sig_alg), signature_input)) signed end |
.sso_sp_metadata_xml(ctx, provider, config = {}) ⇒ Object
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 53 def (ctx, provider, config = {}) provider_id = provider.fetch("providerId") saml_config = sso_provider_config_hash(provider["samlConfig"]) = saml_config.dig(:sp_metadata, :metadata) return unless .to_s.empty? 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)}" acs_url = sso_saml_acs_url(ctx, provider) authn_requests_signed = !!saml_config[:authn_requests_signed] want_assertions_signed = saml_config.key?(:want_assertions_signed) ? !!saml_config[:want_assertions_signed] : true escaped_entity_id = CGI.escapeHTML(entity_id.to_s) escaped_acs_url = CGI.escapeHTML(acs_url.to_s) name_id_format = saml_config[:identifier_format].to_s.empty? ? "" : "<NameIDFormat>#{CGI.escapeHTML(saml_config[:identifier_format].to_s)}</NameIDFormat>" slo = if config.dig(:saml, :enable_single_logout) location = CGI.escapeHTML("#{ctx.context.base_url}/sso/saml2/sp/slo/#{URI.encode_www_form_component(provider_id)}") "<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}\" />" end "<EntityDescriptor entityID=\"#{escaped_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=\"#{escaped_acs_url}\" index=\"0\" /></SPSSODescriptor></EntityDescriptor>" end |
.sso_store_saml_logout_request(ctx, provider, request_id, config) ⇒ Object
224 225 226 227 228 229 230 231 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 224 def sso_store_saml_logout_request(ctx, provider, request_id, config) ttl_ms = (config.dig(:saml, :logout_request_ttl) || SSO_DEFAULT_LOGOUT_REQUEST_TTL_MS).to_i ctx.context.internal_adapter.create_verification_value( identifier: "#{SSO_SAML_LOGOUT_REQUEST_KEY_PREFIX}#{request_id}", value: provider.fetch("providerId"), expiresAt: Time.now + (ttl_ms / 1000.0) ) end |
.sso_store_saml_session(ctx, provider, assertion, session) ⇒ Object
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 178 def sso_store_saml_session(ctx, provider, assertion, session) name_id = assertion[:name_id] || assertion[:nameid] || assertion[:email] session_index = assertion[:session_index] || assertion[:sessionindex] || assertion[:id] return if name_id.to_s.empty? || session_index.to_s.empty? record = { providerId: provider.fetch("providerId"), sessionToken: session.fetch("token"), userId: session.fetch("userId"), nameId: name_id.to_s, sessionIndex: session_index.to_s } expires_at = session["expiresAt"] || Time.now + (SSO_DEFAULT_ASSERTION_TTL_MS / 1000.0) value = JSON.generate(record) session_identifier = "#{SSO_SAML_SESSION_KEY_PREFIX}#{provider.fetch("providerId")}:#{name_id}" ctx.context.internal_adapter.create_verification_value( identifier: session_identifier, value: value, expiresAt: expires_at ) ctx.context.internal_adapter.create_verification_value( identifier: "#{SSO_SAML_SESSION_BY_ID_KEY_PREFIX}#{session.fetch("token")}", value: session_identifier, expiresAt: expires_at ) end |
.sso_validate_saml_algorithm_group!(algorithms, allowed:, secure:, deprecated:, on_deprecated:, label:) ⇒ Object
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/better_auth/sso/plugin/saml_validation_and_state.rb', line 132 def sso_validate_saml_algorithm_group!(algorithms, allowed:, secure:, deprecated:, on_deprecated:, label:) algorithms.each do |algorithm| if allowed next if allowed.include?(algorithm) raise APIError.new("BAD_REQUEST", message: "SAML #{label} algorithm not in allow-list: #{algorithm}") end if deprecated.include?(algorithm) raise APIError.new("BAD_REQUEST", message: "SAML response uses deprecated #{label} algorithm: #{algorithm}") if on_deprecated == "reject" next end next if secure.include?(algorithm) raise APIError.new("BAD_REQUEST", message: "SAML #{label} algorithm not recognized: #{algorithm}") end end |
.sso_validate_saml_algorithms!(xml, options = {}) ⇒ Object
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/better_auth/sso/plugin/saml_validation_and_state.rb', line 81 def sso_validate_saml_algorithms!(xml, = {}) on_deprecated = ([:on_deprecated] || "warn").to_s signature_algorithms = xml.to_s.scan(/SignatureMethod[^>]+Algorithm=["']([^"']+)["']/).flatten.map { |algorithm| sso_normalize_saml_signature_algorithm(algorithm) } digest_algorithms = xml.to_s.scan(/DigestMethod[^>]+Algorithm=["']([^"']+)["']/).flatten.map { |algorithm| sso_normalize_saml_digest_algorithm(algorithm) } key_encryption_algorithms = xml.to_s.scan(/<[^\/>]*EncryptedKey\b[\s\S]*?EncryptionMethod[^>]+Algorithm=["']([^"']+)["']/).flatten data_encryption_algorithms = xml.to_s.scan(/<[^\/>]*EncryptedData\b[\s\S]*?EncryptionMethod[^>]+Algorithm=["']([^"']+)["']/).flatten sso_validate_saml_algorithm_group!( signature_algorithms, allowed: [:allowed_signature_algorithms]&.map { |algorithm| sso_normalize_saml_signature_algorithm(algorithm) }, secure: SSO_SAML_SECURE_SIGNATURE_ALGORITHMS, deprecated: ["http://www.w3.org/2000/09/xmldsig#rsa-sha1"], on_deprecated: on_deprecated, label: "signature" ) sso_validate_saml_algorithm_group!( digest_algorithms, allowed: [:allowed_digest_algorithms]&.map { |algorithm| sso_normalize_saml_digest_algorithm(algorithm) }, secure: SSO_SAML_SECURE_DIGEST_ALGORITHMS, deprecated: ["http://www.w3.org/2000/09/xmldsig#sha1"], on_deprecated: on_deprecated, label: "digest" ) sso_validate_saml_algorithm_group!( key_encryption_algorithms, allowed: [:allowed_key_encryption_algorithms], secure: SSO_SAML_SECURE_KEY_ENCRYPTION_ALGORITHMS, deprecated: ["http://www.w3.org/2001/04/xmlenc#rsa-1_5"], on_deprecated: on_deprecated, label: "key encryption" ) sso_validate_saml_algorithm_group!( data_encryption_algorithms, allowed: [:allowed_data_encryption_algorithms], secure: SSO_SAML_SECURE_DATA_ENCRYPTION_ALGORITHMS, deprecated: ["http://www.w3.org/2001/04/xmlenc#tripledes-cbc"], on_deprecated: on_deprecated, label: "data encryption" ) true end |
.sso_validate_saml_config!(saml_config, plugin_config = {}) ⇒ Object
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 7 def sso_validate_saml_config!(saml_config, plugin_config = {}) = saml_config[:idp_metadata] || saml_config[:metadata] || saml_config[:idp_metadata_xml] = normalize_hash(saml_config[:idp_metadata] || {}) = ![:metadata].to_s.empty? || !saml_config[:metadata].to_s.empty? || !saml_config[:idp_metadata_xml].to_s.empty? has_idp_sso_service = !Array([:single_sign_on_service] || saml_config[:single_sign_on_service]).empty? = plugin_config.dig(:saml, :max_metadata_size) || SSO_DEFAULT_MAX_SAML_METADATA_SIZE if .to_s.bytesize > raise APIError.new("BAD_REQUEST", message: "IdP metadata exceeds maximum allowed size (#{} bytes)") end if saml_config[:entry_point].to_s.empty? && !has_idp_sso_service && ! raise APIError.new("BAD_REQUEST", message: "SAML configuration requires either idpMetadata.metadata, idpMetadata.singleSignOnService, or a valid entryPoint URL") end sso_validate_url!(saml_config[:entry_point], "SAML entryPoint must be a valid URL") unless saml_config[:entry_point].to_s.empty? unless saml_config[:single_sign_on_service].to_s.empty? sso_validate_url!(saml_config[:single_sign_on_service], "SAML singleSignOnService must be a valid URL") end unless saml_config[:single_logout_service].to_s.empty? sso_validate_url!(saml_config[:single_logout_service], "SAML singleLogoutService must be a valid URL") end config_algorithm_xml = +"" unless saml_config[:signature_algorithm].to_s.empty? config_algorithm_xml << "<ds:SignatureMethod Algorithm=\"#{saml_config[:signature_algorithm]}\"/>" end unless saml_config[:digest_algorithm].to_s.empty? config_algorithm_xml << "<ds:DigestMethod Algorithm=\"#{saml_config[:digest_algorithm]}\"/>" end sso_validate_saml_algorithms!( config_algorithm_xml, on_deprecated: plugin_config.dig(:saml, :algorithms, :on_deprecated) || saml_config[:on_deprecated_algorithm] || "warn", allowed_signature_algorithms: plugin_config.dig(:saml, :algorithms, :allowed_signature_algorithms) || saml_config[:allowed_signature_algorithms], allowed_digest_algorithms: plugin_config.dig(:saml, :algorithms, :allowed_digest_algorithms) || saml_config[:allowed_digest_algorithms], allowed_key_encryption_algorithms: plugin_config.dig(:saml, :algorithms, :allowed_key_encryption_algorithms) || saml_config[:allowed_key_encryption_algorithms], allowed_data_encryption_algorithms: plugin_config.dig(:saml, :algorithms, :allowed_data_encryption_algorithms) || saml_config[:allowed_data_encryption_algorithms] ) sso_validate_saml_algorithms!( .to_s, on_deprecated: plugin_config.dig(:saml, :algorithms, :on_deprecated) || saml_config[:on_deprecated_algorithm] || "warn", allowed_signature_algorithms: plugin_config.dig(:saml, :algorithms, :allowed_signature_algorithms) || saml_config[:allowed_signature_algorithms], allowed_digest_algorithms: plugin_config.dig(:saml, :algorithms, :allowed_digest_algorithms) || saml_config[:allowed_digest_algorithms], allowed_key_encryption_algorithms: plugin_config.dig(:saml, :algorithms, :allowed_key_encryption_algorithms) || saml_config[:allowed_key_encryption_algorithms], allowed_data_encryption_algorithms: plugin_config.dig(:saml, :algorithms, :allowed_data_encryption_algorithms) || saml_config[:allowed_data_encryption_algorithms] ) end |
.sso_validate_saml_redirect_signature(ctx, raw_message, signature, sig_alg) ⇒ Object
276 277 278 279 280 281 282 283 284 285 286 287 288 289 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 276 def sso_validate_saml_redirect_signature(ctx, , signature, sig_alg) provider = sso_find_provider!(ctx, sso_fetch(ctx.params, :provider_id)) cert = (provider)[:cert] certificate = OpenSSL::X509::Certificate.new(cert.to_s) has_saml_request = sso_fetch(ctx.body, :saml_request) || sso_fetch(ctx.query, :saml_request) saml_param = has_saml_request ? "SAMLRequest" : "SAMLResponse" relay_state = sso_fetch(ctx.body, :relay_state) || sso_fetch(ctx.query, :relay_state) payload = [[saml_param, ]] payload << ["RelayState", relay_state] unless relay_state.to_s.empty? payload << ["SigAlg", sig_alg] certificate.public_key.verify(sso_saml_signature_digest(sig_alg), Base64.decode64(signature.to_s), URI.encode_www_form(payload)) rescue false end |
.sso_validate_saml_response!(config, assertion, provider, ctx) ⇒ Object
159 160 161 162 163 164 165 |
# File 'lib/better_auth/sso/plugin/saml_response.rb', line 159 def sso_validate_saml_response!(config, assertion, provider, ctx) validator = config.dig(:saml, :validate_response) return unless validator.respond_to?(:call) return if validator.call(response: assertion, provider: provider, context: ctx) raise APIError.new("BAD_REQUEST", message: "Invalid SAML response") end |
.sso_validate_saml_slo_signature!(ctx, raw_message, error_message) ⇒ Object
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 |
# File 'lib/better_auth/sso/plugin/saml_metadata_and_logout.rb', line 257 def sso_validate_saml_slo_signature!(ctx, , ) signature = sso_fetch(ctx.body, :signature) || sso_fetch(ctx.query, :signature) sig_alg = sso_fetch(ctx.body, :sig_alg) || sso_fetch(ctx.query, :sig_alg) if !signature.to_s.empty? && !sig_alg.to_s.empty? return true if sso_validate_saml_redirect_signature(ctx, , signature, sig_alg) raise APIError.new("BAD_REQUEST", message: ) end xml = Base64.decode64(.to_s.gsub(/\s+/, "")) return true if xml.include?("<Signature") || xml.include?(":Signature") raise APIError.new("BAD_REQUEST", message: ) rescue APIError raise rescue raise APIError.new("BAD_REQUEST", message: ) end |
.sso_validate_saml_timestamp!(conditions, config = {}, now: Time.now.utc) ⇒ Object
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/better_auth/sso/plugin/saml_validation_and_state.rb', line 41 def (conditions, config = {}, now: Time.now.utc) conditions = normalize_hash(conditions || {}) not_before = conditions[:not_before] || conditions[:notBefore] not_on_or_after = conditions[:not_on_or_after] || conditions[:notOnOrAfter] if not_before.to_s.empty? && not_on_or_after.to_s.empty? raise APIError.new("BAD_REQUEST", message: "SAML assertion missing required timestamp conditions") if config.dig(:saml, :require_timestamps) return true end clock_skew_seconds = ((config.dig(:saml, :clock_skew) || SSO_DEFAULT_CLOCK_SKEW_MS).to_f / 1000.0) parsed_not_before = (not_before, "SAML assertion has invalid NotBefore timestamp") unless not_before.to_s.empty? parsed_not_on_or_after = (not_on_or_after, "SAML assertion has invalid NotOnOrAfter timestamp") unless not_on_or_after.to_s.empty? raise APIError.new("BAD_REQUEST", message: "SAML assertion is not yet valid") if parsed_not_before && now < (parsed_not_before - clock_skew_seconds) raise APIError.new("BAD_REQUEST", message: "SAML assertion has expired") if parsed_not_on_or_after && now > (parsed_not_on_or_after + clock_skew_seconds) true end |
.sso_validate_single_saml_assertion!(saml_response) ⇒ Object
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
# File 'lib/better_auth/sso/plugin/saml_validation_and_state.rb', line 22 def sso_validate_single_saml_assertion!(saml_response) xml = Base64.decode64(saml_response.to_s) raise APIError.new("BAD_REQUEST", message: "Invalid base64-encoded SAML response") unless xml.include?("<") assertions = xml.scan(/<(?:\w+:)?Assertion(?:\s|>|\/)/).length encrypted_assertions = xml.scan(/<(?:\w+:)?EncryptedAssertion(?:\s|>|\/)/).length total = assertions + encrypted_assertions raise APIError.new("BAD_REQUEST", message: "SAML response contains no assertions") if total.zero? if total > 1 raise APIError.new("BAD_REQUEST", message: "SAML response contains #{total} assertions, expected exactly 1") end true rescue APIError raise rescue raise APIError.new("BAD_REQUEST", message: "Invalid base64-encoded SAML response") end |