Amazon Web Services ブログ

サーバレス JavaScript アプリケーションで SAML: Part I

by AWS Japan Staff | on | in Amazon Cognito, AWS IAM, General, Serverless, セキュリティ |

このブログ記事は AWS の Richard Threlkeld, Gene Ting, Stefano Buliani によって AWS Compute Blog に投稿された「SAML for Your Serverless JavaScript Application: Part I」の翻訳記事です。

このブログ記事に掲載したコードや SAM テンプレートの全体は samljs-serverless-sample GitHub レポジトリにあります。手動でリソースを作成する事もできますが、GitHub レポジトリにある SAM テンプレートを使ってリソースを作成することを強くお勧めします。


SAML 認証連携を実現したくありませんか? AWS プラットフォームで使うことができる一時的なセキュリティ認証情報の発行を、短期間の SAML アサーションを交換で実現できます。

エンタープライズ Web アプリケーションを構築する時は、認証や認可が一貫して行われ業界のベストプラクティスに沿っている事が必須事項です。AWS では、ユーザに一意のIDを作成し、AWS のサービスにアクセスできる短期間の認証情報を使えるようにできる Amazon Cognito と呼ぶサービスを構築しました。これらの認証情報は、IAM ポリシーに基づくロールと関連付けて、異なるリソースへのアクセスを許可や拒否する事ができます。

この記事では、Amazon Cognito で SAML プロバイダと認証連携を行う異なる方式を紹介していきます。応用すると、異なるタイプの認証プロバイダ (IdP) と連携させることができます。Facebook、Twitterやその他のサードパーティのソーシャルメディアを IdP にする事もできます。Amazon Cognito のユーザプールと認証連携させ、独自のマネージドユーザディレクトリを作成することもできます。

例えば、ユーザが Active Directory Federation Services (ADFS) で認証する事ができる JavaScript アプリケーションを構築したいかもしれません。ユーザは AWS 認証情報で許可された範囲で API を呼び出して得た情報をアプリケーションに表示させたり、DynamoDB テーブルに書き込むことができます。AWS Mobileブログの記事「Announcing SAML Support for Amazon Cognito」では、新しい SAML 機能について Java、Android、iOS のサンプルコード付きで紹介しました。この記事では、ADFS フローのカスタマイズについて JavaScript のサンプルを交えて詳細に紹介します。

シナリオ

この記事では、クライアント側のフローを紹介します。SAML アサーションは Amazon API Gateway 経由で渡され、ブラウザ上のコードは Amazon Cognito Identity から直接認証情報を受け取ります。次回のブログ記事では、バックエンド側の処理を紹介します。SAML アサーションと認証情報は、AWS Lambda ファンクション内で処理でき、ビジネスロジックを実現するようカスタマイズしたり監査履歴を取ったりする事ができます。

このシナリオの全コードは SAM テンプレート含め、GitHub の samljs-serverless-sample レポジトリにあります。手動でリソースを作成する事もできますが、GitHub レポジトリにある SAM テンプレートを使ってリソースを作成することを強くお勧めします。

サーバレスのアーキテクチャとなっていますが、ADFS との連携は従来のコンピュート サービスである EC2 インスタンスのようなコンポーネントで置き換えることもできます。ADFS の用語や定義については「Claims-based identity term definitions」を参考にして下さい。

前提条件

このブログ記事では、ADFS が動作している環境が必要です。以下の事を実施して下さい。:

  1. AWS コンソールで ADFS との認証連携を設定。「Enabling Federation to AWS Using Windows Active Directory, ADFS, and SAML 2.0」を AWS CloudFormation テンプレートと共に参考にしてください。
  2. サインインページ(https://localhost/adfs/IdpInitiatedSignOn.aspx)からユーザ example\bob が ADFS-Dev と ADFS-Production の両方のグループとして認証出来ることを確認します。
  3. Amazon Cognito Identity のプールを作成します。

ADFS 設定の概要

チュートリアルを始める前に、幾つか AWS と SAML の設定を確認しましょう。まず、前提条件にある通り IAM ロールが作成されているかから確認して下さい。アプリケーション用に新しく IAM ロールと AD グループを作成してもかまいません。この記事では、「Enabling Federation to AWS Using Windows Active Directory, ADFS, and SAML 2.0」の記事の通り ADFS-Dev と ADFS-Production を利用します。

  1. IAM コンソールで、ロールを選択し ADFS-Ddev を選択し、信頼関係 (Trust Relationships) のタブを開き、以下のコードのようになっている事を確認します。:
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Principal": {
            "Federated": "arn:aws:iam::YOURACCOUNTNUMBER:saml-provider/ADFS"
          },
          "Action": "sts:AssumeRoleWithSAML",
          "Condition": {
            "StringEquals": {
              "SAML:aud": "https://signin.aws.amazon.com/saml"
            }
          }
        }
      ]
    }

