首页
看点啥
插画图片
首页 科技看点 ruby-saml:实践指南

ruby-saml:实践指南

2026-09-11 0

面对实际交付,我看ruby-saml的重点不在星标,而在这项能力:SAML SSO 适用于 Ruby。在日常自动化场景里,常见问题是输入边界、依赖和失败处理如果不清楚就很难稳定复用,这正是评估时需要盯住的地方。短测时我会用一项范围明确的真实任务完成最小试跑,并保留配置时间、输出质量、异常信息和维护痕迹的结果,方便团队复盘。如果团队属于愿意先做小范围验证并复查原始文档的团队,它有继续测试的理由;否则先看替代方案会更省时间。

红宝石 SAML

Ruby SAML 的次要版本和补丁版本可能会引入重大更改。请阅读 UPGRADING.md 有关升级到新 Ruby SAML 版本的指南。

漏洞公告

CVE-2025-66568 和 CVE-2025-66567。影响版本 ruby-saml < 1.18.0(包括1.12.4),升级到1.18.1

CVE-2025-54572 影响版本 ruby-saml < 1.18.1

有影响 ruby-saml < 1.18.0 的严重漏洞,其中两个允许绕过 SAML 身份验证(CVE-2025-25291、CVE-2025-25292、CVE-2025-25293)。请升级到固定版本(1.18.0)

漏洞报告

如果您认为您在此 gem 中发现了安全漏洞,请报告 通过电子邮件发送给维护者:[email protected]

概述

Ruby SAML 库用于实现 SAML 授权的客户端, i.e。它提供了一种管理授权初始化和确认的方法 来自身份提供商的请求。

SAML 授权是一个两步过程,您应该实现对这两个步骤的支持。

我们为 Rails 4 创建了一个演示项目,它使用该库的最新版本: ruby-saml-示例

安全考虑

支持的 Ruby 版本

CI 测试涵盖以下 Ruby 版本:

开始使用

为了使用 Ruby SAML,您需要安装 gem(手动或使用 Bundler), 并在 Ruby 应用程序中需要该库:

使用Gemfile

# latest stable
gem 'ruby-saml', '~> 1.18.0'

# or track master for bleeding-edge
gem 'ruby-saml', :github => 'saml-toolkits/ruby-saml'

使用RubyGems

gem install ruby-saml

您可能需要整个 Ruby SAML gem:

require 'onelogin/ruby-saml'

或者只是单独需要的组件:

require 'onelogin/ruby-saml/authrequest'

在 Ruby 1.8.7 上安装

这个 gem 使用 Nokogiri 作为依赖项,这在 Nokogiri 1.6 中放弃了对 Ruby 1.8.x 的支持。 在 Ruby 1.8.7 上安装此 gem 时,您需要确保 Nokogiri 的版本 1.6 之前的版本已安装或指定(如果尚未安装)。

使用Gemfile

gem 'nokogiri', '~> 1.5.10'

使用RubyGems

gem install nokogiri --version '~> 1.5.10'

配置日志记录

在对 SAML 集成问题进行故障排除时,您会发现检查 该 gem 业务逻辑的输出。默认情况下,日志消息发送到 RAILS_DEFAULT_LOGGER 当 gem 在 Rails 上下文中使用时,以及当 gem 在 Rails 外部使用时为 STDOUT

要覆盖默认行为并控制日志消息的目标,请提供 gem 的日志记录单例的 ruby Logger 对象:

OneLogin::RubySaml::Logging.logger = Logger.new('/var/log/ruby-saml.log')

初始化阶段

这是您从身份提供商处收到的第一个请求。它会影响你的应用程序 在您宣布为 SAML 初始化点的特定 URL 处。回应 此初始化是重定向回身份提供者,它可以看起来像一些东西 像这样(暂时忽略 saml_settings 方法调用):

def init
  request = OneLogin::RubySaml::Authrequest.new
  redirect_to(request.create(saml_settings))
end

如果 SP 知道谁应该在 IdP 中进行身份验证,它可以提供该信息,如下所示:

def init
  request = OneLogin::RubySaml::Authrequest.new
  saml_settings.name_identifier_value_requested = "[email protected]"
  saml_settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
  redirect_to(request.create(saml_settings))
end

一旦您重定向回身份提供商,它将确保用户已被重定向 授权并重定向回您的应用程序以供最终使用。 这可能看起来像这样(authorize_successauthorize_failure 方法特定于您的应用程序):

