LoginSignup
4
2

IDaaSの開発経験を踏まえたSAMLの仕組み

Last updated at Posted at 2023-12-21

SmartHR Advent Calendar 2023 シリーズ2の22日目です。

IDaaSの開発を行い、100以上のSaaSとSAMLの連携を行ってきましたので、IDaaSの開発経験を踏まえてSAMLの仕組みについて記載します。

SAMLとは

SAMLは、「Security Assertion Markup Language」の頭文字を取ったもので「サムル」と読みます。
SAMLは、異なるクラウドサービス間でユーザー認証を行うためのXMLをベースにした標準規格です。
標準化団体のOASIS(Organization for the Advancement of Structured Information Standards)によって策定され、2005年に定義されたバージョン2.0(SAML2.0)が現在も主流です。

SAMLを使うことで、利用するクラウドサービスがSAMLに対応していれば、シングルサインオン(以下「SSO」といいます。)を容易に実現できます。

SAMLのメリット

SAMLには、以下のようなメリットがあります。

  1. 利便性の向上
    SAML認証でSSOをおこなうことで、パスワード管理の必要がなくなります。
    SSOを利用することで管理するIDとパスワードがひとつになるという点は、ユーザーの利便性の向上し、負担の減少につながるメリットがあります。

  2. セキュリティの向上
    SAML認証は、Identify Provider(以下「IdP」といいます。)という認証情報を管理するサービスがユーザーの認証情報を一元管理し、認証情報をXMLベースのSAMLアサーションとしてサービスプロバイダ(以下「SP」といいます。)に送信する事でSSOを実現します。
    この仕組みにより、複数のSaaSに対してそれぞれ別のパスワードを管理する必要がなくなり、パスワード漏洩のリスクを大幅に低減することが可能になります。
    また、SaaSによっては、SAML認証の設定を行う事でIDとパスワードによるログインができなくなる為、不正アクセスの防止にもなります。

SAMLの問題点

利便性の向上やセキュリティの向上等、メリットも多いSAMLですが、IDaaS側から見た問題点が以下になります。

  1. SaaS側がSAML非対応の場合は対応できない
    全てのSaaSがSAML認証に対応しているわけではなく、SaaS側が対応していない場合はSAML認証は行えません。

  2. SAMLの利用が導入企業のコスト増加になるケースが多い
    SAML対応しているSaaSでSAMLを利用する場合、有料オプションになるか、上位プランの契約が必要になるケースも多く、SAMLの利用がコスト増加になる事もあります。

SAML認証の仕組み

SAML認証の仕組みは、ユーザー、SP、IdPの3者間で成立します。

  • ユーザー
    • クラウドサービスの利用者のことを指します。
  • SP
    • Service Providerの略で認証情報を利用する側です。SaaSのことを指します。
  • IdP
    • Identity Providerの略で認証情報を管理・提供するサービスのことを指します。ユーザーの認証情報をSPへ渡すという役割を持っており、IDaaSはこの役割を担います。

SAMLでは主にSPとIdPの間で信頼関係を構築し、認証情報のやりとりを行います。

SAML認証のフロー

SAML認証のフローは、SPからの認証フローと、IdPからの認証フローの2種類の認証フローがあります。

SPからの認証フロー

SPが起点になる認証フローで「SP-Initiated」といいます。

このフローはユーザーがSPにログインしようとすることから始まり、以下のような順番で処理が行われます。

  1. SPのログイン画面を表示し、ログインボタンを押す
  2. SPが「SAML Request」という認証リクエストをIdPに送る
  3. IdPはユーザーの認証を行う為、IdPへのログインを求める ※IdPにログイン済みの場合は、5へ
  4. IdPへログインする
  5. IdPは受け取ったSAML RequestとIdPにログインしているユーザー情報を元に「SAML Response」を返却する
  6. 「SAML Response」をSPに送る
    7. SPは受け取ったSAML Responseを検証し、問題なければSAML Responseに記載されているユーザー情報を元にログイン処理を行う

IdPからの認証フロー

IdPが起点になる認証フローで「IdP-Initiated」といいます。

このフローはユーザーがIdP画面にあるSPへログインしようとすることから始まり、以下のような順番で処理が行われます。

  1. IdPの画面に用意されているSPへのログインボタンを押す
  2. IdPはログインしているユーザー情報を元に「SAML Response」を返却する
  3. 「SAML Response」をSPに送る
  4. SPは受け取ったSAML Responseを検証し、問題なければSAML Responseに記載されているユーザー情報を元にログイン処理を行う

「SP-Initiated」「IdP-Initiated」ですが、SaaSによって対応が異なります。
「SP-Initiated」のみ対応しているSaaSもあれば、その逆で「IdP-Initiated」のみ対応している、または両方対応しているというケースもあります。

SAMLでやりとりする情報

「SP-Initiated」「IdP-Initiated」のどちらのフローも「SAML Response」を生成して、返却します。
「SAML Response」の内容について記載します。

