こんにちは!kossyです!
今回は、SAMLでのSSOを実現するGem「ruby-saml」のソースコードを追ってみた(SamlResponseだけ)ので、
備忘録としてブログに残してみたいと思います。
環境
Ruby 2.6.8
Rails 6.0.4.1
MacOS Catalina
なお、IdPにはLINE WORKSを用いていると仮定してコードリーディングしてみます。
そして、リーディング元のレポジトリは拙著で作成したアプリケーションとします。
kossy-web-engineer.hatenablog.com
metadataとは?
そもそもmetadataとはなんでしょうか。
SAML メタデータ・ファイルには、 SAML 2.0 プロトコル・メッセージ交換で使用できるさまざまな SAML 権限に関する情報が含まれています。
このメタデータは、 ID プロバイダー・エンドポイントと証明書を識別し、 SAML 2.0 メッセージ交換を保護します。
SAMLパーティ間で設定情報を表現および共有するXMLのスキーマを定義
SPがIdPを利用するための情報を記述して、IdPとSPの信頼関係を構築できる。
・エンティティのサポートされているSAMLバインディング
・運用上の役割(IDP、SPなど)
・識別子情報、サポートID属性
・および暗号化と署名のための鍵情報
SAML認証では基本的にIdPにServiceProviderの情報を登録する必要があるのですが、この登録時にmetadata(= SP Issuer、EntityIDとも)を取得するエンドポイントを登録します。
LINE WORKSの場合だと以下の画像のように登録します。
より詳しいMetadataの仕様について拙著では解説しませんので、興味のある方は以下のPDFを参考にしてみてください。
https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf
metadataアクション
コードリーディングに移ります。
def metadata settings = Account.get_saml_settings(get_url_base) meta = OneLogin::RubySaml::Metadata.new render :xml => meta.generate(settings, true) end
metadataアクション自体は3行のコードです。
まずはOneLogin::RubySaml::Metadataのinitializeメソッドを見に行ってみます。と思ったらinitializeメソッドが定義されてなかった、、、
なので、generateメソッドを読んでみます。
OneLogin::RubySaml::Metadata#generate
def generate(settings, pretty_print=false, valid_until=nil, cache_duration=nil) meta_doc = XMLSecurity::Document.new add_xml_declaration(meta_doc) root = add_root_element(meta_doc, settings, valid_until, cache_duration) sp_sso = add_sp_sso_element(root, settings) add_sp_certificates(sp_sso, settings) add_sp_service_elements(sp_sso, settings) add_extras(root, settings) embed_signature(meta_doc, settings) output_xml(meta_doc, pretty_print) end
最終的な返り値はXMLかと思われますが、いろいろなメソッドが呼ばれてますので、一つ一つ見ていきます。
add_xml_declaration(meta_doc)
def add_xml_declaration(meta_doc) meta_doc << REXML::XMLDecl.new('1.0', 'UTF-8') end
引数のmeta_docにXMLDeclarationを追加するメソッドでした。
# rails c $ meta_doc = XMLSecurity::Document.new $ meta_doc << REXML::XMLDecl.new('1.0', 'UTF-8') => <UNDEFINED> ... </>
add_root_element
def add_root_element(meta_doc, settings, valid_until, cache_duration) namespaces = { "xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata" } if settings.attribute_consuming_service.configured? namespaces["xmlns:saml"] = "urn:oasis:names:tc:SAML:2.0:assertion" end root = meta_doc.add_element("md:EntityDescriptor", namespaces) root.attributes["ID"] = OneLogin::RubySaml::Utils.uuid root.attributes["entityID"] = settings.sp_entity_id if settings.sp_entity_id root.attributes["validUntil"] = valid_until.strftime('%Y-%m-%dT%H:%M:%S%z') if valid_until root.attributes["cacheDuration"] = "PT" + cache_duration.to_s + "S" if cache_duration root end
SAMLに必要なRootElementを付与するメソッドでした。
add_sp_sso_element
def add_sp_sso_element(root, settings) root.add_element "md:SPSSODescriptor", { "protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol", "AuthnRequestsSigned" => settings.security[:authn_requests_signed], "WantAssertionsSigned" => settings.security[:want_assertions_signed], } end
SPSSODescriptorの説明については以下ブログが参考になりました。
AuthnRequestsSignedについては以下の記事が参考になりました。
AuthnRequestsSigned
このサービス・プロバイダーによって送信される <samlp:AuthnRequest> メッセージに署名するかどうかを指定します。
この変更が最初に加わったのはこのPRのようです。(当初はDefaultでfalseで外部からカスタマイズができない設定だったんですね。)
wantAssertionsSignedについては以下の記述。
wantAssertionsSigned
このサービス・プロバイダーが受信する <saml:Assertion> エレメントが、アサーションに署名する Signature エレメントを含む必要があるかを指定します。
add_sp_certificates
# Add KeyDescriptor if messages will be signed / encrypted # with SP certificate, and new SP certificate if any # メッセージがSP証明書で署名/暗号化される場合はKeyDescriptorを追加し、 # 存在する場合は新しいSP証明書を追加します def add_sp_certificates(sp_sso, settings) cert = settings.get_sp_cert cert_new = settings.get_sp_cert_new for sp_cert in [cert, cert_new] if sp_cert cert_text = Base64.encode64(sp_cert.to_der).gsub("\n", '') kd = sp_sso.add_element "md:KeyDescriptor", { "use" => "signing" } ki = kd.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"} xd = ki.add_element "ds:X509Data" xc = xd.add_element "ds:X509Certificate" xc.text = cert_text if settings.security[:want_assertions_encrypted] kd2 = sp_sso.add_element "md:KeyDescriptor", { "use" => "encryption" } ki2 = kd2.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"} xd2 = ki2.add_element "ds:X509Data" xc2 = xd2.add_element "ds:X509Certificate" xc2.text = cert_text end end end sp_sso end
正直なんのこっちゃなのでコンソールで実行してみます。
$ cert = settings.get_sp_cert => nil $ cert_new = settings.get_sp_cert_new => nil $ for sp_cert in [cert, cert_new] if sp_cert cert_text = Base64.encode64(sp_cert.to_der).gsub("\n", '') kd = sp_sso.add_element "md:KeyDescriptor", { "use" => "signing" } ki = kd.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"} xd = ki.add_element "ds:X509Data" xc = xd.add_element "ds:X509Certificate" xc.text = cert_text if settings.security[:want_assertions_encrypted] kd2 = sp_sso.add_element "md:KeyDescriptor", { "use" => "encryption" } ki2 = kd2.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"} xd2 = ki2.add_element "ds:X509Data" xc2 = xd2.add_element "ds:X509Certificate" xc2.text = cert_text end end end => [nil, nil]
私の環境ではcertもcert_newもnilでした。
理由はsettingsオブジェクトのcertificate属性に値を入れていないからでした。
add_sp_service_elements
def add_sp_service_elements(sp_sso, settings) if settings.single_logout_service_url sp_sso.add_element "md:SingleLogoutService", { "Binding" => settings.single_logout_service_binding, "Location" => settings.single_logout_service_url, "ResponseLocation" => settings.single_logout_service_url } end if settings.name_identifier_format nameid = sp_sso.add_element "md:NameIDFormat" nameid.text = settings.name_identifier_format end if settings.assertion_consumer_service_url sp_sso.add_element "md:AssertionConsumerService", { "Binding" => settings.assertion_consumer_service_binding, "Location" => settings.assertion_consumer_service_url, "isDefault" => true, "index" => 0 } end if settings.attribute_consuming_service.configured? sp_acs = sp_sso.add_element "md:AttributeConsumingService", { "isDefault" => "true", "index" => settings.attribute_consuming_service.index } srv_name = sp_acs.add_element "md:ServiceName", { "xml:lang" => "en" } srv_name.text = settings.attribute_consuming_service.name settings.attribute_consuming_service.attributes.each do |attribute| sp_req_attr = sp_acs.add_element "md:RequestedAttribute", { "NameFormat" => attribute[:name_format], "Name" => attribute[:name], "FriendlyName" => attribute[:friendly_name], "isRequired" => attribute[:is_required] || false } unless attribute[:attribute_value].nil? Array(attribute[:attribute_value]).each do |value| sp_attr_val = sp_req_attr.add_element "saml:AttributeValue" sp_attr_val.text = value.to_s end end end end # With OpenSSO, it might be required to also include # <md:RoleDescriptor xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:query="urn:oasis:names:tc:SAML:metadata:ext:query" xsi:type="query:AttributeQueryDescriptorType" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"/> # <md:XACMLAuthzDecisionQueryDescriptor WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"/> sp_sso end
ServiceProviderの情報をXMLに適宜追加するメソッドですね。
add_extras
# can be overridden in subclass def add_extras(root, _settings) root end
サブクラスでオーバーライドできるとのことでした。
このメソッドが加わったのは以下のPRです。
Conversationを見ると独自で追加したいメタデータを定義する用途で使うメソッドっぽいです。
embed_signature
def embed_signature(meta_doc, settings) return unless settings.security[:metadata_signed] private_key = settings.get_sp_key cert = settings.get_sp_cert return unless private_key && cert meta_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method]) end
XMLに署名を埋め込むメソッドですね。こちらはmetadata_signed属性やsp_key、certificate等がないと処理がスキップされるみたいです。
まとめ
metadataの仕様が分からなさすぎて、ただ処理を追うだけになってしまい「なんでこの処理が必要なのか」までは理解ができませんでした。。。
次はmetadataの仕様を自分なりに調べてまとめた記事になりそう、、、
大いに参考にさせていただいたサイト
素晴らしいコンテンツの提供、誠にありがとうございます。
SAML認証ができるまで - Cybozu Inside Out | サイボウズエンジニアのブログ
SAML Web SSO 2.0 認証 (samlWebSso20)