def consume
  response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], :settings => saml_settings)

  # We validate the SAML Response and check if the user already exists in the system
  if response.is_valid?
     # authorize_success, log the user
     session[:userid] = response.nameid
     session[:attributes] = response.attributes
  else
    authorize_failure  # This method shows an error message
    # List of errors is available in response.errors array
  end
end

上面有一些假设,其中之一是 response.nameid 是电子邮件地址。 这一切都通过您如何通过 saml_settings 方法指定正在使用的设置来处理。 可以按照以下方式实施:

response = OneLogin::RubySaml::Response.new(params[:SAMLResponse])
response.settings = saml_settings

如果SAMLResponse的断言没有加密,可以初始化Response 不带:settings参数,稍后再设置。如果 SAMLResponse 包含加密的 断言,您需要在初始化方法中提供设置才能获取 解密断言,使用服务提供商私钥进行解密。 如果您不知道期望什么,请始终使用前者(在初始化时设置设置)。

def saml_settings
  settings = OneLogin::RubySaml::Settings.new

  settings.assertion_consumer_service_url = "http://#{request.host}/saml/consume"
  settings.sp_entity_id                   = "http://#{request.host}/saml/metadata"
  settings.idp_entity_id                  = "https://app.onelogin.com/saml/metadata/#{OneLoginAppId}"
  settings.idp_sso_service_url            = "https://app.onelogin.com/trust/saml2/http-post/sso/#{OneLoginAppId}"
  settings.idp_sso_service_binding        = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" # or :post, :redirect
  settings.idp_slo_service_url            = "https://app.onelogin.com/trust/saml2/http-redirect/slo/#{OneLoginAppId}"
  settings.idp_slo_service_binding        = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" # or :post, :redirect
  settings.idp_cert_fingerprint           = OneLoginAppCertFingerPrint
  settings.idp_cert_fingerprint_algorithm = "http://www.w3.org/2000/09/xmldsig#sha1"
  settings.name_identifier_format         = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"

  # Optional for most SAML IdPs
  settings.authn_context = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
  # or as an array
  settings.authn_context = [
    "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
    "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
  ]

  # Optional bindings (defaults to Redirect for logout POST for ACS)
  settings.single_logout_service_binding      = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" # or :post, :redirect
  settings.assertion_consumer_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" # or :post, :redirect

  settings
end

自版本 1.11.0 起,不推荐使用 settings.issuer,转而使用 settings.sp_entity_id

通过将参数传递给 OneLogin::RubySaml::Response.new() 可以跳过一些断言验证。 例如,您可以跳过 AuthnStatementConditionsRecipientSubjectConfirmation 通过使用不同选项初始化响应来进行验证:

response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], {skip_authnstatement: true}) # skips AuthnStatement
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], {skip_conditions: true}) # skips conditions
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], {skip_subject_confirmation: true}) # skips subject confirmation
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], {skip_recipient_check: true}) # doesn't skip subject confirmation, but skips the recipient check which is a sub check of the subject_confirmation check
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], {skip_audience: true}) # skips audience check

剩下的就是将所有内容包装在控制器中并在初始化和引用中引用它 OneLogin 中消耗 URLs。完整的控制器示例如下所示:

# This controller expects you to use the URLs /saml/init and /saml/consume in your OneLogin application.
class SamlController < ApplicationController
  def init
    request = OneLogin::RubySaml::Authrequest.new
    redirect_to(request.create(saml_settings))
  end

  def consume
    response          = OneLogin::RubySaml::Response.new(params[:SAMLResponse])
    response.settings = saml_settings

    # We validate the SAML Response and check if the user already exists in the system
    if response.is_valid?
       # authorize_success, log the user
       session[:userid] = response.nameid
       session[:attributes] = response.attributes
    else
      authorize_failure  # This method shows an error message
      # List of errors is available in response.errors array
    end
  end

  private

  def saml_settings
    settings = OneLogin::RubySaml::Settings.new

    settings.assertion_consumer_service_url = "http://#{request.host}/saml/consume"
    settings.sp_entity_id                   = "http://#{request.host}/saml/metadata"
    settings.idp_sso_service_url             = "https://app.onelogin.com/saml/signon/#{OneLoginAppId}"
    settings.idp_cert_fingerprint           = OneLoginAppCertFingerPrint
    settings.name_identifier_format         = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"

    # Optional for most SAML IdPs
    settings.authn_context = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"

    # Optional. Describe according to IdP specification (if supported) which attributes the SP desires to receive in SAMLResponse.
    settings.attributes_index = 5
    # Optional. Describe an attribute consuming service for support of additional attributes.
    settings.attribute_consuming_service.configure do
      service_name "Service"
      service_index 5
      add_attribute :name => "Name", :name_format => "Name Format", :friendly_name => "Friendly Name"
    end

    settings
  end