このポリシーは “ADFS” という SAML IdP で認証されたユーザがロールを引き受ける事を許可しています。また、ポリシーには条件が含まれており、SAML アサーション内の AudienceRestriction が https://signin.aws.amazon.com/ である事となっています。SAML アサーションは ADFS から AWS に HTTP POST で SAMLResponse というヘッダとして送られて来ます。前提条件に記載されている設定が実施済みであれば、ADFS コンソールでは以下のように設定されており、利用者(Ralying Party)の URL として AWS メタデータ URL が指定されています。詳細については、「認証レスポンスの SAML アサーションを設定する」を確認して下さい。



認証後、ADFS は利用者のアプリケーションに自動的にリダイレクトします。

上記のスクリーンショットでは、認証後の SAMLResponse の値を見るために Chrome を利用しました。「トラブルシューティングのためにブラウザで SAML レスポンスを表示する方法」に記載されているとおり、他のブラウザでも同様のことを行えます。SAMLResponse を貼り付ければ、Audience、Roles や Destination などの値を見ることができる “SAML Decoder” がインターネット上にあります。パート 2 のブロク記事では、これをプログラムで行う方法を紹介します。

ADFS からサーバレス Web サイトにリダイレクトし、Amazon Cognito に繋げる

この1つ目のブログのシナリオの方が難しくありません。多くの組織での要件にベストな流れとなっています。ユーザは ADFS で認証して Amazon Cognito から AWS 認証情報を受け取り、アプリの中でアクションを行えます。このサンプルアプリケーションは:

  1. ADFS で認証するログイン メカニズムを変更して取り出すことで、SAMLResponse ヘッダを取り出します。ユーザが S3 上に配置されたウェブサイトを訪問した際に、自動的にこの仕組みが行われます。
  2. ADFS-Dev ロールの信頼ポリシーを変更し、Active Directory の AWS-gDev グループのメンバであれば Amazon Cognio から一時的な認証情報を受け取れるようにします。
  3. ユーザのロールを選択するコードをアプリケーションの中に組み込みます。このブログ記事では、ADFS-Dev ロールを選択します。

参考までに、AWS Console への認証連携の場合 #3 と同様の内容は、ADFS のウェブページ IdpInitiatedSignOn.aspx からリダイレクトされた後にラジオボタンでロールをクリックして選択する事で実現されます。より詳しい内容については、セキュリティブログの「Enabling Federation to AWS Using Windows Active Directory, ADFS, and SAML 2.0」をご覧下さい。もしユーザが 1 つの Active Directory グループのメンバだけであれば、SAMLResponse には複数のロールが含まれておらず、その場合は #3 は省略されます。構成は以下の図のとおりとなります。

チュートリアル: ADDFS からサーバーレス Web サイトにリダイレクトし、Amazon Cognito に繋げる

まず、サーバレス Web サイトをセットアップし、認証情報を取得するログインフローを開始させます。シングル Web アプリを配置するのには S3 を使用します。

