セキュリティ

シングルサインオン(SSO)入門(SAML編)

投稿日:2025年2月8日 更新日:

シングルサインオン(SSO)は、ユーザーが一度の認証(ログイン)で、複数のシステムやサービスにアクセスできる仕組みを指します。ユーザを認証するログイン処理を一か所に統括し、アプリケーションやサービスと分離することでSSOを実現します。

SAML(Security Assertion Markup Language) は、SSOを実現するための標準プロトコルの一つです。

■ SAMLの仕組み

SAMLは、アイデンティティプロバイダ(IdP)と呼ばれるサーバが中心となり、システムやサービスに該当するサービスプロバイダ(SP)とユーザ(Webブラウザ)がXML形式のメッセージをHTTPプロトコルによってやり取りする仕組みです。ユーザがSPにアクセスする場合の認証フロー(SP-Initiated方式)を下図に示します。

①ユーザーは、SPにアクセスする
②SPは、ログインを受け付け、SAML認証要求(XML)を作成し、ユーザからldpへリダイレクト送信する(※)
③IdPはログイン画面(ユーザーIDとパスワード)をユーザに表示する
④ユーザはユーザIDとパスワードを入力し、ログインする
⑤IdPでユーザーが入力した情報を確認し、認証する
⑥ldPは、認証応答としてSAMLアサーション(ACS)を作成し、ユーザーからSPへリダイレクト送信する
⑦SPは、SAMLアサーション(ACS)を受信し、検証する
⑧SPは、NameID(ユーザの一意な識別子:ユーザ名)とIdpとのセッションインデックス(SAML認証における一意なセッション識別子:IdpとSP間のセッションを管理)を、ブラウザとのセッション情報として保持する。その結果、SPが提供するすべてのサービスを利用する場合、ログインせずに利用可能となる。
⑨SPは結果をユーザに返却する
 
SPのシステムやアプリケーションにおいて、必要となる処理は②と⑦と⑧の処理となります。

■ 準備

今回は、WindowsのPCにWSLをインストールし、Windows上でSPを動かし、IdpをWSLで動かします。

Windowsには、あらかじめAnaconda3をインストールし、Python環境を構築しておきます。
また、WSLには、opensslをインストールしておきます。


1)Anacondaのプロンプトを起動し、PythonのSAMLツールキットをインストール

<strong>$</strong> <strong>pip install python3-saml</strong>

2)WSLのプロンプトからテスト用のldpをインストール

saml-idpというNode.jsで記述されているテスト用のIdPをUbuntuにインストールします。
(Windows環境をお使いならば、WSLにインストールしても構いません)

$ npm install saml-idp

インストール後、IdP用にRASの鍵ペアを生成する必要があります。
(ここでは、とりあえず、saml-idpに記載されている通りに作成します)

$ openssl req -x509 -new -newkey rsa:2048 -nodes -subj '/C=US/ST=California/L=San Francisco/O=JankyCo/CN=Test Identity Provider' -keyout idp-private-key.pem -out idp-public-cert.pem -days 7300

カレントディレクトリにインストールしたsaml-ldpと上記で作成したRSAの秘密鍵と証明書が置かれます。

-rw-------   1 myname myname  1704  1月 13 12:24  idp-private-key.pem
-rw-r--r--   1 myname myname  1428  1月 13 12:24  idp-public-cert.pem
drwxr-xr-x 137 myname myname  4096  1月 13 12:22  node_modules
-rw-r--r--   1 myname myname 66922  1月 13 12:22  package-lock.json
-rw-r--r--   1 myname myname   399  1月 18 19:04  package.json

特に、他の設定などを変更する必要はありません。次のようにsaml-idpを起動します。
saml-idpは、デフォルトではローカルアドレス(127.0.0.1)のポート(7000)で起動します。

$ npm run saml-idp
Listener Port:
localhost:7000
HTTPS Enabled:
false

[Identity Provider]
Issuer URI:
<strong><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-black-color"><mark>urn:example:idp</mark></mark></strong>
Sign Response Message:
true
Encrypt Assertion:
false
Authentication Context Class Reference:
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
Authentication Context Declaration:
None
Default RelayState:
None