end

签名验证

Ruby SAML 允许使用不同的方式来验证 SAMLResponse 的签名:

验证重定向绑定签名时,指纹无用,证书 需要 IdP 才能执行验证。您可以通过该选项 如果想避免签名,则将 :relax_signature_validation 更改为 SloLogoutrequestLogoutresponse 如果没有提供IdP的证书,则进行验证。

在生产中,我们也强烈建议在设置上注册 IdP 证书 使用指纹法。指纹是一个哈希值,因此最终可能会发生冲突 可以绕过签名验证而结束的攻击。其他 SAML 工具包已弃用该机制, 我们维护它是为了兼容性,也为了在测试环境中使用。

处理多个 IdP 证书

如果 IdP 元数据 XML 包含多个证书,则可以指定 idp_cert_multi 参数。使用时,idp_certidp_cert_fingerprint 参数将被忽略。 这在以下场景中很有用:

idp_cert_multi 必须是 Hash,如下所示。下面的 :signing:encryption 数组, 添加在 IdP 元数据中发布的 IdP X.509 公共证书。

{
  :signing => [],
  :encryption => []
}

基于元数据的配置

上述方法需要一些额外的工作来手动指定有关 IdP 和 SP 应用程序的属性。 有一个更简单的方法:使用元数据交换。元数据是一个 XML 文件,定义了 IdP 的功能 和 SP 应用程序。它还包含添加到信任关系的 X.509 公钥证书。 IdP 管理员还可以根据元数据为 SP 配置自定义设置。

使用 IdpMetadataParser#parse_remote,IdP 元数据将添加到设置中。

def saml_settings

  idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
  # Returns OneLogin::RubySaml::Settings pre-populated with IdP metadata
  settings = idp_metadata_parser.parse_remote("https://example.com/auth/saml2/idp/metadata")

  settings.assertion_consumer_service_url = "http://#{request.host}/saml/consume"
  settings.sp_entity_id                   = "http://#{request.host}/saml/metadata"
  settings.name_identifier_format         = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
  # Optional for most SAML IdPs
  settings.authn_context = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"

  settings
end

设置以下属性:

当元数据中存在多个实体描述符时,检索一个实体描述符

如果元数据包含多个实体,则相关实体 从设置中检索设置时可以指定描述符 IdpMetadataParser 通过其实体 ID 值:

  validate_cert = true
  settings = idp_metadata_parser.parse_remote(
               "https://example.com/auth/saml2/idp/metadata",
               validate_cert,
               entity_id: "http//example.com/target/entity"
             )

当有多个实体描述符可用时,检索一个具有特定绑定和 nameid 格式的实体描述符

如果元数据包含多个绑定且 NameID 格式,则相关的 从 IdpMetadataParser 检索设置时可以指定 通过绑定和 NameID 的值:

  validate_cert = true
  options = {
    entity_id: "http//example.com/target/entity",
    name_id_format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
    sso_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
    slo_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
  }
  settings = idp_metadata_parser.parse_remote(
               "https://example.com/auth/saml2/idp/metadata",
               validate_cert,
               options
             )

将元数据解析为哈希

OneLogin::RubySaml::IdpMetadataParser还提供方法#parse_to_hash#parse_remote_to_hash。 它们返回一个哈希值而不是 Settings 对象,这对于配置可能有用 例如,omniauth-saml。

验证元数据签名并检索设置

目前 ruby_saml 没有方法来验证将要解析的元数据的签名,但可以按如下方式完成:

require "xml_security"
require "onelogin/ruby-saml/utils"
require "onelogin/ruby-saml/idp_metadata_parser"

url = ""
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new

