BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

IdPとしてSAML認証機能を自前実装した

はじめに

みなさんはじめまして。BASEでエンジニアをしております田村 ( taiyou )です。

先日、BASEではショップオーナー向けのコミュニティサイト「BASE Street」にログインするための機能としてSSOログイン機能をリリースしました。 SSOログインを実現するための認証方式はいくつかあるのですが、弊社ではSAML認証方式を用いて実現しました。 そのため、この記事ではSAML認証機構のIdPとしてOSSを使わずにSAML認証機能を実装する方法を紹介します。

前回のテックブログで、このSSOログイン機能のフロント側を開発したPJメンバーの若菜が「サーバーサイドエンジニアがフロントエンドに挑戦して最高の経験になった話」を執筆したのでこちらも見てみてください!

SAML認証機能を提供しているOSSには、Keycloakなどがありますが、BASEでは以下の理由により自前実装することにしました。

  • 既に大量のユーザー情報を管理しており、Keycloakなどにユーザー情報の連携を行う必要がある
  • 弊社で採用しているPHP, Goで実装された有名なOSSがないため、弊社エンジニアで管理・運用するハードルが高い
  • SAML認証機能を有するライブラリ(lightSAML)があり、自前実装のコストが高くなかった

以上の理由により、OSSを使わずにIdPとしてSAML認証機能を開発しました。

対象読者

  • SAML認証についてこれから調べようと思っている方
  • IdPとしてkeycloakなどのOSSを使わずにSAML認証機能を開発するエンジニア

SAML認証とは?

SAML認証とは、シングルサインオン(SSO)を実現する一つの認証方式です。

シングルサインオン(SSO)とは?

Single sign-on (SSO) is an authentication scheme that allows a user to log in with a single ID to any of several related, yet independent, software systems.

True single sign-on allows the user to log in once and access services without re-entering authentication factors.

Single sign-on - Wikipediaより引用

シングルサインオン(SSO)とは、ユーザーが1つのIDで複数のサービスにログインできるする認証方法です。 このシングルサインオンによりユーザーは一度ログインすれば、認証要素を再入力することなくサービスにアクセスすることができます。

SAML認証はシングルサインオン(SSO)を実現するための認証方式

上記のシングルサインオン(以下、SSOと呼称)を実現する認証方式は、Single sign-on - Wikipediaで記載されている通りいくつかあります。SAML認証は、そのSSOを実現するための一つの認証方式です。

SAML認証はシングルサインオン(SSO)を実現するための認証方式

SAML認証方式以外の方法については、以下の文献を参照してください。

SAML認証のフロー

それでは、SAML認証方式でSSOを行うためのフローを説明します。以降でSAML認証のフローについて説明する前に、サービスプロパイダー(SP)とアイデンティティプロパイダー(IdP)について説明します。

サービスプロバイダー(SP)

文字通り、ユーザーに対してアプリケーションサービスを提供するものです。ユーザーがサービスプロパイダー(以降、SPと呼称)にログインする際、後述するアイデンティティプロパイダーにユーザー認証を行ってもらいSPにログインします。

サービスプロバイダー(SP)

アイデンティティプロパイダー(IdP)

ユーザーの認証に必要な情報を管理しているのがアイデンティティプロバイダーです。アイデンティティプロバイダーでは、SPから送信された認証リクエストを処理し、ユーザー情報をSPに返却します。

アイデンティティプロバイダー(IdP)

SAML認証フロー

詳しい内容は、Security Assertion Markup Language - Wikipediaを参照してください。

Security Assertion Markup Language - Wikipediaより引用

1. SPのページへアクセスする

まずユーザーはSPのページにアクセスするとします。このとき、ユーザーが既にSP側で認証済みの場合はSAML認証を行う必要がないため、ページが表示されます。認証されていない場合は、IdPへリダイレクトされます。

2. IdPへリダイレクトする

ユーザーがまだ認証されていない場合は、IdPへリダイレクトされます。IdPへリダイレクトされる際に、クエリパラメーターにSAMLRequestパラメーターが付与されます。このSAMLRequestはIdPに認証の要求をする際に必要となる以下のようなxml形式の情報を圧縮した文字列になります。

<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{認証要求ID}" Version="2.0" ProviderName="{サービスプロバイダー名}" IssueInstant="{SAMLRequestの生成日時}" Destination="{SAMLRequestの送信先IdPのURL}" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="{認証結果のPOST先SPのURL}">
  <saml:Issuer>http://sp.example.com/hoge/metadata.php</saml:Issuer>
  <samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" AllowCreate="true"/>
  <samlp:RequestedAuthnContext Comparison="exact"> <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
  </samlp:RequestedAuthnContext>
</samlp:AuthnRequest>

3. SAMLRequestを検証/ユーザー認証を行います