[Service Provider]
Issuer URI:
None
Audience URI:
<strong><mark>mock-audience</mark></strong>
ACS URL:
http://localhost:7000/auth/saml
SLO URL:
http://localhost:7000/auth/slo
Trust ACS URL in Request:
true

Starting IdP server on port localhost:7000…
IdP Metadata URL:
http://localhost:7000/metadata

SSO Bindings:
urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST
=> http://localhost:7000/saml/sso
<strong><mark>urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect</mark></strong>
=> <strong><mark>http://localhost:7000/saml/sso</mark></strong>

SLO Bindings:
urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST
=> {cyan http://localhost:7000/saml/slo}
urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect
=> {cyan http://localhost:7000/saml/slo}

IdP server ready at
http://localhost:7000

以下のSPでのPythonサンプルコードの設定において、必要になる情報は黄色の網掛けの部分となります。

■ サンプルコード

まず、SPのPythonコードで使用する必要な設定を先に説明します。

# サンプル設定:
saml_settings = {
    'strict': True,
    'debug': True,
    'sp': {
        'entityId': 'mock-audience',
        'assertionConsumerService': {
            'url': 'http://localhost:60443/acs',
            'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
        },
        'singleLogoutService': {
            'url': '',
            'binding': '',
        },
        "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
        'x509cert': '',
        'privateKey': ''
    },
    'idp': {
        'entityId': 'urn:example:idp',
        'singleSignOnService': {
            'url': 'http://localhost:7000/saml/sso',
            'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
        },
        'singleLogoutService': {
            'url': 'http://localhost:7000/saml/slo',
            'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
        },
        "x509cert": "MIID8TCCAtmgAwIBAgIUKvMQqq0+EKLGZ9CzIUfBl9E7pvEwDQYJKoZIhvcNAQELBQAwgYcxCzAJBgNVBAYTAkpQMREwDwYDVQQIDAhLYW5hZ2F3YTERMA8GA1UEBwwIS2F3YXNha2kxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDERMA8GA1UEAwwIdGVzdCBsZHAxHDAaBgkqhkiG9w0BCQEWDXRlc3RAdGVzdC5jb20wHhcNMjUwMTEzMDMyNDU5WhcNNDUwMTA4MDMyNDU5WjCBhzELMAkGA1UEBhMCSlAxETAPBgNVBAgMCEthbmFnYXdhMREwDwYDVQQHDAhLYXdhc2FraTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMREwDwYDVQQDDAh0ZXN0IGxkcDEcMBoGCSqGSIb3DQEJARYNdGVzdEB0ZXN0LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALVDWOxkxLJ8n0xwxKRPc8YwLbst1NRcJAXf5YWFB4yjqpfQMlxvVckdIVo0mOSlQpmKvLfqJj9Do0PDUXy64aPSB05uea4ArNBxnsOfCNN7AeS49e7upmbzFhsvKtR12i73l8CRsYnCvvQQ5X3blC2zH9tWPaLG72jIPzPLUaNdexVOxapmp60Ui9h/nMKOrT6CbvV/wqFMX3SwstILhsuQhSWRc9oxeMFNbp+/x5bJvYt8G9F+89zE+ZzSrXpLmbu1jrhSaM2Z32JkOtQQJpqgTgLmjmXmAEyGd7w1xlUMe9Lz5NiadZRcB5fYr6i0exymd19VQs8C2NKo+wJUaicCAwEAAaNTMFEwHQYDVR0OBBYEFI3hEu1v8YlUQy2Q6JaXeQ9Ia7ZrMB8GA1UdIwQYMBaAFI3hEu1v8YlUQy2Q6JaXeQ9Ia7ZrMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAHn6tVlRW1+cWP72uA+xNHDBosh0dYT4IvmCRakyePI6hl5yTq/wL9Bp0xytSnWA7ii4azaV2Snx1565wckxE25pZ8tW5hyTPvxI/v3vtR5g6M8wWlo6q/qj65lWYvRHUHek95aJ+dTUwvkcqm6Js6eVJGjAj/R20yRYkjdf/d4sBSFchWP86/DXoDo/7koegr2iGdeHPbbpP41YM/5dpy+Q0Z37mCi4tvq5tHz7NDtbklmEWe8eKG4e9j5vlNbgvuYIyvzf4odiBpknDotSvD21bWUyS43ue3foDQXYNVTccjIJU8CMPMjr/uvOR02REFABtXeqBihNTYUBJpNB15M="
    }
}

①デバッグモードや厳密モード(debug, strict)
②サービスプロバイダー(SP)の情報 

・entityId:

 IDPの一意な識別子。通常、IDPのメタデータURL 

 ※今回は、saml-Idpを起動した際に表示される「Audience URI:mock-audience」を設定します

・singleSignOnService:

 url: IDPから送信されたSAMLレスポンスを受け取るエンドポイントのURL

 ※SPのコードとして実装するエンドポイントとして、”http://localhost:50443/acs”を指定します

 binding: 使用するSAMLバインディング方式(例: HTTP-POST)

 ※今回は、”urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST”を指定します

・singleLogoutService:

 url: ログアウトリクエストやレスポンスを処理するエンドポイントのURL

 ※今回は、未使用です

 binding: 使用するSAMLバインディング方式(例: HTTP-Redirect)

 ※今回は、未使用です

・x509cert:

 SPの公開鍵証明書(TLS通信で使用される場合がある) ※今回は、未使用

・privateKey:

 SPの秘密鍵  ※今回は、未使用
 
③アイデンティティプロバイダー(IDP)の情報
・entityId:

 IDPの一意な識別子。通常、IDPのメタデータURL
 
 ※今回は、saml-idpの起動時に表示された”urn:example:idp“を指定します。

・singleSignOnService:

 url: SPがリダイレクトするためのIDPのログインエンドポイントのURL

 ※今回は、saml-idpの起動時に表示された”http://localhost:7000/saml/sso”を指定します。

 binding: 使用するSAMLバインディング方式(例: HTTP-Redirect)

※今回は、saml-idpの起動時に表示された”urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect“を指定します。

・singleLogoutService:

 url: SPがリダイレクトするためのIDPのログアウトエンドポイントのURL

 ※今回は、未使用ですが、”http://localhost:7000/saml/slo”を指定します 

 binding: 使用するSAMLバインディング方式(例: HTTP-Redirect)

 ※今回は、未使用ですが、”urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect”を指定します 

・x509cert: IDPの公開鍵証明書

 SAMLレスポンスの署名を検証するために使用します。

 ※idp-public-cert.pemの内容をそのまま貼り付けます

次に、ログイン(サインイン)処理のPython+FlaskのSPサンプルコード全文を以下に示します。

# -*- coding: utf-8 -*-
from flask import Flask, request, redirect, url_for, make_response, session
from onelogin.saml2.auth import OneLogin_Saml2_Auth             # OneLoginのSAML認証ライブラリ
from onelogin.saml2.settings import OneLogin_Saml2_Settings     # OneLoginのSAML設定ライブラリ
import os

app = Flask(__name__)
app.secret_key = "your_secret_key"  # セッション用の秘密鍵を設定

# FlaskリクエストオブジェクトをOneLogin用に変換する関数
def prepare_flask_request(request):
    """
    FlaskリクエストオブジェクトをOneLogin用に変換する関数。
    """
    return {
        'https': 'on' if request.scheme == 'https' else 'off',
        'http_host': request.host,
        'script_name': request.path,
        'get_data': request.args.copy(),
        'post_data': request.form.copy()
    }

# SAML認証オブジェクトを初期化する関数  
def init_saml_auth(request):
    """
    SAML認証オブジェクトを初期化する。
    """
    auth = OneLogin_Saml2_Auth(prepare_flask_request(request), old_settings=saml_settings)
    return auth

# Loginエンドポイント
@app.route("/login")
def login():
    """
    SAML認証を開始するエンドポイント。
    """
    auth = init_saml_auth(request)
    return redirect(auth.login())

# Idpからのリダイレクトを処理するエンドポイント
@app.route("/acs", methods=['POST'])
def acs():
    """
    Assertion Consumer Service (ACS)エンドポイント。
    SAML応答を処理する。
    """
    
    # SAML認証オブジェクトを初期化
    auth = init_saml_auth(request)
    # SAML応答を処理
    auth.process_response()
    # エラーを取得
    errors = auth.get_errors()

    # エラーがない場合
    if len(errors) == 0:
        
        # ユーザーが認証されている場合
        if auth.is_authenticated():
            session["name_id"] = auth.get_nameid()               # 認証成功時に name_id をセッションに保存
            session["session_index"] = auth.get_session_index()  # 認証成功時に session_index をセッションに保存
            
            # ユーザー情報を取得
            user_data = {
                "attributes": auth.get_attributes(),
                "name_id": auth.get_nameid(),
                "assertion_id": auth.get_last_assertion_id(),                       # SAMLアサーションID
                "assertion_issue_time": auth.get_last_assertion_issue_instant(),    # SAMLアサーション発行時間
                "assertion_expiry": auth.get_last_assertion_not_on_or_after()       # SAMLアサーションの有効期限
                
            }
            # ユーザー情報を表示
            return f"ユーザー認証成功: {user_data}"
        else:
            # ユーザーが認証されていない場合,401(Unauthorized)を返す
            return "ユーザー認証失敗", 401
    else:
        # エラーがある場合,500(Internal Server Error)を表示
        return f"エラー: {errors}", 500


# metadataエンドポイント
@app.route("/metadata")
def metadata():
    """
    サービスプロバイダーのメタデータを提供するエンドポイント。
    """
    # SAML設定を取得
    settings = OneLogin_Saml2_Settings(settings=saml_settings, custom_base_path=os.getcwd())
    # メタデータを取得
    metadata = settings.get_sp_metadata()
    # メタデータを検証
    errors = settings.validate_metadata(metadata)

    # エラーがない場合
    if len(errors) == 0:
        # メタデータを返す
        response = make_response(metadata, 200)
        response.headers['Content-Type'] = 'text/xml'
        return response
    else:
        return f"Metadataエラー: {errors}", 500

# サンプル設定
saml_settings = {
    'strict': True,
    'debug': True,
    'sp': {
        'entityId': 'mock-audience',
        'assertionConsumerService': {
            'url': 'http://localhost:60443/acs',
            'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
        },
        'singleLogoutService': {
            'url': '',
            'binding': '',
        },
        "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
        'x509cert': '',
        'privateKey': ''
    },
    'idp': {
        'entityId': 'urn:example:idp',
        'singleSignOnService': {
            'url': 'http://localhost:7000/saml/sso',
            'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
        },
        'singleLogoutService': {
            'url': 'http://localhost:7000/saml/slo',
            'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
        },
        "x509cert": "MIID8TCCAtmgAwIBAgIUKvMQqq0+EKLGZ9CzIUfBl9E7pvEwDQYJKoZIhvcNAQELBQAwgYcxCzAJBgNVBAYTAkpQMREwDwYDVQQIDAhLYW5hZ2F3YTERMA8GA1UEBwwIS2F3YXNha2kxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDERMA8GA1UEAwwIdGVzdCBsZHAxHDAaBgkqhkiG9w0BCQEWDXRlc3RAdGVzdC5jb20wHhcNMjUwMTEzMDMyNDU5WhcNNDUwMTA4MDMyNDU5WjCBhzELMAkGA1UEBhMCSlAxETAPBgNVBAgMCEthbmFnYXdhMREwDwYDVQQHDAhLYXdhc2FraTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMREwDwYDVQQDDAh0ZXN0IGxkcDEcMBoGCSqGSIb3DQEJARYNdGVzdEB0ZXN0LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALVDWOxkxLJ8n0xwxKRPc8YwLbst1NRcJAXf5YWFB4yjqpfQMlxvVckdIVo0mOSlQpmKvLfqJj9Do0PDUXy64aPSB05uea4ArNBxnsOfCNN7AeS49e7upmbzFhsvKtR12i73l8CRsYnCvvQQ5X3blC2zH9tWPaLG72jIPzPLUaNdexVOxapmp60Ui9h/nMKOrT6CbvV/wqFMX3SwstILhsuQhSWRc9oxeMFNbp+/x5bJvYt8G9F+89zE+ZzSrXpLmbu1jrhSaM2Z32JkOtQQJpqgTgLmjmXmAEyGd7w1xlUMe9Lz5NiadZRcB5fYr6i0exymd19VQs8C2NKo+wJUaicCAwEAAaNTMFEwHQYDVR0OBBYEFI3hEu1v8YlUQy2Q6JaXeQ9Ia7ZrMB8GA1UdIwQYMBaAFI3hEu1v8YlUQy2Q6JaXeQ9Ia7ZrMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAHn6tVlRW1+cWP72uA+xNHDBosh0dYT4IvmCRakyePI6hl5yTq/wL9Bp0xytSnWA7ii4azaV2Snx1565wckxE25pZ8tW5hyTPvxI/v3vtR5g6M8wWlo6q/qj65lWYvRHUHek95aJ+dTUwvkcqm6Js6eVJGjAj/R20yRYkjdf/d4sBSFchWP86/DXoDo/7koegr2iGdeHPbbpP41YM/5dpy+Q0Z37mCi4tvq5tHz7NDtbklmEWe8eKG4e9j5vlNbgvuYIyvzf4odiBpknDotSvD21bWUyS43ue3foDQXYNVTccjIJU8CMPMjr/uvOR02REFABtXeqBihNTYUBJpNB15M="
    }
}

if __name__ == "__main__":
    app.run("127.0.0.1", 60443, debug=True)

■ 動作確認

saml-idpを起動します。

VSCodeからPyhton+FlaskのSPコードを起動します。

ブラウザから”http://localhost:60443/login”に接続します。
そうすると、SPからリダイレクトされ、Idpのログイン画面が表示されます。

”Sign in”ボタンをクリックし、ログインします。(画面にはデフォルトのユーザ情報が設定されています)

認証が成功するとIdpからブラウザ介してSAMLアサーション(XML)がSPへリダイレクトされ、ユーザ認証が成功します。(※SPには、認証されたユーザ情報が返却されます)

■ まとめ

SPには、認証されたユーザ情報が返却されることで検証ができる点が、SAMLが認証機能を提供するものであることを示す理由です。(一方、OAuth2では、ユーザ情報ではなくトークンが返却されるため、認可機能となります)

IdPの機能を提供するサービスは、Okta、Azure、AWSなどをはじめとして、幾つかありますが、セキュリティの観点でパスワード強度や更新処理、多要素認証サポートなどを踏まえて設定する必要があります。

シングルサインオン(SSO)は、一度のログインで複数のサービスやアプリケーションをシームレスに利用できる便利な仕組みです。SSOを導入することで、ユーザー体験の向上とセキュリティ強化を同時に実現できます。

-セキュリティ

執筆者:


comment

メールアドレスが公開されることはありません。

関連記事

はじめての「Webセキュリティ」入門

情報セキュリティの中で、最も注視される領域がインターネットを使うWebシステムのセキュリティです。拡大しているECサイトやWebサービスでは、誰でも使える「インターネット」を介してアクセスされます。顧 …

はじめての「情報セキュリティ」入門

情報セキュリティとは、見えない敵のために防御を張り巡らせることです。情報の価値は、外へと公開されている情報より、会社などの組織で流れている情報の方が価値がずっと高くなります。それらの情報は、組織の内部 …

防御のための「セキュリティ攻撃」入門

ソフトウェアに脆弱性があっても、通常の機能を使う上では全く問題はありません。しかし、ソフトウェア設計者、プログラマーの関心や意識のない機能外の箇所で、悪意のある処理を実行できるアクセスが可能となってし …

Chinese (Simplified)Chinese (Traditional)EnglishFilipinoFrenchGermanHindiJapaneseKoreanMalayThaiVietnamese