uri = URI.parse(url)
raise ArgumentError.new("url must begin with http or https") unless /^https?/ =~ uri.scheme
http = Net::HTTP.new(uri.host, uri.port)
if uri.scheme == "https"
    http.use_ssl = true
    http.verify_mode = OpenSSL::SSL::VERIFY_PEER
end

get = Net::HTTP::Get.new(uri.request_uri)
get.basic_auth uri.user, uri.password if uri.user
response = http.request(get)
xml = response.body
errors = []
doc = XMLSecurity::SignedDocument.new(xml, errors)
cert_str = ""
cert = OneLogin::RubySaml::Utils.format_cert(cert_str)
metadata_sign_cert = OpenSSL::X509::Certificate.new(cert)
valid = doc.validate_document_with_cert(metadata_sign_cert, true)
if valid
  settings = idp_metadata_parser.parse(
    xml,
    entity_id: ""
  )
else
  print "Metadata Signature failed to be verified with the cert provided"
end

检索属性

如果您使用saml:AttributeStatement传输数据,例如用户名,则可以通过response.attributes访问所有属性。它包含所有saml:AttributeStatements,其“名称”作为无关键,并包含一个或多个saml:AttributeValues作为值。返回的值取决于 single_value_compatibility(激活时,仅返回第一个值)

response = OneLogin::RubySaml::Response.new(params[:SAMLResponse])
response.settings = saml_settings

response.attributes[:username]

想象一下这个 saml:AttributeStatement

  
    
      demo
    
    
      value1
      value2
    
    
      role1
    
    
      role2
      role3
    
    
      
    
    
      
      valuePresent
      
      
    
    
      usersName
    
  
pp(response.attributes)   # is an OneLogin::RubySaml::Attributes object
# => @attributes=
  {"uid"=>["demo"],
   "another_value"=>["value1", "value2"],
   "role"=>["role1", "role2", "role3"],
   "attribute_with_nil_value"=>[nil],
   "attribute_with_nils_and_empty_strings"=>["", "valuePresent", nil, nil]
   "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"=>["usersName"]}>

# Active single_value_compatibility
OneLogin::RubySaml::Attributes.single_value_compatibility = true

pp(response.attributes[:uid])
# => "demo"

pp(response.attributes[:role])
# => "role1"

pp(response.attributes.single(:role))
# => "role1"

pp(response.attributes.multi(:role))
# => ["role1", "role2", "role3"]

pp(response.attributes.fetch(:role))
# => "role1"

pp(response.attributes[:attribute_with_nil_value])
# => nil

pp(response.attributes[:attribute_with_nils_and_empty_strings])
# => ""

pp(response.attributes[:not_exists])
# => nil

pp(response.attributes.single(:not_exists))
# => nil

pp(response.attributes.multi(:not_exists))
# => nil

pp(response.attributes.fetch(/givenname/))
# => "usersName"

# Deprecated single_value_compatibility
OneLogin::RubySaml::Attributes.single_value_compatibility = false

pp(response.attributes[:uid])
# => ["demo"]

pp(response.attributes[:role])
# => ["role1", "role2", "role3"]

pp(response.attributes.single(:role))
# => "role1"

pp(response.attributes.multi(:role))
# => ["role1", "role2", "role3"]

pp(response.attributes.fetch(:role))
# => ["role1", "role2", "role3"]

pp(response.attributes[:attribute_with_nil_value])
# => [nil]

pp(response.attributes[:attribute_with_nils_and_empty_strings])
# => ["", "valuePresent", nil, nil]

pp(response.attributes[:not_exists])
# => nil

pp(response.attributes.single(:not_exists))
# => nil

pp(response.attributes.multi(:not_exists))
# => nil

pp(response.attributes.fetch(/givenname/))
# => ["usersName"]

AuthNRequest的saml:AuthnContextClassRef可以由settings.authn_context提供;可能的值在[SAMLAuthnCxt]中描述。比较方法可以使用settings.authn_context_comparison参数来设置。可能的值包括:“精确”、“更好”、“最大”和“最小”(默认值为“精确”)。 要添加 saml:AuthnContextDeclRef,请定义 settings.authn_context_decl_ref

在SP-发起的流程中,SP可以向IdP指示应该认证的主体。这是通过之前定义 settings.name_identifier_value_requested 来完成的 构建 authrequest 对象。

服务提供商元数据

为了与IdP形成可信配对关系,SP(您)需要提供元数据XML 出于各种充分的理由,将其发送给 IdP。 (缓存、证书查找、中继方权限等)