IdPでは、まずユーザーのブラウザ(ユーザーエージェント)を経由してSPから送信されたSAMLRequestの検証を行います。この検証では、電子署名付きのSAMLRequestが送信される場合では、電子署名を行います。

SAMLRequestの検証が終了したら、ユーザー認証のためにログインページを表示します。ユーザーはIdPに登録したメールアドレスやパスワードを入力します。

4. SAMLResponseを生成します

ユーザー認証のための情報をフォームに入力して送信したら、IdP側でログイン処理を行います。そして、ログインに成功したら、SPに送信するSAMLResponseを生成します。このSAMLResponseはSP側でユーザー認証を行う際に利用する情報をxmlに格納します。

SAMLResponseの例

<?xml version="1.0" encoding="UTF-8"?>
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Destination="{SAMLResponseの送信先SPのURL}" ID="{IdP側で発行するID}" InResponseTo="{SPから送信されたSAMLRequestに含まれる認証要求ID}" IssueInstant="{SAMLResponseを発行した日時}" Version="2.0">
    <saml:Issuer>{IdPのURL}</saml:Issuer>
    <samlp:Status>
        <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
    </samlp:Status>
    <saml:Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="{IdP側で発行するID}" IssueInstant="{SAMLResponseを発行した日時}" Version="2.0">
        <saml:Issuer>{IdPのURL}</saml:Issuer>
        <dsig:Signature xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
            <dsig:SignedInfo>
                <dsig:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
                <dsig:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
                <dsig:Reference URI="#ID_b93d4d7d-1937-474f-84df-2f3440025a3c">
                    <dsig:Transforms>
                        <dsig:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
                        <dsig:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
                    </dsig:Transforms>
                    <dsig:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
                    <dsig:DigestValue>{ハッシュ値}</dsig:DigestValue>
                </dsig:Reference>
            </dsig:SignedInfo>
            <dsig:SignatureValue>{電子署名の値}</dsig:SignatureValue>
            <dsig:KeyInfo>
                <dsig:X509Data>
                    <dsig:X509Certificate>{IdP側で発行した証明書}</dsig:X509Certificate>
                </dsig:X509Data>
            </dsig:KeyInfo>
        </dsig:Signature>
        <saml:Subject>
            <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">{ログイン対象となるユーザのメールアドレス}</saml:NameID>
            <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
                <saml:SubjectConfirmationData InResponseTo="{SPから送信されたSAMLRequestに含まれる認証要求ID}" NotOnOrAfter="{SAMLResponseの有効期限}" Recipient="{SAMLResponseの送信先SPのURL}" />
            </saml:SubjectConfirmation>
        </saml:Subject>
        <saml:Conditions NotBefore="{SAMLResponseの有効期限開始日時}" NotOnOrAfter="{SAMLResponseの有効期限終了日時}">
            <saml:AudienceRestriction>
                <saml:Audience>{SPのドメイン}</saml:Audience>
            </saml:AudienceRestriction>
        </saml:Conditions>
        <saml:AuthnStatement AuthnInstant="{SAMLResponseを発行した日時}" SessionIndex="{IdP側で発行するID}" SessionNotOnOrAfter="{IdP側のセッション有効期限}">
            <saml:AuthnContext> <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef>
            </saml:AuthnContext>
        </saml:AuthnStatement>
        <saml:AttributeStatement>
            ...
        </saml:AttributeStatement>
    </saml:Assertion>
</samlp:Response>

5. SPへSAML認証情報をPOSTします

IdP側でSAMLResponseの生成が完了したら、次のようなHTMLをレンダリングし、POSTします。

  <form method="post" action="https://sp.example.com/saml2/sso/post" ...>
    <input type="hidden" name="SAMLResponse" value="{XML形式のSAMLResponseをbase64エンコーディングした値}" />
    ...
    <input type="submit" value="Submit" />
  </form>

レンダリングした際、JavaScript側で画面が表示されたらsubmitボタンを自動で押下する処理を行うことでユーザーはSP画面へ自動遷移するようになります。

6. SPのページが表示される

SP側でIdPから送られたSAMLResponseの検証が終了し、ログイン処理が終了したら、SAML認証は成功です。これで、ユーザー認証が必要なSPのページが表示されます。

SAML認証を実現する方法

上記で説明したSAML認証を実現するための方法として、次の2つが考えられます。

方法1. OSSを利用する

SAML認証を実現するための代表的な方法として、OSSライブラリを利用する方法があります。現在、SAML認証機能を提供できるOSSとして以下のものがあります。

これらのOSSを利用することで、ユーザーに対してSAML認証機能を提供することができます。

メリットとデメリットはそれぞれ以下の通りです。

  • メリット
    • OSSをcloneし、SAML認証用サーバーを用意し、起動すればSAML認証機能を提供できること
  • デメリット
    • SAML認証用サーバーの保守が新たに必要になる
    • BASEのように既にユーザーデータが管理されている場合は、BatchやAPI, MQなどを利用してSAML認証用サーバーにデータを連携する必要がある

