SAML2.0を用いたAPI/CLIへのフェデレーテッドアクセスに関する汎用的なソリューションの実装
私の以前の記事、"How to Implement Federated API and CLI Access Using SAML 2.0 and AD FS"(「SAML2.0とAD FSを用いてフェデレーテッドAPIとCLIアクセスを実装する方法」)の中で、私はActive Directory Federation Services (AD FS)とPythonを用いて、フェデレーテッドAPIやCLIアクセスを実装する方法について解説をしました。 それ以来、私はSAML (Security Assertion Markup Language) 2.0をサポートした他のIDプロバイダーで同じ方法を用いることができるかという多くのご質問を頂いてきました。 その質問に対して今勿論と答えられることをうれしく思います。
このBlog記事では、ほぼ全てのIDプロバイダー(IdPs)でサポートされているフォームベース認証を使うために、私の以前の記事の実装をどのように拡張していくかお見せいたします。
始めてみましょう
この記事を進めるに当たり以下のことを行ってください。
- あなたのAWSアカウントにおいてシングルサインオン(SSO)でのコンソールアクセスのためにIdPとの統合をしてください。もしこの点について助けが必要な場合は、多くの一般的なIdPの導入ガイドへのリンクを含んだこちらの文書をご確認ください。Note: もしまだSAML IdPがないようでしたら、始めるに当たりSingle Sign-On:Integrating AWS, OpenLDAP, and Shibbolethを参照してください。
- IdPの設定でフォームベース認証を有効化してください。フォームベース認証では、IdPはユーザーがユーザー名とパスワードを入力するHTMLのログインフォームを提供します。
- 私の以前の記事を確認し、"Getting Started"ステップに書かれていることを全て行ってください。統合の手順の多くは同じです。この記事では、特にNTLM認証とフォームベース認証の違いについてフォーカスをします。
- 新しいフォームベース認証用のスクリプトをダウンロードしてください。
フォームベース認証用スクリプトの利用
始める前に、あなたはIdP-initiated login URLを確認する必要があります。 これはAWS管理コンソールにSSOアクセスするために使われるURLです。 例として、IdP-initiated login URLがhttps://<fqdn>:<port>/idp/profile/SAML2/Unsolicited/SSO?providerId=urn:amazon:webservicesの形になるShibboleth2.xを使用しています。 このURLをブラウザのアドレスフィールドに入力すると、以下の画像のようなIdPのログインページが表示されます。
ログインページが表示される前に、バックグラウンドでIdPがいくつものHTTPリダイレクトを行っていることに気が付くかと思います。 このプロセスの後、ツールバーのURLはこの処理を始めた時のものとは異なったものになっています。 もともとのIdP-initiated login URLを確認し、それを新しいスクリプトにidpentryurlとして入れてください。
idpentryurl = 'https://<fqdn>:<port>/idp/profile/SAML2/Unsolicited/SSO?providerId=urn:amazon:webservices'
新しいスクリプトでは、あなたはフォームベース認証の実装のための以下のようなセクションがあることに気が付くかと思います。
# Programmatically get the SAML assertion # Opens the initial IdP url and follows all of the HTTP302 redirects, and # gets the resulting login page formresponse = session.get(idpentryurl, verify=sslverification) # Capture the idpauthformsubmiturl, which is the final url after all the 302s idpauthformsubmiturl = formresponse.url # Parse the response and extract all the necessary values # in order to build a dictionary of all of the form values the IdP expects formsoup = BeautifulSoup(formresponse.text.decode('utf8')) payload = {} for inputtag in formsoup.find_all(re.compile('(INPUT|input)')): name = inputtag.get('name','') value = inputtag.get('value','') if "user" in name.lower(): #Make an educated guess that this is correct field for username payload[name] = username elif "email" in name.lower(): #Some IdPs also label the username field as 'email' payload[name] = username elif "pass" in name.lower(): #Make an educated guess that this is correct field for password payload[name] = password else: #Populate the parameter with existing value (picks up hidden fields in the login form) payload[name] = value # Debug the parameter payload if needed # Use with caution since this will print sensitive output to the screen #print payload # Some IdPs don't explicitly set a form action, but if one is set we should # build the idpauthformsubmiturl by combining the scheme and hostname # from the entry url with the form action target # If the action tag doesn't exist, we just stick with the idpauthformsubmiturl above for inputtag in formsoup.find_all(re.compile('(FORM|form)')): action = inputtag.get('action') if action: parsedurl = urlparse(idpentryurl) idpauthformsubmiturl = parsedurl.scheme + "://" + parsedurl.netloc + action # Performs the submission of the login form with the above post data response = session.post( idpauthformsubmiturl, params=payload, verify=sslverification) # Debug the response if needed #print (response.text)
このコードはPythonのrequestsモジュールを使用してIdP接続を行い、IdPログインページへのHTTPリダイレクトを全てフォローします。最終的なログインページはBeautifulSoupを用いて二つの方法でパースされます。一つ目は、全ての入力フィールドをキャプチャーしてフォームからの値が投入されます。この処理の中で、コードはユーザーが入力したusernameとpasswordがどこか判断をします。二つ目に、もし存在した場合、フォームアクションがidpauthformsubmiturlとしてキャプチャーされ、コードはフォームがどこにサブミットされるか確認します。集められたpayloadパラメータを用い、最後のステップではpayloadをIdPに戻します。認証が成功すれば、レスポンスはAWS Security Token Service (STS)の一時的な認証情報をリクエストするために必要なSAMLの認証応答を含みます。
イニシャルエラーハンドリング
あなたはこの記事や以前の記事で提供しているコードが適切なエラーハンドリングの処理を持たないことに気が付いたかもしれません。私は2つの理由から故意にそのようにしています。
- 比較的容易に理解と説明ができる、よりきれいでコンパクトな形でお見せすることができるため
- これは実際に利用する前に修正が必要となるPoCのコードであることをはっきりとさせるため
しかし、よく考えてみると、間違った認証情報という最も基本的なエラーを取り扱うためのものをいれておくべきでした。新しいスクリプトの以下のセクションを確認してください。
# Better error handling is required for production use. if (assertion == ''): #TODO: Insert valid error checking/handling print 'Response did not contain a valid SAML assertion' sys.exit(0)
このエラーハンドリングは、このアプローチの仕方の難しさを示しています。IdPは認証が成功したかに拘らず、HTTP200 successのコードを返してきます。IdPから返される記述されたエラーメッセージがHTMLの中に埋め込まれており、それが解析される必要があります。
すべてをまとめてみると
新しいスクリプトを実行すると以下のようなアウトプットとなります。
janedoe@Ubuntu64:/tmp$ ./samlapi_formauth.py Username: janedoe Password: **************** Please choose the role you would like to assume: [ 0 ]: arn:aws:iam::012345678987:role/Shib-Administrators [ 1 ]: arn:aws:iam::012345678987:role/Shib-Operators Selection: 1 --------------------------------------------------------------- Your new access key pair has been stored in the AWS configuration file /home/janedoe/.aws/credentials under the saml profile. Note that it will expire at 2015-07-16T17:16:20Z. After this time, you may safely rerun this script to refresh your access key pair. To use this credential, call the AWS CLI with the --profile option (e.g., aws --profile saml ec2 describe-instances). --------------------------------------------------------------- Simple API example listing all S3 buckets: [<Bucket: mybucket1>, <Bucket: mybucket2>, <Bucket: mybucket3>, <Bucket: mybucket4>, <Bucket: mybucket5>]
復習として、こちらがこのアウトプットの中で見られるものの説明になります。
- スクリプトがフェデレーテッドユーザーに認証情報(ユーザーネーム/パスワード)を入力するように促します。これらの認証情報は設定されたIdPに対してユーザーの認証と認可にセキュアに用いられます。
- スクリプトは返されてくるSAMLの認証応答を調べ、ユーザーが引き受けることを認可されたAWS Identity and Access Management (IAM)ロールを決定します。ユーザーが望むロールを選択した後、スクリプトは一時的な認証情報を取得するためにSTSを利用します。
- このツールはこれらの認証情報をユーザーのローカルのAWS Credentialファイルに自動的に書き込むので、ユーザーはAWS APIやCLIを呼び出すことができるようになります。
もし様々なAWS SDKにおけるSAMLプロファイルの参照の仕方についてのガイダンスが必要でしたら、"A New and Standardized Way to Manage Credentials in the AWS SDKs"をお勧めいたします。
もしトラブルになった場合
もし何かうまくいかないことがあった場合、まずフォーカスすべきエリアがいくつかあります。 最も気を付けるのは、新しいスクリプトはusernameやpasswordを保管するフィールドの名前を仮定していることです。もしブラウザでIdPのログインページのHTMLを調べてみて、これらのフィールドが異なる名前の付け方をされていたら、コードの中のuserやpassの文字をマッチするように置き換えてください。あなたは値が適切に集められているかテストするために一時的にprint payload行のコメントを外すこともできます。
あなたはIdPがまず違う認証メカニズム(Kerberosのような)を試み、それからフォームベース認証に落ちていることを見つけるかもしれません。そうしたフォールバック特有の実装によりますが、正しいIdPログインページにたどり着くために、あなたはフローに関する追加の調査や解析が必要になるかもしれません。もし、あなたがそのようなケースだと疑う場合は、formresponse.text.decode('utf8')の出力のような追加のprintステートメントが何が起きているか知るためのシンプルで優れたデバッグツールとなります。
まとめ
この記事で示されたコードは幅広い様々なお客様やIdPと働く中で開発されたものです。緊密に統合されたAPIベースのアプローチのようにエレガントではありませんが、ほとんどの場合、適切なIdP-initiated login URLをセットした後、更なる変更が必要とされることは稀であるという事実にコードの美しさを見てとれます。そうは言っても、私は全てのIdPで仕事をする機会を持ってきたわけではありません。もしあなたが実装に際し何か問題を見つけたら、どうか詳細と共に下記にコメントをしてください。私はこのソリューションが適切に拡張されていくためにできるだけの努力をしていきたいと思います。
この記事があなたを楽しませることを望みます。また継続的なフィードバックやこのソリューションを利用した成功事例を楽しみにしています。
- Quint (日本語訳は高田智己が担当しました)
コメント