OneLogin::RubySaml::Metadata 通过读取设置并返回 XML 来处理此问题。 你所要做的就是添加一个控制器来返回数据,然后将这个URL交给IdP管理员。

元数据将由 IdP 每隔几分钟轮询一次,因此更新您的设置应该传播 到 IdP 设置。

class SamlController < ApplicationController
  # ... the rest of your controller definitions ...
  def metadata
    settings = Account.get_saml_settings
    meta = OneLogin::RubySaml::Metadata.new
    render :xml => meta.generate(settings), :content_type => "application/samlmetadata+xml"
  end
end

您可以使用以下命令将 ValidUntilCacheDuration 添加到 SP 元数据 XML:

  # Valid until => 2 days from now
  # Cache duration = 604800s = 1 week
  valid_until = Time.now + 172800
  cache_duration = 604800
  meta.generate(settings, false, valid_until, cache_duration)

签名与解密

Ruby SAML 支持以下功能:

  1. 签署您的 SP 元数据 XML
  2. 签署您的 SP SAML 消息
  3. 收到后解密 IdP 断言消息 (EncryptedAssertion)
  4. 验证 SAML 消息和 IdP 断言上的签名

为了使用上面的功能1-3,您必须首先定义您的SP公共证书和私钥:

  settings.certificate = "CERTIFICATE TEXT WITH BEGIN/END HEADER AND FOOTER"
  settings.private_key = "PRIVATE KEY TEXT WITH BEGIN/END HEADER AND FOOTER"

请注意,相同的证书(及其关联的私钥)用于执行 上面所有与解密和签名相关的功能 (1-4)。 Ruby SAML 当前不允许 为每个函数指定不同的证书。

您还可以全局设置 SP 签名和摘要方法,用于 SP 签名(上面的函数 1 和 2):

  settings.security[:digest_method]    = XMLSecurity::Document::SHA1
  settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1

签署SP元数据

您可以使用以下设置将 数字签名元素添加到 SP 元数据 XML 中:

  settings.certificate = "CERTIFICATE TEXT WITH BEGIN/END HEADER AND FOOTER"
  settings.private_key = "PRIVATE KEY TEXT WITH BEGIN/END HEADER AND FOOTER"

  settings.security[:metadata_signed] = true # Enable signature on Metadata

签署 SP SAML 消息

Ruby SAML 支持 SAML 请求签名。服务提供商将签署 request/responses 及其私钥。然后身份提供者将验证签名 收到的 request/responses 与服务提供商的公共 X.509 证书。

要启用,请首先设置您的证书和私钥。这将添加 到您的 SP 元数据 XML,由 IdP 读取。

  settings.certificate = "CERTIFICATE TEXT WITH BEGIN/END HEADER AND FOOTER"
  settings.private_key = "PRIVATE KEY TEXT WITH BEGIN/END HEADER AND FOOTER"

接下来,您可以指定要签名的特定 SP SAML 消息:

  settings.security[:authn_requests_signed]   = true  # Enable signature on AuthNRequest
  settings.security[:logout_requests_signed]  = true  # Enable signature on Logout Request
  settings.security[:logout_responses_signed] = true  # Enable signature on Logout Response

HTTP-RedirectHTTP-POST 绑定的签名将自动处理。 请注意,在 HTTP-Redirect 绑定上创建签名时使用 RelayState 参数。 如果您要发送 GET RelayState 参数或 身份提供者处的签名验证过程将失败。

解密 IdP SAML 断言

Ruby SAML 支持 EncryptedAssertion。身份提供者将使用以下内容加密断言 服务提供商的公共证书。服务提供商将用其私钥解密EncryptedAssertion。

您可以按如下方式启用 EncryptedAssertion。这会将 添加到您的 SP 元数据 XML,由 IdP 读取。

  settings.certificate = "CERTIFICATE TEXT WITH BEGIN/END HEADER AND FOOTER"
  settings.private_key = "PRIVATE KEY TEXT WITH BEGIN/END HEADER AND FOOTER"

  settings.security[:want_assertions_encrypted] = true # Invalidate SAML messages without an EncryptedAssertion

验证 IdP 断言上的签名

您可能需要 IdP 使用以下设置签署其 SAML 断言。 这会将 添加到您的 SP 元数据 XML 中。 将根据 元素检查签名 存在于 IdP 的元数据中。

  settings.security[:want_assertions_signed]  = true  # Require the IdP to sign its SAML Assertions

