IAM認証 + Cognito

IAM認証とCognitoを組み合わせる方法。CognitoがIAMを返して、そのIAMに基づいて、API GatewayはLambdaを実行しようする。

API Gatewayのリソースメニューにあるメソッドリクエストの設定で「認証」に「AWS_IAM」を選択してデプロイする。

Cognitoで認証するユーザは、「登録されているユーザ」と「登録されていないゲストユーザ」の二種類に大きく分けられる。会員と非会員で実行できるAPIを分けるなどに使える。登録されていないゲストユーザを使うときは以下の画像のように「認証されていないIDに対してアクセスを有効にする」にチェックをつける。

登録されているユーザ

Cognitoユーザープールを使用するのであれば、ユーザープールのアプリクライアントを設定し、フェデレーティッドアイデンティティの認証プロバイダーに記載する。

APIアクセス時、フェデレーティッドアイデンティティの「認証されたロール」のIAMでLambdaが実行可能であれば実行する。

ユーザープールでグループにユーザーを属させている場合、「認証されたロール」のIAMだけでなく、グループのIAMでLambdaが実行可能であれば実行する。

Node.jsクライアント実装例

実際に用意したLambdaとAPI Gatewayにアクセスしてみる。

Node.js version: 8

global.fetch = require("node-fetch");
const AmazonCognitoIdentity = require('amazon-cognito-identity-js');

// ユーザープール設定
const userPool = new AmazonCognitoIdentity.CognitoUserPool({
    UserPoolId : 'ap-northeast-1_XXXXXXXXX',
    ClientId : 'xxxxxxxxxxxxxxxxxxxxxxxxxx'
});

// ユーザー設定
const cognitoUser = new AmazonCognitoIdentity.CognitoUser({
    Username: 'xxxxx',
    Pool: userPool
});

// パスワード設定
const authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails({
    Password: 'xxxxxxxxxxx'
});

// ユーザープール/ユーザー/パスワードを使って認証
cognitoUser.authenticateUser(authenticationDetails, {
    onSuccess(result){
        // Web APIを認証/認可サーバで保護して他のアプリケーションに公開する場合、アクセストークンを使用する
        // Web APIを自分のアプリケーションの一部(バックエンドサービス)として作成して保護する場合、IDトークンを使用する
        //const accessToken = result.getAccessToken().getJwtToken();
        const idToken = result.getIdToken().getJwtToken();
        requestApi(idToken);
    },
    onFailure(err){
        console.error(err);
    },
    // 初回認証時のパスワードの変更
    newPasswordRequired(user_attributes, required_attributes){
        cognitoUser.completeNewPasswordChallenge(authenticationDetails.password, user_attributes, this);
    }
});


const requestApi = (idToken) => {
    const AWS = require('aws-sdk');
    const credentials = new AWS.CognitoIdentityCredentials({
        IdentityPoolId:'ap-northeast-1:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
        Logins: { "cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_XXXXXXXXX" : idToken }
    });
    AWS.config.update({
      credentials: credentials,
      region: 'ap-northeast-1'
    });

    const apigClientFactory = require('aws-api-gateway-client').default;

    AWS.config.credentials.get(function(err){
        if(!err){
            const apigClient = apigClientFactory.newClient({
                accessKey: AWS.config.credentials.accessKeyId,
                secretKey: AWS.config.credentials.secretAccessKey,
                sessionToken: AWS.config.credentials.sessionToken,
                region: 'ap-northeast-1',
                invokeUrl: 'https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod'
            });
            const params = {
            };
            const pathTemplate = '/api';
            const method = 'POST';
            const additionalParams = {
                queryParams: {
                }
            };

            const body = {
            };

            apigClient.invokeApi(params, pathTemplate, method, additionalParams, body)
                .then(function(result){
                    console.log(result.data);
                })
                .catch(function(result){
                    console.log(result);
                });
          }else{
              console.log(err);
          }
    });
};

登録されていないゲストユーザ

フェデレーティッドアイデンティティの「認証されていないロール」のIAMでLambdaが実行可能であれば実行する

Node.jsクライアント実装例

Node.js version: 8

const AWS = require('aws-sdk');
const credentials = new AWS.CognitoIdentityCredentials({
    IdentityPoolId:'ap-northeast-1:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
});
AWS.config.update({
  credentials: credentials,
  region: 'ap-northeast-1'
});

const apigClientFactory = require('aws-api-gateway-client').default;

