SHIINBLOG

API Gatewayのカスタム認証でワンタイムパスワードを使ってみた

概要

API Gatewayの認証でなんとなく(時間ベースの)ワンタイムパスワード(以後、TOTP)を使いたくなったので作ってみる。
基本的にnodejsを使用して構築する。

認証方法

API Gatewayのカスタム認証でTOTPの認証を行う。
TOTPのライブラリにはSpeakeasy(GitHub)を使う。
また、Google Authenticatorなどに登録するためのQRコードの生成にはQRCode Terminal Edition(GitHub)を使用する。

構築

TOTP用の鍵(?)とAuthenticator登録用のQRコードを生成する

まずは、必要なライブラリをインストールする。

$ mkdir make-key
$ cd make-key
$ npm install speakeasy
$ npm install qrcode-terminal

インストールしたら、鍵を作りQRコードを表示するgenerate.jsを作成する。

"use strict";
const fs = require('fs'); // 鍵のテキストを保存する
const speakeasy = require('speakeasy');
const qrcode = require('qrcode-terminal');

// TOTPの鍵の情報を生成する
const secret = speakeasy.generateSecret({length: 20});

// TOTPの鍵をテキスト保存する
const key = secret.base32;
fs.writeFile("totp_secret_base32.txt", key);

// 登録用QRコードを表示する。
const url = secret.otpauth_url;
qrcode.generate(url);

実行すると以下のようになる。 f:id:luciferous_report:20170301133650p:plain (Warningが出ているが気にしない) このQRコードが登録用のものになる。 TOTPの鍵は次のように作られている。

$cat totp_secret_base32.txt
KJ4DGPZ4LM7DU2KWPVOV222YLNEFKJLT

この値は次のカスタム認証用Lambda関数で使用する。

カスタム認証用Lambda関数の作成

こちらもまずはライブラリのインストールを行う。

$ mkdir auth-method
$ cd auth-method
$ npm install speakeasy

次に認証用のLambda関数index.jsを書きます。

'use strict';
const speakeasy = require('speakeasy');

const generatePolicy = function(principalId, isAllow, resource) {
  const effect = isAllow? "Allow": "Deny";
  // IAM ポリシーを生成する
  return {
    principalId: principalId,
    policyDocument: {
      Version: '2012-10-17',
      Statement: [{
        Action: 'execute-api:Invoke',
        Effect: effect,
        Resource: resource
      }]
    }
  };
};

exports.handler = (event, context, callback) => { 
  // トークンを取得
  const token = event.authorizationToken;
  const key = process.env.TOTP_SECRET;
  const result = speakeasy.totp.verify({
    secret: key,
    encoding: 'base32',
    token: token,
    window: 2
  });
  if (result){
    callback(null, generatePolicy('user', true, event.methodArn));
  }else{
    callback(generatePolicy('user', true, event.methodArn));
  }
};

Lambdaに登録するためのzipファイルsource.zipを作成します。

$ zip -r source.zip index.js node_modules/

このsource.zipをLambda関数作成時にアップロードします。
この際、環境変数を設定します。
キー"TOTP_SECRET“にtotp_secret_base32.txtの中身の文字列を入力します。
Lambda関数の作成ができました。

API Gatewayにカスタム認証を設定する

API Gatewayで新しいAPIを作成する(ここは既存のAPIでも問題はない)。 ここでは"totp“という名前で作成したことにする。 f:id:luciferous_report:20170302113305p:plain APIを作成したら左端のカラムを下にスクロールさせて、作成したAPIのオーソライザーを選択します。
Lambda関数のリージョンや名前などを入力し作成ボタンを押します。
(キャッシュの仕組み等は検証してないので結果のTTLを”0“に設定しています)
f:id:luciferous_report:20170302113800p:plain 作成すると下にオーサライザーのテストというものができるので、IDトークンにワンタイムパスワードを入れてどういう動きになるかを見るといいかもしれません。

次に、APIのリソースを作成します(リソースの作成は、左端のカラムのtotpのリソースです)。
ここでは、

  1. CORSを有効にしたgetリソースを作成し、
  2. getリソースにGETメソッドを作成し、統合タイプをMockにする。

ということにします。 f:id:luciferous_report:20170302114939p:plain メソッドリクエストをクリックし、認証に作成したオーソライザーを選択します。 f:id:luciferous_report:20170302115111p:plain オーソライザーを設定したら、APIをデプロイします。
これでAPIの構築は完了です。

curlで叩いてみる

curlコマンドで叩いてみます。
f:id:luciferous_report:20170302123006p:plain 最初はワンタイムパスワードを渡してないので当然失敗しています。
二度目は正規のワンタイムパスワードを渡しているので成功しています。

やってみて

結局のところは、カスタム認証に使うLambda関数の分岐条件にワンタイムパスワードを使うだけなので検証前のワクワク感がすぐなくなってしまいました。
これにユーザ認証まで噛ませようとすると、JSON文字列を渡してパースするほうがいいのかなぁとも思うところです。 個人用途のちょっとした認証だったら便利そうです。