S3 バケットの作成

  1. S3 のコンソールで、「バケットを作成」を選択して一意なバケット名を入力します。以下の例では “serverlessweb” としますが、皆様は何か他の名前として下さい。
  2. バケットを作成後、詳細設定のページで「プロパティ」を選び、「バケットポリシーの編集」をクリックします。
  3. 以下のポリシーを設定して下さい。YOURBUCKETNAMEGOESHERE は置き換えて下さい。このポリシーは、バケットに入っている HTML や JavaScript などのファイルを誰でも GET リクエストを行えるようにします。Web サイトにアクセスすると Web ブラウザは GET リクエストを発行し、このポリシーがリソースの読み込みをユーザに許可します。
    {
      "Version": "2012-10-17",
      "Statement": [
          {
              "Sid": "PublicReadForGetBucketObjects",
              "Effect": "Allow",
              "Principal": "*",
              "Action": "s3:GetObject",
              "Resource": "arn:aws:s3:::YOURBUCKETNAMEGOESHERE/*"
          }
      ]
    }
  4. 「静的ウェブサイトホスティング」を選択し、「ウェブサイトのホスティングを有効にする」とします。フォームに「index.html」と「error.html」を入力します。

  5. 次に、HTML ファイルをバケットに追加して、https://YOURBUCKETNAME.s3-website-REGION.amazonaws.com をブラウザで開きページが見れるか確認します(YOURBUCKETNAME と REGION は読み替えて下さい。)。まずは以下の HTML をテンプレートとして使って下さい。
    <!DOCTYPE html>
    <html>
     <head>
      <title>
       Amazon Cognito SAML Example
      </title>
      <script src="aws-sdk.min.js">
      </script>
     </head>
     <body>
      <h1>
       Testing SAMLResponse
      </h1>
     </body>
    </html>
      
  6. JavaScript SDK から圧縮(Minify)されたバージョン aws-sdk.min.js をダウンロードして、HTML ファイルと同様にバケットに保存し、エラー無くロードできることを確認します。

(オプション) CloudFront ディストリビューションのセットアップ

次に進める前に、さらにもう1つセットアップしたいと思います。CloudFront ディストリビューションです。S3 静的 Web ホスティングに CloudFront やその他の CDN を使用することで、独自ドメイン名で SSL を使用することができるようになります。

API Gateway から HTTP サイトにリダイレクトする事もできるため強制ではありませんが、Web サイトや認証や認可のシステムはエンドツーエンドで HTTPS を使うべきです。以下がセットアップで行う内容です。

  1. CloudFront コンソールで、Web タイプのディストリビューションを作成します。
  2. 「Viewer Protocol Policy」には、「HTTPS Only」を選択します。
  3. 「Origin Domain Name」には S3 バケットを選択します。
  4. ベストプラクティス通り、「Restrict Bucket Access」を選択します。こうすることで、バケットが直接アクセスされることから守ることができます。これで CloudFront ディストリビューションのドメイン名で、サイトにアクセスできるようになると思います。

ログイン メカニズム

次に、ログイン メカニズムを構築します。

ウェブサイトではログインさせるために、ボタンを押させることも出来ますし、自動的に認証情報が有効かどうか確認してからログインのフローにリダイレクトをさせる事もできます。

この例では、2 つ目のアプローチを取ります。ユーザがページを訪れると JavaScript を使ってすぐに状態を確認し、初回の訪問であれば認証情報を入手するためにリダイレクトさせます。このページにはログインの流れの中で API Gateway から再度リダイレクトされても来るため、ログインの進捗に合わせ ADFS のログインページにリダイレクトさせるのと同様に、届いた SAMLResponse のデータをキャプチャする事も必要です。今回の例では、以下のような流れになります。:

function loginWorkflow(){
    var activelogin = sessionStorage.getItem('activelogin');
    if (activelogin=='inProgress'){                                   //ADFS login redirect from API Gateway
        var samlResponse = getParameterByName('SAMLResponse');
        sessionStorage.removeItem(‘activelogin’);
        if (samlResponse != null){
            getSamlCredentials(samlResponse.replace(/\s/g, ''));
        }
    }
    else if (activelogin === null) {                                 //First page visit. Redirect to ADFS login.
        var RPID = encodeURIComponent('urn:amazon:webservices');
        var result = 'https://localhost/adfs/ls/IdpInitiatedSignOn.aspx' + "?loginToRp=" + RPID;
        sessionStorage.setItem('activelogin', 'inProgress');
        window.location = result;
    }
    else {//Credentials exist, page refresh, etc.
        console.log('activelogin already exists in session and the value is ' + activelogin);
    }
}

