Cognito UserPoolとAPI Gatewayで認証付きAPIを立てる

(2018-02-25)

UserPoolを作成。デフォルト設定はこんな感じ。 必須項目や、確認メールの文面などを自由にカスタマイズでき、 登録時などのタイミングでLambdaを発火させることもできる。

デフォルト設定

作成したUserPoolにアプリクライアントを追加する。 ブラウザで使うのでクライアントシークレットはなし。

クライアント側

amazon-cognito-identity-jsを使う。

依存するjsを持ってくる。

$ wget https://raw.githubusercontent.com/aws/amazon-cognito-identity-js/master/dist/amazon-cognito-identity.min.js
$ wget https://raw.githubusercontent.com/aws/amazon-cognito-identity-js/master/dist/aws-cognito-sdk.min.js

Sign UpからAPIを呼ぶところまでのボタンを並べた。 SignInすると以下のデータをそのページのドメインのLocal Storageに保持する。

CognitoIdentityServiceProvider.<clientId>.<name>.idToken
CognitoIdentityServiceProvider.<clientId>.<name>.accessToken
CognitoIdentityServiceProvider.<clientId>.<name>.refreshToken
CognitoIdentityServiceProvider.<clientId>.<name>.clockDrift
CognitoIdentityServiceProvider.<clientId>.LastAuthUser

APIを呼ぶときはidToken(JWT)をAuthorization Headerに乗せる。

<button id="signUp">Sign Up</button>
<p><label>Code:<input type="text" id="code"></label></p>
<button id="confirm">Confirm</button>
<button id="signIn">Sign In</button>
<button id="whoAmI">Who am I?</button>
<button id="requestAPI">Request API with token</button>
<button id="signOut">Sign Out</button>

<script src="aws-cognito-sdk.min.js"></script>
<script src="amazon-cognito-identity.min.js"></script>
<script>
const USER_NAME = "*****";
const USER_PASSWORD = "*****";
const USER_EMAIL = "*****";
class CognitoUserPoolAuth {
  constructor(UserPoolId, clientId, apiEndpoint) {
    const poolData = {
      UserPoolId : UserPoolId,
      ClientId : clientId
    };
    this.userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
    this.apiEndpoint = apiEndpoint
  }
  
  signUp(userName, password, email) {
    const attributeList = [];
    if (email) {
      attributeList.push(new AmazonCognitoIdentity.CognitoUserAttribute({
        Name : 'email',
        Value : email
      }));
    }

    return new Promise((resolve, reject) => {
      this.userPool.signUp(userName, password, attributeList, null, (err, result) => {
        if (err) {
          return reject(err);
        }
        resolve(result);
      });
    });
  }

  confirmCode(userName, confirmCode) {
    const cognitoUser = this.getCognitoUser(userName);
    return new Promise((resolve, reject) => {
      cognitoUser.confirmRegistration(confirmCode, true, (err, result) => {
        if (err) {
          return reject(err);
        }
        resolve(result);
      });
    })
  }

 signIn(userName, password) {
    const authenticationData = {
      Username : userName,
      Password : password,
    };
    const authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData);
    const cognitoUser = this.getCognitoUser(userName);
    return new Promise((resolve, reject) => {
      cognitoUser.authenticateUser(authenticationDetails, {
        onSuccess: (result) => {
          resolve(result.getAccessToken().getJwtToken());
        },

        onFailure: function(err) {
          reject(err);
        },
      });
    });
  }

  signOut() {
    const currentUser = this.currentUser();
    if (!currentUser) return;
    const cognitoUser = this.getCognitoUser(currentUser.username);
    if (!cognitoUser) return;
    cognitoUser.signOut();
  }

  getCognitoUser(userName) {
    const userData = {
      Username : userName,
      Pool : this.userPool
    };
    return new AmazonCognitoIdentity.CognitoUser(userData);
  }

  currentUser() {
    return this.userPool.getCurrentUser()
  }
  
  getJwtToken() {
    return new Promise((resolve, reject) => {
      const cognitoUser = this.currentUser();
      if (!cognitoUser) {
        return reject("unauthorized");
      }
      cognitoUser.getSession((err, result) => {
        if (err) { 
          return reject(err);
        }
        resolve(result.getIdToken().getJwtToken());
      });
    })
  }

  async requestAPIWithToken() {
    const token = await this.getJwtToken().catch(
      (err) => {
        console.log(err);
      } 
    );
    const headers = token ? { 'Authorization': token } : {};
    return fetch(this.apiEndpoint, {
      headers: headers
    }).then((response) => {
      return response.json();
    });
  }
}

// -------------
// Handler
// -------------

const auth = new CognitoUserPoolAuth(
    "<poolID>", 
    "<clientID>",
    "https://*****.execute-api.us-east-1.amazonaws.com/dev/secret"
)

document.getElementById("signUp").addEventListener("click", async () => {
    const result = await auth.signUp(USER_NAME, USER_PASSWORD, USER_EMAIL).catch((err) => {
        if (err.code === "UsernameExistsException") {
        return Promise.reject("User name is already used");
        } else {
        return Promise.reject(err);
        }
    });
    console.log(`signUp successfully`);
}, false);

document.getElementById("confirm").addEventListener("click", async () => {
    const code = document.getElementById("code").value;
    const result = await auth.confirmCode(USER_NAME, code);
    console.log(`confirm successfully`);
}, false);

document.getElementById("signIn").addEventListener("click", async () => {
    const result = await auth.signIn(USER_NAME, USER_PASSWORD).catch((err) => {
        if (err.code === "UserNotConfirmedException") {
        return Promise.reject("Confirm your email");
        } else {
        return Promise.reject(err);
        }
    });
console.log(`signIn successfully`);
}, false);

document.getElementById("whoAmI").addEventListener("click", async () => {
    console.log(auth.currentUser());
}, false);

document.getElementById("requestAPI").addEventListener("click", async () => {
    console.log(await auth.requestAPIWithToken());
}, false);

document.getElementById("signOut").addEventListener("click", async () => {
    auth.signOut();
    console.log("signout successfully");
}, false);
</script>

API側

Serverless FrameworkでCognitoのJWTを認証に使うには authorizerのarnにUserPoolのARNを入れる。

Serverless FrameworkでLambdaをデプロイする - sambaiz-net

$ cat serverless.yml
service: cognitoapi

provider:
  name: aws
  runtime: nodejs6.10

functions:
  createTodo:
    handler: handler.secret
    events:
      - http:
          path: secret
          cors: true
          method: get
          authorizer:
            arn: ***** # UserPool's ARN

JWTは.で区切った真ん中がBase64 encodeされたpayloadになっている。 これをdecodeして返してみる。

OpenID ConnectのIDトークンの内容と検証 - sambaiz-net

$ cat handler.js
'use strict';

module.exports.secret = (event, context, callback) => {
  const payload = JSON.parse(
    new Buffer(
      event.headers.Authorization.split(".")[1], 
      "base64"
    ).toString()
  );
  const response = {
    statusCode: 200,
    body: JSON.stringify({
      userInfo: payload
    }),
    headers: {
      "Access-Control-Allow-Origin": "*"
    },
  };
  callback(null, response);
};

こんな感じ。Sign Upしたときにコード付きのメールが送られている。

動作