证书和签名验证

您可以使用以下设置要求 SP 和 IdP 证书不过期:

  settings.security[:check_idp_cert_expiration] = true  # Raise error if IdP X.509 cert is expired
  settings.security[:check_sp_cert_expiration] = true   # Raise error SP X.509 cert is expired

默认情况下,如果签名或证书,Ruby SAML 将引发 OneLogin::RubySaml::ValidationError 验证失败。您可以使用 settings.security[:soft] 参数禁用此类异常。

  settings.security[:soft] = true  # Do not raise error on failed signature/certificate validations

高级 SP 证书使用和密钥滚动更新

Ruby SAML 提供了 settings.sp_cert_multi 参数来启用以下功能 高级使用场景:

sp_cert_multi 参数替换 certificateprivate_key (您不能同时指定这两个参数。)sp_cert_multi 具有以下形状:

settings.sp_cert_multi = {
  signing: [
    { certificate: cert1, private_key: private_key1 },
    { certificate: cert2, private_key: private_key2 }
  ],
  encryption: [
    { certificate: cert1, private_key: private_key1 },
    { certificate: cert3, private_key: private_key1 }
  ],
}

证书轮换是通过在每个列表的底部插入新证书来实现的, 然后在 IdPs 迁移后从列表顶部删除旧证书。 应用程序的常见做法是在 URL 端点发布当前的 SP 元数据,并具有 IdP 定期轮询更新。

请注意以下事项:

观众验证

仅当 IdP 包含 时,服务提供商才应认为 SAML 响应有效 包含唯一标识服务提供商的 元素的元素。除非您指定 skip_audience 选项,Ruby SAML 将验证每个 SAML 响应是否包含 元素 其内容与 settings.sp_entity_id 匹配。

默认情况下,Ruby SAML 认为 元素仅包含空 元素 是有效的。这意味着具有如下条件的有效 SAML 响应将是有效的:


  

您可以强制 元素仅包含空 元素 使用 settings.security[:strict_audience_validation] 参数无效。

settings.security[:strict_audience_validation] = true

单点注销

Ruby SAML 支持 SP- 启动单点注销和 IdP-Initiated 单点注销。

以下是我们可以添加到之前的控制器中以生成 SAML 注销请求并将其发送到 IdP 的示例:

# Create a SP initiated SLO
def sp_logout_request
  # LogoutRequest accepts plain browser requests w/o parameters
  settings = saml_settings

  if settings.idp_slo_service_url.nil?
    logger.info "SLO IdP Endpoint not found in settings, then executing a normal logout'"
    delete_session
  else

    logout_request = OneLogin::RubySaml::Logoutrequest.new
    logger.info "New SP SLO for userid '#{session[:userid]}' transactionid '#{logout_request.uuid}'"

    if settings.name_identifier_value.nil?
      settings.name_identifier_value = session[:userid]
    end

    # Ensure user is logged out before redirect to IdP, in case anything goes wrong during single logout process (as recommended by saml2int [SDP-SP34])
    logged_user = session[:userid]
    logger.info "Delete session for '#{session[:userid]}'"
    delete_session

    # Save the transaction_id to compare it with the response we get back
    session[:transaction_id] = logout_request.uuid
    session[:logged_out_user] = logged_user

    relayState = url_for(controller: 'saml', action: 'index')
    redirect_to(logout_request.create(settings, :RelayState => relayState))
  end
end

该方法处理 IdP 发送的 SAML 注销响应作为 SAML 注销请求的回复:

# After sending an SP initiated LogoutRequest to the IdP, we need to accept
# the LogoutResponse, verify it, then actually delete our session.
def process_logout_response
  settings = Account.get_saml_settings

  if session.has_key? :transaction_id
    logout_response = OneLogin::RubySaml::Logoutresponse.new(params[:SAMLResponse], settings, :matches_request_id => session[:transaction_id])
  else
    logout_response = OneLogin::RubySaml::Logoutresponse.new(params[:SAMLResponse], settings)
  end

  logger.info "LogoutResponse is: #{logout_response.to_s}"

  # Validate the SAML Logout Response
  if not logout_response.validate
    logger.error "The SAML Logout Response is invalid"
  else
    # Actually log out this session
    logger.info "SLO completed for '#{session[:logged_out_user]}'"
    delete_session
  end