上記では、ADFS IdP への呼び出しを始める時にセッション変数を設定し、SAMLResponse (getParameterByName() で取り出せる)と一緒にWeb サイトに戻って来た時には getSamlCredentials() 関数を呼び出します。

AWS が必要とする SAMLResponse 値は POST binding のみがサポートされていますが、S3 の静的ウェブサイトは、GET リクエストしか受け取ることができません。このため、JavaScript アプリケーションが ADFS からの SAMLResponse を受け取るために API Gateway を利用します。Lambda をプロキシとして利用し、SAMLResponse をクエリ文字列に入れて静的ウェブサイトにリダイレクトで戻ってこさせます。

Lambda 関数の設定

  1. Lambda のコンソールで、GitHub レポジトリにある /Scenario1/lambda/redirect/index.js を使って samlRedirect という名前の関数をランタイムは Node.js 4.3 で作成します。実行ロールにはログ保存のために CloudWatch Logs の権限だけが必要です。
  2. 基本的な実行ロールが既になければ、関数作成時に新しいロールを Lambda のコンソール上で作成する事ができます。
  3. LOG_LEVELREDIRECT_URL という名前の環境変数を作成し、LOG_LEVEL には info を REDIRECT_URL には S3 の静的ウェブサイトの URL(静的ウェブホスティングを有効にしている場合にはバケットのプロパティに表示されているエンドポイント、あるいは先程記載したオプションの CloudFront ディストリビューション設定をした場合はドメイン名)を設定します。

API Gateway 設定

  1. API Gateway のコンソールで、SAMLAuth あるいは何か似た名前を付けた API を作成します。
  2. リソース」 を選択し、「SAML」という名前の子リソースを作成します。
    lambdasamlone_7.png
  3. POST」メソッドを作成します。「統合タイプ」には「Lambda」を選択し、「samlRedirect」関数を指定します。「メソッドリクエスト」の「HTTP リクエストヘッダー」に「SAMLResponse」を設定します。
    lambdasamlone_8.png

先ほど作成した samlRedirect 関数では、クエリ文字列に SAMLResponse を付けた静的ウェブサイトの URL を location プロパティに設定します。リダイレクトが実行されるように、API Gateway が 302 ステータスコードを返すように設定します。

  1. メソッドレスポンス」でステータスコード 200 を削除し、302 を追加します。「レスポンスヘッダ」に「Location」を設定し、「レスポンス本文」には「コンテンツタイプ」に「application/json」、「モデル」に「Empty」と設定します。
    lambdasamlone_9.png
  2. 「統合レスポンス」で「メソッドレスポンス」のステータスが 200 のものを削除し、302 のものを追加します。レスポンスヘッダ「Location」の「マッピングの値」には「integration.response.body.location」を設定します。
    lambdasamlone_10.png
  3. Lambda 関数が SAMLResponse と RelayState を受け取れるように、「統合リクエスト」で「コンテンツタイプ」が 「application/x-www-form-urlencoded」の「本文マッピングテンプレート」を以下のように追加します。:

    {
    "SAMLResponse" :"$input.params('SAMLResponse')",
    "formparams" : $input.json('$')
    }
  4. 変更を保存します。
  5. /saml リソースで「アクション」から「CORS の有効化」を選択します。「CORS を有効にして既存の CORS ヘッダを置換」を行いします。

  6. アクション」から「API のデプロイ」を選択します。「ステージ」には「Prod」や似た名前を使用します。「ステージエディター」で、「SDK 生成」を選択します。「プラットフォーム」には「JavaScript」を選択し、「SDK の生成」を行い、どこかのフォルダに保存します。ADFS の設定に必要になるため、トップに表示されている「URL の呼び出し」をメモします。

