シングルサインオン(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を導入することで、ユーザー体験の向上とセキュリティ強化を同時に実現できます。