AWS.config.credentials.get(function(err){
    if(!err){
        const apigClient = apigClientFactory.newClient({
            accessKey: AWS.config.credentials.accessKeyId,
            secretKey: AWS.config.credentials.secretAccessKey,
            sessionToken: AWS.config.credentials.sessionToken,
            region: 'ap-northeast-1',
            invokeUrl: 'https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod'
        });
        const params = {
        };
        const pathTemplate = '/api';
        const method = 'POST';
        const additionalParams = {
            queryParams: {
            }
        };

        const body = {
        };

        apigClient.invokeApi(params, pathTemplate, method, additionalParams, body)
            .then(function(result){
                console.log(result.data);
            })
            .catch(function(result){
                console.log(result);
            });
    }else{
        console.log(err)
    };
});

Cognitoユーザープールオーソライザー

Cognitoでユーザ認証ができるかどうかだけ確認する方法。

IAM認証 + Cognitoと異なり、フェデレーティッドアイデンティティは使用しないし、IAMも使用しない。

API Gatewayのオーソライザーメニューでオーソライザーを作成した後、API Gatewayのリソースメニューにあるメソッドリクエストの設定で「認証」に「Cognitoユーザープールオーソライザー」を選択してデプロイする。

オーソライザー作成時には次にあげる認証方法の「Lambdaオーソライザー」も作成できる。新規作成画面と作成したLambdaオーソライザーとCognitoユーザープールオーソライザーは以下の通り。

Cognitoユーザープールのユーザで認証した後、IDトークンをAuthorizationヘッダにセットして送信することで、Lambdaを実行する

Node.jsクライアント実装例

Node.js version: 8

global.fetch = require("node-fetch");
const AmazonCognitoIdentity = require('amazon-cognito-identity-js');

// ユーザープール設定
const userPool = new AmazonCognitoIdentity.CognitoUserPool({
    UserPoolId : 'ap-northeast-1_XXXXXXXXX',
    ClientId : 'xxxxxxxxxxxxxxxxxxxxxxxxxx'
});

// ユーザー設定
const cognitoUser = new AmazonCognitoIdentity.CognitoUser({
    Username: 'xxxxx',
    Pool: userPool
});

// パスワード設定
const authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails({
    Password: 'xxxxxxxxxxx'
});

// ユーザープール/ユーザー/パスワードを使って認証
cognitoUser.authenticateUser(authenticationDetails, {
    onSuccess(result){
        // Web APIを認証/認可サーバで保護して他のアプリケーションに公開する場合、アクセストークンを使用する
        // Web APIを自分のアプリケーションの一部(バックエンドサービス)として作成して保護する場合、IDトークンを使用する
        //const accessToken = result.getAccessToken().getJwtToken();
        const idToken = result.getIdToken().getJwtToken();
        requestApi(idToken);
    },
    onFailure(err){
        console.error(err);
    },
    // 初回認証時のパスワードの変更
    newPasswordRequired(user_attributes, required_attributes){
        cognitoUser.completeNewPasswordChallenge(authenticationDetails.password, user_attributes, this);
    }
});


const requestApi = (idToken) => {
    const AWS = require('aws-sdk');
    AWS.config.update({
      region: 'ap-northeast-1'
    });

    const apigClientFactory = require('aws-api-gateway-client').default;

    const apigClient = apigClientFactory.newClient({
      region: 'ap-northeast-1',
      invokeUrl: 'https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod'
    });
    const params = {
    };
    const pathTemplate = '/api';
    const method = 'POST';

    const additionalParams = {
      headers: {
        Authorization: idToken
      },
      queryParams: {
      }
    };

    const body = {
    };

    apigClient.invokeApi(params, pathTemplate, method, additionalParams, body)
        .then(function(result){
            console.log(result.data);
        })
        .catch(function(result){
            console.log(result);
        });
}

Lambdaオーソライザー

Lambdaに独自の認証ロジックを書く方法。Lambdaに独自の認証ロジック(Authorizationヘッダにセットしたトークンの検証等)を記述でき、認証する場合、Lambdaを実行可能なIAMポリシーを生成してreturnすると、Lambdaを実行する

Node.jsクライアント実装例

Node.js version: 8

const AWS = require('aws-sdk');
AWS.config.update({
  region: 'ap-northeast-1'
});

const apigClientFactory = require('aws-api-gateway-client').default;

const apigClient = apigClientFactory.newClient({
  region: 'ap-northeast-1',
  invokeUrl: 'https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod'
});
const params = {
};
const pathTemplate = '/api';
const method = 'POST';