ADFS リダイレクト設定

  1. ADFS のコンソールで、「利用信頼関係」にある Amazon Web Services のプロパティを開き、「利用者を自動的に更新する」の項目を無効(チェックしない)にします。
    lambdasamlone_11.png
  2. エンドポイント」のタブを開き、既にあるエンドポイントを編集します。「バインディング」は「POST」のままで、「URL」には API Gateway でデプロイした API の「URL の呼び出し」を入力します。URL の最後に 「/saml」を追加します。
    lambdasamlone_12.png
    これまで構築してきたことを復習します。:

    • Web ページはユーザが認証情報を持っていなければ、ADFS のログインページにリダイレクトします。
    • 認証に成功したら、ADFS は SAMLResponse を付けて Web ページにユーザを戻します(API Gateway 経由)。
    • SAMLResponse は元の Web ページにクエリ文字列の一部として渡されます。

    Amazon Cognito から認証情報を取得するには、JavaScript コードでこのクエリ文字列を取り出し getCredentialsForIdentity 呼び出しのパラメータの一部として渡す必要があります。

  3. Amazon Cognito の Federated Identities のコンソールで、前提条件でセットアップした認証プールを開きます。認証プールの ID を確認し、以下の JavaScript コードの先頭に利用するリージョン名と同様に記載します。
    const identityPool = 'YOURCOGNITOPOOLID';
    AWS.config.region = 'COGNITOREGION'; // Region
    
    AWS.config.credentials = new AWS.CognitoIdentityCredentials({
        IdentityPoolId: identityPool
    });
    
    var identityId = localStorage.getItem('cognitoid');
    if (identityId != null){
        console.log('Identity ID: ' + identityId);
        loginWorkflow();
    } else {
        console.log('Calling GetID');
        AWS.config.credentials.get(function(){
            console.log('Identity ID: ' + AWS.config.credentials.identityId);
            localStorage.setItem('cognitoid', AWS.config.credentials.identityId);
            identityId = localStorage.getItem('cognitoid');
            loginWorkflow();
        });
    }

このコードは、アプリケーションでユーザに Amazon Cognito で生成した一意の ID を付与する最初の部分です。その上で、クエリ文字列に SAMLResponse の値を入れて返させるために loginWorkflow() を呼び出します。

以下は Stack Overflow で紹介されているサンプルのユーティリティ関数で、クエリ文字列から特定の項目を取り出すためのものです。

function getParameterByName(name, url) {
    if (!url) {
      url = window.location.href;
    }
    name = name.replace(/[\[\]]/g, "\\$&amp;");
    var regex = new RegExp("[?&amp;]" + name + "(=([^&amp;#]*)|&amp;|#|$)"),
        results = regex.exec(url);
    if (!results) return null;
    if (!results[2]) return '';
    return decodeURIComponent(results[2].replace(/\+/g, " "));
}

これで Web ページは ADFS から BASE64 エンコードされた SAMLResponse を取得しました。これを Amazon Cognito に渡せば AWS 認証情報を取得することが出来ます。Cognito の getSamlCredentials() 関数は以下のコードで、loginWorkflow() から呼び出されます。:

function getSamlCredentials(samlResponse) {
    var role = 'arn:aws:iam::ACCOUNTNUMBER:role/ADFS-Dev';
    var params = {
        IdentityId: identityId,
        CustomRoleArn: role,
        Logins: {
            'arn:aws:iam::ACCOUNTNUMBER:saml-provider/ADFS' : samlResponse,
        }
    };

    cognitoidentity.getCredentialsForIdentity(params, function(err, data) {
        if (err) console.log(err, err.stack);
        else {
            console.log('Credentials from Cognito: ');
            console.log(data.Credentials);
        }
    });
}

ARN にある ACCOUNTNUMBER は2つとも書き換える必要がある事に注意して下さい。今回は使用するロールがソースに直書きされています。もし、SAML クレームに複数のロールが入って返されないのであれば、このパラメータは省略することもできます。引き受けるロールを何かしらのロジックで選択するようにしたい場合もあると思います。このブログ記事のパート II ではクライアント側ではなくバックエンドでこれをできるようにする内容を含める予定です。

また、logins map にあるキーは IAM コンソールで作成した ADFS の IdP 登録の ARN で、値は samlResponse です。これもアカウント番号に書き換える必要があります。この情報により Amazon Cognito は SAMLResponse の値が信頼している対象の ADFS からのものであるか検証できます。

この時点でこのコードを試しても動作しません。Amazon Cognito の認証プールで、認証プロバイダとして SAML IdP を有効にする必要があります。

  1. Amazon Cognito の Federated Identities のコンソールで、プールを選択し、Authentication Providers までスクロールし、SAML を選択します。ADFS の IdP を示すチェックボックスを選択します。
  2. Save Changes をクリックします。
    lambdasamlone_13.png