end

# Delete a user's session.
def delete_session
  session[:userid] = nil
  session[:attributes] = nil
  session[:transaction_id] = nil
  session[:logged_out_user] = nil
end

下面是一个示例,我们可以将其添加到之前的控制器中,以处理来自 IdP 的 SAML 注销请求,并向 IdP 回复 SAML 注销响应:

# Method to handle IdP initiated logouts
def idp_logout_request
  settings = Account.get_saml_settings
  # ADFS URL-Encodes SAML data as lowercase, and the toolkit by default uses
  # uppercase. Turn it True for ADFS compatibility on signature verification
  settings.security[:lowercase_url_encoding] = true

  logout_request = OneLogin::RubySaml::SloLogoutrequest.new(
    params[:SAMLRequest], settings: settings
  )
  if !logout_request.is_valid?
    logger.error "IdP initiated LogoutRequest was not valid!"
    return render :inline => logger.error
  end
  logger.info "IdP initiated Logout for #{logout_request.name_id}"

  # Actually log out this session
  delete_session

  # Generate a response to the IdP.
  logout_request_id = logout_request.id
  logout_response = OneLogin::RubySaml::SloLogoutresponse.new.create(settings, logout_request_id, nil, :RelayState => params[:RelayState])
  redirect_to logout_response
end

所有提到的方法都可以在一个独特的视图中处理:

# Trigger SP and IdP initiated Logout requests
def logout
  # If we're given a logout request, handle it in the IdP logout initiated method
  if params[:SAMLRequest]
    return idp_logout_request
  # We've been given a response back from the IdP, process it
  elsif params[:SAMLResponse]
    return process_logout_response
  # Initiate SLO (send Logout Request)
  else
    return sp_logout_request
  end
end

时钟漂移

服务器时钟往往会自然漂移。如果在验证响应期间收到错误“当前时间早于 NotBefore 条件”,这可能是由于您的系统与身份提供商的系统之间的时钟差异造成的。

首先,确保两个系统同步其时钟,例如使用行业标准 网络时间协议 (NTP)。

即使如此,您也可能会遇到间歇性问题,因为身份提供商的时钟可能会稍微提前于您的系统时钟。为了允许少量时钟漂移,您可以通过传入名为 :allowed_clock_drift 的选项来初始化响应。其值必须以秒数(and/or 小数)给出。在针对 NotBefore 断言进行测试之前,给定的值将添加到验证响应的当前时间。例如:

response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], :allowed_clock_drift => 1.second)

确保将该值保持在尽可能小的范围内,以将安全风险降至最低。

通货紧缩限制

为了防止减压炸弹(DoS 攻击的一种形式),SAML 消息默认限制为 250,000 字节。 有时合法的 SAML 消息会超出此限制, 例如,由于自定义声明(例如包含用户所属的组)。 如果要自定义此限制,则需要在初始化响应对象时提供不同的设置。 示例:

def consume
  response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], { settings: saml_settings })
  ...
end

private

def saml_settings
  OneLogin::RubySaml::Settings.new(message_max_bytesize: 500_000)
end

属性服务

要从 IdP 请求属性,SP 必须在其元数据中提供属性服务并引用断言中的索引。

settings = OneLogin::RubySaml::Settings.new
settings.attributes_index = 5
settings.attribute_consuming_service.configure do
  service_name "Service"
  service_index 5
  add_attribute :name => "Name", :name_format => "Name Format", :friendly_name => "Friendly Name"
  add_attribute :name => "Another Attribute", :name_format => "Name Format", :friendly_name => "Friendly Name", :attribute_value => "Attribute Value"
end

attribute_value 选项还接受可能值的数组。

自定义元数据字段

某些 IdPs 可能需要 SPs 添加其他字段(组织、ContactPerson 等) 进入 SP 元数据。这可以通过扩展 OneLogin::RubySaml::Metadata 来实现 类并重写 #add_extras 方法,如下例所示:

class MyMetadata < OneLogin::RubySaml::Metadata
  def add_extras(root, _settings)
    org = root.add_element("md:Organization")
    org.add_element("md:OrganizationName", 'xml:lang' => "en-US").text = 'ACME Inc.'
    org.add_element("md:OrganizationDisplayName", 'xml:lang' => "en-US").text = 'ACME'
    org.add_element("md:OrganizationURL", 'xml:lang' => "en-US").text = 'https://www.acme.com'

    cp = root.add_element("md:ContactPerson", 'contactType' => 'technical')
    cp.add_element("md:GivenName").text = 'ACME SAML Team'
    cp.add_element("md:EmailAddress").text = '[email protected]'
  end