「SAML Response」の構成は、大まかに分けると次のように分けられます。

  • Response
    • Responseに関する情報
  • Issuer
    • 発行者(IdP)に関する情報
  • Singature
    • 署名
  • Status
    • 結果のStatusCode
  • Assertion
    • 認証情報

以下は、Responseに署名を含めた「SAML Response」のサンプルになります。

<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="pfx31b3c63c-b358-606a-eaa1-041a98dbd6d8" Version="2.0" IssueInstant="2014-07-17T01:01:48Z" Destination="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685">
  <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
  <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
    <ds:SignedInfo>
    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
    <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
    <ds:Reference URI="#pfx31b3c63c-b358-606a-eaa1-041a98dbd6d8">
     <ds:Transforms>
      <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
      <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
     </ds:Transforms>
     <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
     <ds:DigestValue>P/Jl9zcgzgdZhTJ9F1ePzdi2CLU=</ds:DigestValue>
    </ds:Reference>
   </ds:SignedInfo>
   <ds:SignatureValue>Lz+2Icy4lgmeJqyzxhmRh6jwiBr07RXq3ePCPGOXTIzJ6h7Tl1tLDuVI9XTG5oMjKO4YcFa6e9xak7MQGT+lb3W8uuSWi8KNlz9xUal6NwIpbd2IBlSjn/rYQZh1iUDisKTFW1EXdehM0SQSr7oQ65Hu0wQGpRAk1aMJ6NZJ5T8=</ds:SignatureValue>
   <ds:KeyInfo>
    <ds:X509Data>
     <ds:X509Certificate>MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg==</ds:X509Certificate>
    </ds:X509Data>
   </ds:KeyInfo>
  </ds:Signature>
  <samlp:Status>
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
  </samlp:Status>
  <saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="_d71a3a8e9fcc45c9e9d248ef7049393fc8f04e5f75" Version="2.0" IssueInstant="2014-07-17T01:01:48Z">
    <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
    <saml:Subject>
      <saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
      <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/>
      </saml:SubjectConfirmation>
    </saml:Subject>
    <saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
      <saml:AudienceRestriction>
        <saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
      </saml:AudienceRestriction>
    </saml:Conditions>
    <saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
      <saml:AuthnContext>
        <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
      </saml:AuthnContext>
    </saml:AuthnStatement>
    <saml:AttributeStatement>
      <saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
        <saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
        <saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="eduPersonAffiliation" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
        <saml:AttributeValue xsi:type="xs:string">users</saml:AttributeValue>
        <saml:AttributeValue xsi:type="xs:string">examplerole1</saml:AttributeValue>
      </saml:Attribute>
    </saml:AttributeStatement>
  </saml:Assertion>
</samlp:Response>

Responseに関する情報

<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="pfx31b3c63c-b358-606a-eaa1-041a98dbd6d8" Version="2.0" IssueInstant="2014-07-17T01:01:48Z" Destination="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685">

サンプル内の上記の箇所が該当します

各要素の内容は以下になります

項目 必須 内容
ID SAML Response側が発行する一意のID
Version SAMLのバージョン。基本 2.0
IssueInstant SAML Responseが作成された時刻(UTC)
Destination SAML Responseの送信先URL
InResponseTo SAML RequestのID
Issuerタグ Issuerに関する情報
Singatureタグ Singatureに関する情報
Responseに署名を含める場合はここに記載 
Statusタグ Statusに関する情報 
Assertionタグ Statusに関する情報 

Issuerに関する情報

  <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>

サンプル内の上記の箇所が該当します

各要素の内容は以下になります

項目 必須 内容
Issuer SAML Responseの発行者をURL形式で設定する
Responseに署名が含まれている場合は必須

Singatureに関する情報

  <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
    <ds:SignedInfo>
    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
    <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
    <ds:Reference URI="#pfx31b3c63c-b358-606a-eaa1-041a98dbd6d8">
     <ds:Transforms>
      <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
      <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
     </ds:Transforms>
     <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
     <ds:DigestValue>P/Jl9zcgzgdZhTJ9F1ePzdi2CLU=</ds:DigestValue>
    </ds:Reference>
   </ds:SignedInfo>
   <ds:SignatureValue>Lz+2Icy4lgmeJqyzxhmRh6jwiBr07RXq3ePCPGOXTIzJ6h7Tl1tLDuVI9XTG5oMjKO4YcFa6e9xak7MQGT+lb3W8uuSWi8KNlz9xUal6NwIpbd2IBlSjn/rYQZh1iUDisKTFW1EXdehM0SQSr7oQ65Hu0wQGpRAk1aMJ6NZJ5T8=</ds:SignatureValue>
   <ds:KeyInfo>
    <ds:X509Data>
     <ds:X509Certificate>MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg==</ds:X509Certificate>
    </ds:X509Data>
   </ds:KeyInfo>
  </ds:Signature>