次に、Amazon Cognito が有効な SAMLResponse アサーションを受け取った時に、ユーザがロールを引き受けられるように信頼ポリシーを変更します。Amazon Cognito は STSAssumeRoleWithWebIdentity API を呼び出してこれを実現します。

IAM のコンソールで、ADFS-Dev ロールの信頼ポリシーを以下のポリシーに編集します。(JavaScript コードのように適切な位置に Amazon Cognito のプール ID を挿入します。):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Federated": "cognito-identity.amazonaws.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "cognito-identity.amazonaws.com:aud": "YOURCOGNITOPOOLID"
        },
        "ForAnyValue:StringLike": {
          "cognito-identity.amazonaws.com:amr": "authenticated"
        }
      }
    }
  ]
}

これで Amazon Cognito の信頼ポリシーは設定できました。Web ページをテストして下さい。AWS サービスのリクエストへの署名に使える認証情報はコンソールログに (AccessKey, SecretKey, Token) の 3 つが含まれる形で “data” オブジェクトで出力されます。アプリケーションの共通のシナリオは、AWS_IAM 認証に設定された API Gateway のメソッド呼び出しを行うことです。認証情報は SDK のリクエストを使うことで渡す事ができます。上記の呼び出して指定したロールにその API を呼び出す権限があれば、メソッド呼び出しは成功します。

例えば、API Gateway で API を作成した場合、メソッドに AWS_IAM 認証を有効にできます。上記で指定されたロール(logins mapで指定)に、メソッド呼び出しの権限があることを確認して下さい。より詳しいことは、「API Gateway へのアクセスを IAM アクセス権限で制御する」をご覧下さい。API をデプロイした場合、SDK の生成を使ってそのステージの JavaScript SDK を生成してダウンロードできます。これからのメソッド呼び出しを Web ページに組み込む事ができます。より詳しいことは、「API Gateway で生成した JavaScript SDK を使用する」をご覧下さい。トピックの最後に、apigClientFactory.newClient コンストラクタにアクセスキーシークレットキーをどうやって渡すことができるか確認しましょう。

Amazon Cognito Identity は短期間だけ有効な認証情報(1 時間で無効になる)を利用するため、一時的なアクセスキー、シークレットキー、セッショントークンを、apigClientFactory.newClient コンストラクタに渡します。:

        var apigwClient = apigClientFactory.newClient({
        accessKey: data.Credentials.AccessKeyId,
        secretKey: data.Credentials.SecretKey,
        sessionToken: data.Credentials.SessionToken
    });

機能の動作確認ができる Web ページのコードは、GitHub レポジトリ内の /scenario1/website/index.html にあります。同じディレクトリにある configs.js ファイルを更新します。リージョン、Cognito Identity のプール ID、SAML IdP の ARN、ADFS-Dev ロールの ARN を変更します。SAM テンプレートを使って API Gateway に AWS_IAM 認証の MOCK エンドポイントを作成する事もでき、連携された認証情報が機能するか Web ページ上のテストボタンで確認する事ができます。

lambdasamlone_14.png

最後に

チュートリアルを通じて SAML 認証や Amazon Cognito を使った AWS 認証について深いレベルで理解する手助けになる事を願っています。まだ完了していなければ、少し時間をかけて頂き、GitHub レポジトリの samljs-serverless-sample にあるコードを実際に動かし確認して下さい。また感想についても教えてください。

このシナリオは最初のステップとして十分かもしれませんが、組織によってはより多くのことをする必要があるかもしれません。例えば、ロールの選択をユーザに任せたいやビジネスロジックに基づき独自の IAM 範囲を実装したい事もあるかと思います。このシリーズのパート 2 ではこれらをどのようにしたら実現できるかを見ていきたいと思います。

[訳注] このブログで紹介している方法では、ADFS におけるデフォルトの AWS の証明書利用者設定を変更します。そのため、同じ ADFS 環境で AWS のマネージメントコンソールへ認証連携してアクセスさせる事とは同時に実現できません。

原文: SAML for Your Serverless JavaScript Application: Part I (翻訳: SA 辻 義一)