end

# Output XML with custom metadata
MyMetadata.new.generate(settings)

防止重放攻击

重放攻击是指攻击者拦截有效的 SAML 断言并在稍后“重放”它以获得未经授权的访问。

该库仅检查断言的有效性窗口(NotBeforeNotOnOrAfter 条件)。攻击者可以在此窗口内多次重播有效断言。

强大的防御需要跟踪断言 IDs 以确保任何给定的断言仅被接受一次。

1.验证后提取断言ID

成功验证响应后,获取断言 ID。该库通过 response.assertion_id 提供此功能。

2. 存储 ID 并设置过期时间

您必须将此 ID 存储在跨服务器共享的持久缓存(如 Redis 或 Memcached)中。不要将其存储在用户的会话中,因为这不是安全的缓存。

应存储 ID 直到断言的有效性窗口过去。您需要检查受信任的 IdPs 认为断言有效的时间长短,然后添加 allowed_clock_drift。

您可以定义一个全局值,也可以根据re+allowed_clock_driftnot_on_or_after值动态设置该值。

# In your `consume` action, after a successful validation:
if response.is_valid?
  # Prevent replay of this specific assertion
  assertion_id = response.assertion_id
  authorize_failure("Assertion ID is mandatory") if assertion_id.nil?

  assertion_not_on_or_after = response.not_on_or_after
  # We set a default of 5 min expiration in case is not provided
  assertion_expiry = (Time.now.utc + 300) if assertion_not_on_or_after.nil?

  # `is_new_assertion?` is your application's method to check and set the ID
  # in a shared, persistent cache (e.g., Redis, Memcached).
  if is_new_assertion?(assertion_id, expires_at: assertion_expiry)
    # This is a new assertion, so we can proceed
    session[:userid] = response.nameid
    session[:attributes] = response.attributes
    # ...
  else
    # This assertion ID has been seen before. This is a REPLAY ATTACK.
    # Log the security event and reject the user.
    authorize_failure("Replay attack detected")
  end
else
  authorize_failure("Invalid response")
end

您的 is_new_assertion? 方法看起来像这样(Redis 的示例):


def is_new_assertion?(assertion_id, expires_at)
  ttl = (expires_at - Time.now.utc).to_i
  return false if ttl <= 0 # The assertion has already expired

  # The 'nx' option tells Redis to only set the key if it does not already exist.
  # The command returns `true` if the key was set, `false` otherwise.
  $redis.set("saml_assertion_ids:#{assertion_id}", "1", ex: ttl, nx: true)
end

通过 InResponseTo 验证强制执行 SP-Initiated Flow

这是防止 IdP-initiated 登录并确保您只接受最近请求的断言的最佳方法。

1. 存储 AuthnRequest ID

当您创建 AuthnRequest 时,库会为其分配唯一的 ID。您必须存储此 ID,例如将用户重定向到 IdP 之前*将其存储在用户会话中。

def init
  request = OneLogin::RubySaml::Authrequest.new
  # The unique ID of the request is in request.uuid
  session[:saml_request_id] = request.uuid
  redirect_to(request.create(saml_settings))
end

2. 使用存储的 ID 验证 Response 的 InResponseTo 值

当您处理 SAMLResponse 时,从会话中检索 ID 并将其传递给 Response 构造函数。使用session.delete确保ID只能使用一次。

def consume
  request_id = session.delete(:saml_request_id) # Use delete to prevent re-use

  # You can reject the response if no previous saml_request_id was stored
  raise "IdP-initiaited detected" if request_id.nil?

  response = OneLogin::RubySaml::Response.new(
    params[:SAMLResponse],
    settings: saml_settings,
    matches_request_id: request_id
  )

  if response.is_valid?
    # ... authorize user
  else
    # Response is invalid, errors in response.errors
  end
end
喜欢(0)

上一篇

pokeruby:实践指南

pokeruby:实践指南

下一篇

RaspberryPi-WebRTC:实践指南

RaspberryPi-WebRTC:实践指南
猜你喜欢