const additionalParams = {
  headers: {
    // たとえば毎回Authorizationヘッダが変わるように、AES暗号化してみる。
    const CryptoJS = require('crypto-js');
    const key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
    Authorization: CryptoJS.AES.encrypt('xxxxxxx', key).toString()
  },
  queryParams: {
  }
};

const body = {
};

apigClient.invokeApi(params, pathTemplate, method, additionalParams, body)
    .then(function(result){
        console.log(result.data);
    })
    .catch(function(result){
        console.log(result);
    });

Lambda認証実装例

クライアント実装例で記載した通り、今回はAuthorizationヘッダにAES暗号化した毎回変わる値をセットし、Lambdaで値をチェックすることにした。

例えばサーバ間のAPI連携をする場合、固定のキーを渡すことでリクエスト元が正当なサーバであるとみなすという簡易な認証機能を作りたいとする。この仕様であれば次にあげる認証方法の「APIキー」で実現できるが、より安全性を高めるため、毎回ネットワークを流れるキーの値を変えたいとする。

Node.js version: 8

const CryptoJS = require('crypto-js');

let key;

exports.handler = async (event) => {
    const token = event.authorizationToken;
    key = process.env.key;
    const decodedToken = CryptoJS.AES.decrypt(token, key).toString(CryptoJS.enc.Utf8);
    
    if (decodedToken == 'xxxxxxx') {
        return generatePolicy('user', 'Allow', event.methodArn);
    } else {
        return generatePolicy('user', 'Deny', event.methodArn);
    }
};

// IAMポリシーを生成し返却します
var generatePolicy = function(principalId, effect, resource) {
    var authResponse = {};

    authResponse.principalId = principalId;
    if (effect && resource) {
        var policyDocument = {};
        policyDocument.Version = '2012-10-17'; 
        policyDocument.Statement = [];
        var statementOne = {};
        statementOne.Action = 'execute-api:Invoke'; 
        statementOne.Effect = effect;
        statementOne.Resource = resource;
        policyDocument.Statement[0] = statementOne;
        authResponse.policyDocument = policyDocument;
    }

    return authResponse;
};

APIキー

APIキーでアクセス制限する。APIキーをx-api-keyヘッダに設定したときのみLambdaを実行する。

AWSは認証機能として使用しないようにドキュメントに記載しているが、一般ユーザ間の通信ではなくサーバ間のAPI通信などAPIキーが外部に漏れない状況であれば、簡易的な認証として使用できる。

APIキーについてはcurlで簡単に叩けるほど簡単なので、Node.jsによる実装例は書かない。

curl -H 'x-api-key:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
     -X POST \
     https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/api

各認証方法の適した利用用途まとめ

認証方法 利用用途
IAM認証 + Cognito ユーザの属しているグループでLambdaの実行可否を制御するのに向いている。
Cognitoユーザープールオーソライザー ユーザのグループにかかわらず認証できればLambdaを実行可能とするのに向いている。
Lambdaオーソライザー サーバ間API連携等に向いている。独自の認証ロジックを書けるので、やろうと思えば大抵のことができる。
APIキー クライアントアプリやWEBブラウザからユーザに見られなければ問題ないため、サーバ間API連携等に向いている。

API GatewayとLambdaをServerless Frameworkでつくる

今回の動作検証で使用したAPI GatewayとLambda(とついでにDynamoDB)はServerless Frameworkで作成した。

serverless.yml

service: test-api

provider:
  name: aws
  runtime: nodejs8.10

  region: ap-northeast-1
  stage: ${opt:stage, 'dev'}

  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:*
      Resource: "*" 

package:
  exclude:
    - .git
    - ./conf

functions:
  api:
    handler: index.handler
    name: test-${self:provider.stage}-api
    environment:
      tableName:        ${file(./conf/${self:provider.stage}.yml):tableName}
      key:              ${file(./conf/${self:provider.stage}.yml):key}
    package:
      include:
        - index.js
        - node_modules
    events:
      - http:
          path: api
          method: post
          integration: lambda-proxy

resources:
  Resources:
    Api:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: test-${self:provider.stage}-api
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
#        ProvisionedThroughput:
#          ReadCapacityUnits: 5
#          WriteCapacityUnits: 5
        BillingMode: PAY_PER_REQUEST

conf

conf/prod.yml

tableName: test-prod-api
key: '12345678901234567890123456789012'

conf/dev.yml

tableName: test-dev-api
key: '12345678901234567890123456789012'

デプロイと削除

sls deploy -v --stage prod
sls remove -v --stage prod