サンプル内の上記の箇所が該当します

Responseに署名を含める場合はここに記載します。ここらかAssertionのどちらかに署名が必要です

Statusに関する情報

  <samlp:Status>
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
  </samlp:Status>

サンプル内の上記の箇所が該当します

各要素の内容は以下になります

項目 必須 内容
StatusCode 結果コード
StatusMessage 任意のメッセージ
StatusDetail SAML Requestに対する追加情報を設定

StatusCodeは結果に応じて返却する値が決められています
urn:oasis:names:tc:SAML:2.0:status:{code}のフォーマットになっており、{code}の箇所には、結果に応じて以下の値を設定します。

  • Success
    • 成功
  • Requester
    • SAML Reuestを出した側の問題で失敗
  • Responder
    • SAML Responseを作成した側の問題で失敗
  • VersionMismatch
    • SAML Reuestのバージョンが正しくない場合

成功した場合は、urn:oasis:names:tc:SAML:2.0:status:Success となります。
また、上記以外にも設定できるStatusCodeが多数存在します。

Assertionに関する情報

  <saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="_d71a3a8e9fcc45c9e9d248ef7049393fc8f04e5f75" Version="2.0" IssueInstant="2014-07-17T01:01:48Z">
    <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
    <saml:Subject>
      <saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
      <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/>
      </saml:SubjectConfirmation>
    </saml:Subject>
    <saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
      <saml:AudienceRestriction>
        <saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
      </saml:AudienceRestriction>
    </saml:Conditions>
    <saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
      <saml:AuthnContext>
        <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
      </saml:AuthnContext>
    </saml:AuthnStatement>
    <saml:AttributeStatement>
      <saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
        <saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
        <saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="eduPersonAffiliation" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
        <saml:AttributeValue xsi:type="xs:string">users</saml:AttributeValue>
        <saml:AttributeValue xsi:type="xs:string">examplerole1</saml:AttributeValue>
      </saml:Attribute>
    </saml:AttributeStatement>
  </saml:Assertion>

サンプル内の上記の箇所が該当します

各要素の内容は以下になります

項目 必須 内容
ID Assertionに対する一意のID
Version SAMLのバージョン。基本 2.0
IssueInstant Assertionが作成された時刻(UTC)
Issuerタグ Assertionの発行者の情報
Singatureタグ 署名
Assertionに署名を含める場合はここに記載 
Subjectタグ 認証の対象者の情報 
Conditionsタグ Assertionの有効性を評価する際に考慮する条件 
AuthnStatementタグ 認証に関する情報 
AttributeStatementタグ 認証の対象者の属性情報 

[Assertion] Issuerタグ

Assertionの発行者をURL形式で設定します。
ResponseにもIssuerタグが存在しますが、Responseの発行者とAssertionの発行者が同一の場合、同じ値が設定されます。

[Assertion] Subjectタグ

認証の対象者や検証するための情報を設定します。

以下のように指定します。

    <saml:Subject>
      <saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
      <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/>
      </saml:SubjectConfirmation>
    </saml:Subject>
項目 必須 内容
NameIDタグ 認証の対象者の情報
SubjectConfirmationタグ 認証の対象者を検証する為の情報

NameIDタグには以下の項目を設定します

  • SPNameQualifier
    • SPの名前。SPのEntityIDが入る
  • Format
    • NameIDのフォーマット
  • NameID
    • ログインを行うユーザーを識別するデータ

SubjectConfirmationタグには以下の項目を設定します

項目 必須 内容
Method 検証を行う方法を指定。基本 bearerが使用される
NotOnOrAfter Assertionの有効期限
現在時刻がこの有効期限の時刻より後の時間の場合は無効とする
Recipient Assertionの検証を行うエンドポイント
InResponseTo SAML RequestのID

[Assertion] Conditionsタグ

Assertionに対する検証条件を設定します。
SP側ではこの条件を検証する必要があります。

以下のように条件を指定します。

    <saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
      <saml:AudienceRestriction>
        <saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
      </saml:AudienceRestriction>
    </saml:Conditions>
  • NotBefore
    • Assertionの有効期限を設定
    • 現在時刻がこの有効期限の時刻より前の時間の場合は無効とする
  • NotOnOrAfter
    • Assertionの有効期限を設定
    • 現在時刻がこの有効期限の時刻より後の時間の場合は無効とする
    • NotOnOrAfterが含まれない場合、有効期限は無期限扱いになる
  • Audience
    • 特定のSPに向けたAssertionである事を示す値を設定
    • 事前にIdPに設定したSPの一意な値と一致する必要があります

さいごに

SAMLのXMLは巨大ですが、項目を分解してそれぞれの項目を理解すると仕様を理解しやすくなると思います。
SAMLの仕様を理解する上での参考になれば幸いです。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2