方法2. 自前でSAML認証機能を開発する

別の方法として、既に提供しているサービスの1機能としてSAML認証機能を開発し、提供する方法があります。つまり、SAML認証用のエンドポイントとSAML認証用のログインフォームなどを開発することで提供する方法です。

  • メリット
    • 稼働中のサービスの1機能として提供するので、新たにSAML認証用サーバーを用意する必要がなく、運用コストが抑えられる
    • ユーザー認証に必要な情報をOSSなどに連携する必要がない
  • デメリット
    • SAML認証機能を開発する実装難易度が比較的高い

BASEでは、次の2つの理由から方法2を採用しました。

  • SAML認証機能を利用するユーザーが限られているため、keycloakのメンテナンス・ランニングコストとユーザーへの費用対効果が釣り合わない
  • BASEでは既に大量のユーザー情報を管理しているため、これをkeycloakに連携する必要がある
  • 弊社で採用しているPHP, Goで実装された利用実績のある有名なOSSがないため、弊社エンジニアで管理・運用するハードルが高い

IdPとしてBASEではどのような設計になったか?

BASEのように既にサービスに登録しているユーザーのSAML認証を行う際に、BASEではどのような設計になったのか紹介していきます。

SAML認証のための必要な機能とページ

具体的な設計の紹介に入る前に、IdPとしてBASEでは、そもそもどのような機能が必要なのかを明らかにしてきます。SAML認証のための必要な機能は以下の通りです。

機能 説明
SAMLRequestの検証機能 SPからクエリパラメーターで送信されたSAMLRequestを検証する機能です。SP側で電子署名を行っている場合など検証が必要な場合に呼び出される機能です。
ログイン判定機能 SPからリダイレクトされたユーザーがBASEに既にログインしているか判定する機能です。もしログイン済みの場合は、ログイン処理をスキップするようにします。
ログイン機能 メールアドレス、パスワードなどユーザー情報を指定することでログイン処理を行う機能です。機能自体は通常のログインと差異はありません。
SAMLResponseの生成機能 メールアドレスやユーザーネームなどのユーザー情報とSPから送信されたSAMLRequestからSAMLResponseを生成する機能です。

また、必要なページは以下の通りです。

ページ 説明
SSOログインページ SSOログインのために必要なフォームを有するページです。
SPリダイレクトページ SAMLResponseをSPに送信するためのフォームページです。通常、このページではユーザー自身がフォームの送信ボタンを押すのではなく、ページがロードされたらjsで送信ボタンを押すように実装されます。

SPからBASEにリダイレクトされた際の画面遷移としては、SSOログインページでログインを行い、SAMLResponseをSPにフォームPOSTするSPリダイレクトページを表示するような画面遷移になります。

これらの機能とページがそれぞれどのようにやりとりするのか詳細の設計を紹介します。

処理の流れ

1. SPからBASEにリダイレクトされる

SPからBASEにリダイレクトされたら、次の処理を行います。

  1. SAMLRequestの検証処理
  2. ログインの判定処理

既にBASEにログインしている場合は、「3. SAMLResponseを生成し、SPにリダイレクトする」へ進みます。 BASEにログインしていない場合は、ログインページを表示します。

2. BASEにログインする

ログインページが表示されたら、ログイン処理に必要なメールアドレスとパスワードを入力し、submitします。今回BASEで開発したSAML認証機能ではすでにBASEサービスに登録しているユーザーにのみ提供する機能なので新規登録フォームは除外しています。

3. SAMLResponseを生成し、SPにリダイレクトする

ログイン処理が正常に完了したら、SPへ返す認証情報であるSAMLResponseを生成します。SAMLResponseを生成したら、SPリダイレクトページを表示します。このSPリダイレクトページは、次のようなSPへPOSTするフォームページです。

  <form method="post" action="https://sp.example.com/saml2/sso/post" ...>
    <input type="hidden" name="SAMLResponse" value="{SAMLResponseをbase64エンコーディングした値}" />
    ...
    <input type="submit" value="Submit" />
  </form>

この場合、ユーザーが手動でsubmitボタンを押すことでSPへ遷移することも可能ですが、SAML認証機能を提供する多くのサービスはページがロードされたら自動でsubmitボタンを押下するjavascriptコードを実装することで自動遷移するようにしています。

おわりに

この記事では、OSSを使わずにIdPとしてSAML認証機能を開発する方法について紹介しました。 IdPとしてSAML認証機能を提供する方法として、それぞれ ①OSSを利用する ②自前実装する 方法があります。今回、BASEでは②自前実装する方法を採用し、開発を行いました。その開発中に私が苦労した点として「SAML認証についての資料が少なかった」があげられます。そのため、この記事がみなさんの参考に少しでもなれれば幸いです。

参考文献