読者です 読者をやめる 読者になる 読者になる

Make Local Happiness

自分の幸せは自分で作る!!!

serverlessでCUSTOM AUTHを利用する時はTTLの設定に気をつけて!!

Serverless Advent Calendar 2016 10日目の記事です。 qiita.com

みなさんAmazon API Gateway使っていますか?

API Gatewayとは? サーバなしでAPIを作成できる便利なツールです。 エンドポイントを作成できURLやリクエストのメソッドに応じてLambdaを紐付けることも可能です。

今年はServerless Frameworkがv1に変わりAPI GatewayやLambdaがかなり使いやすくなったと思います。

さらにAPI GatewayではAPIの認証も3つの方法を設定することができます。

  1. APIキーによる認証
  2. IAMによる認証
  3. 独自の認証

今回は、3の「独自の認証」をserverless Frameworkを使って利用した際に、TTLの設定が原因で認証されないことがあった話をしたいと思います。

作成したソースはGithub上に公開しております。

github.com

API Gatewayの認証について

3つのAPI Gatewayについて簡単にご紹介していきます。

1. APIキーによる認証

1つ目はとってもシンプルでx-api-keyをリクエストのヘッダに含め、事前に設定したAPIキーが合えばリクエストを正常に返してくれます。

2. IAMによる認証

2つ目はIAMを使った認証で、IAMのroleにexecute-api:InvokeのActionが適切に設定されており、AWS Signature Version 4を使って認証付きのリクエストを送ると認証されます。

方法としてはAmazon Cognitoを使って一時的なIAM roleを発行し、認証するのがよいかと思います。

もちろん、API認証専用のユーザを作ってもできますが、ユーザを削除するか、ロールを外す以外に認証を外すことができないので、オススメはできないです。 CognitoからIAM Roleを発行してもらうAPIを作成したり、そのためのRole設定だったり、AWS Signature Version 4を使った認証情報の作成だったりと、ものすごく面倒くさいです。

3. 独自の認証

3つ目の独自の認証はLambdaを作成し、独自の認証を作成することができます。 独自のトークンをリクエストのヘッダーに設定し、そのトークンのチェックをLambdaに書くことができます。 デフォルトではAuthorizationをヘッダーに設定します。

私はログイン後にJWT(Json Web Token)のトークンを発行し、ログイン後に利用するAPIの認証に利用しました。

実際にやってみる

以下の4つを作成し、どこでTTLが問題になるかを検証していきます。

  • createToken (node function)
  • jwtAuthorizer (Lambda)
  • customAuthAPI (API Gateway)
  • hello (API Gateway)

serverless のプロジェクトを作成する

serverlessのセットアップは、公式ページにインストールとCredentialsの設定はあるので、こちら参照してください。

プロジェクトを作成し、最初にデプロイしてみます。

$ serverless create --template aws-nodejs --path serverless-custom-auth-example

Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/repos/serverless/serverless-custom-auth-example"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.2.1
 -------'

Serverless: Successfully generated boilerplate for template: "aws-nodejs"


$ cd serverless-custom-auth-example
$ serverless deploy

Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading service .zip file to S3 (733 B)...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
............
Serverless: Stack update finished...

Service Information
service: serverless-custom-auth-example
stage: dev
region: us-east-1
api keys:
  None
endpoints:
  None
functions:
  serverless-custom-auth-example-dev-hello: arn:aws:lambda:us-east-1:xxxxxxxxxx:function:serverless-custom-auth-example-dev-hello

jwtAuthorizerを作成

jsonwebtokenを利用するので、npm でインストールします。

$ mkdir jwtAuthorizer
$ cd jwtAuthorizer
$ npm init
$ npm install --save jsonwebtoken
$ vim handle.js

実際にコードを書いていきます。 CustomTokenでLambdaを使用した際に、リクエストでヘッダに設定されたAuthrizationのパラメータはLambda上ではevent.authorizationTokenに入ってきます。

ヘッダの情報は以下のような形式をを想定するので、Bearerは認証には不要なので、splitで分割し、後ろのトークン部分を使いjwt.verifyでトークンを複合していきます。

Bearer eySDSGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9qZWN0IjoidGVzdCIsImlhdCI6MTQ4MTE3NjI4NiwiZXhwIjoxNDgxMTc5ODg2fQ.X3k9VtnfSASDSQwDDSDSU8ZEdBonRmp7KClokOJHmQ

トークンには鍵の他に幾つかパラーメータを設定することができるので、 今回は鍵の有効期限(expiresIn)と idを設定することを想定して書いていきます。

トークンの認証が通らなかった場合は、500エラーでUnauthorizedで返し、 トークンを複合した際にidがuserではなかった場合は、403エラーで呼び出しを許可しないようにします。 トークンが正しく、デコードした際のidがuserだった場合に、APIの実行を許可するRoleを発行されます。execute-api:InvokeというのがAPIを実行できる権限になります。

const jwt = require('jsonwebtoken');
const jwtOptions = {
  secretOrKey: 'serverless-custom-auth-example',
  expiresIn: { expiresIn: '1h'},
};

const generatePolicy = function(principalId, effect, resource) {
  const authResponse = {};
  authResponse.principalId = principalId;

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

module.exports.jwtAuthorizer = (event, context) => {
  const jsonWebToken = event.authorizationToken.split(' ')[1];

  jwt.verify(jsonWebToken, jwtOptions.secretOrKey, (err, decode) => {
    if (err) {
      return context.fail("Unauthorized");
    }

    if (decode.id !== 'user') {
      return context.succeed(generatePolicy('user', 'Deny', event.methodArn));
    } else {
      return context.succeed(generatePolicy('user', 'Allow', event.methodArn));
    }
  });
};

APIの認証にjwtAuthorizerを設定する

jwtAuthorizerのlambdaの設定とHelloのAPIの作成をしていきます。

jwtAuthorizerのLambdaを設定

$ vim serverless.yml

  jwtAuthorizer:
    handler: jwtAuthorizer/handler.jwtAuthorizer
    memorySize: 128
    package:
      include:
        - jwtAuthorizer/handler.js
        - jwtAuthorizer/package.json
        - jwtAuthorizer/node_modules

hello APIを作成

以下のようにevents配下を追加していくことで、 APIが設定することができます。

$ vim serverless.yml

  hello:
    handler: handler.hello
    memorySize: 128
    package:
      include:
        - handler.js
    events:
      - http:
          path: hello
          method: post

hello APIにCUSTOM Authの設定を追加

authorizerに先程作成した、jwtAuthorizerの名前を指定するだけです。 すごく簡単です。

※ integrationにlambdaを設定しない場合、 APIのレスポンスが紐付かないことがあったので、お気をつけください。

$ vim serverless.yml

  hello:
    handler: handler.hello
    memorySize: 128
    package:
      include:
        - handler.js
    events:
      - http:
          path: hello
          method: post
          integration: lambda
          cors: true
          authorizer:
            name: jwtAuthorizer

デプロイしてみる

serverless.ymlの設定ができたところで、デプロイしてみます。

$ serverless deploy
Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading service .zip file to S3 (1.21 MB)...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
................................
Serverless: Stack update finished...

Service Information
service: serverless-custom-auth-example
stage: dev
region: us-east-1
api keys:
  None
endpoints:
  POST - https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/hello
functions:
  serverless-custom-auth-example-dev-hello: arn:aws:lambda:us-east-1:xxxxxxx:function:serverless-custom-auth-example-dev-hello
  serverless-custom-auth-example-dev-jwtAuthorizer: arn:aws:lambda:us-east-1:xxxxxxxxxx:function:serverless-custom-auth-example-dev-jwtAuthorizer

AWS マネジメントコンソールからAPI Gatewayを確認するとhello のPOSTメソッドの認証にjwtAuthorizerが設定されていることが確認できます。

f:id:iwate_takayu:20161211000558p:plain

ためしに、curlコマンドで叩いてみます。

curl -X POST https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/hello
{"message":"Unauthorized"}

はい。ヘッダーに何も設定していないのでうまくいきません。。

トークン作成の関数を作成しログインが成功するか試してみる

createTokenの関数を作成します。

$ mkdir createToken
$ cd createToken
$ npm init
$ npm install --save jsonwebtoken
$ vim index.js

'use strict';

const jwt = require('jsonwebtoken');
const jwtOptions = {
  secretOrKey: 'serverless-custom-auth-example',
  expiresIn: { expiresIn: '1h'},
};

const payload = { id: 'user'};
const token = jwt.sign(payload, jwtOptions.secretOrKey, jwtOptions.expiresIn);

console.log(`token: ${token}`);

実行するとTokenが取得できるので、Hello APIをためしてみる

$ node index.js 
token: dasdasdasdasdasdasdasda.eyJpZCI6InVzZXIiLCJpYXQiOjE0ODEzODAwMDMsImV4cCI6MTQ4MTM4MzYwM30.hhV0IVMWJAZGOixzsyNbluL4Eqe4cgtlk7NtA3SHaRI

$ curl -X POST https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/hello -H 'Authorization: Bearer dasdasdasdasdasdasdasda.eyJpZCI6InVzZXIiLCJpYXQiOjE0ODEzODAwMDMsImV4cCI6MTQ4MTM4MzYwM30.hhV0IVMWJAZGOixzsyNbluL4Eqe4cgtlk7NtA3SHaRI' | jq

{
  "statusCode": 200,
  "body": "{\"message\":\"Go Serverless v1.0! Your function executed successfully!\"}"
}

はい!いい感じですね!!!

2回目のリクエストが 403エラーになる

タイトルが答えなのですが、 試していきます。

2つ目のエンドポイントを作っていきます。 全然関係ないですが、最近裏サンデーの「Good Night World」がすごい面白くて、 しかも日曜日が更新日なので、今はワクワクしながらこの記事を書いているので、 Helloの反対のGoodNight関数を作ります。

$ vim handler.js

module.exports.goodNgiht = (event, context, callback) => {
  callback(null, { Message: 'Good Night World!'});
};

serverless.ymlにAPIの設定を追加

  goodNight:
    handler: handler.goodNight
    memorySize: 128
    package:
      include:
        - handler.js
    events:
      - http:
          path: good-night
          method: post
          integration: lambda
          cors: true
          authorizer:
            name: jwtAuthorizer

デプロイする

$ serverless deploy

Service Information
service: serverless-custom-auth-example
stage: dev
region: us-east-1
api keys:
  None
endpoints:
  POST - https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/hello
  POST - https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/good-night
functions:
  serverless-custom-auth-example-dev-hello: arn:aws:lambda:us-east-1:xxxxxxxx:function:serverless-custom-auth-example-dev-hello
  serverless-custom-auth-example-dev-goodNight: arn:aws:lambda:us-east-1:xxxxxxxx:function:serverless-custom-auth-example-dev-goodNight
  serverless-custom-auth-example-dev-jwtAuthorizer: arn:aws:lambda:us-east-1:xxxxxxxx:function:serverless-custom-auth-example-dev-jwtAuthorizer

これで準備はすべて完了です。 ためしてみます。

2回目のリクエストが 403エラーになるか確認

1回目にHello APIを叩くと成功!!

$ curl -X POST https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/hello -H 'Authorization: Bearer dsdadasdasdasdasdas.eyJpZCI6InVzZXIiLCJpYXQiOjE0ODEzODAwMDMsImV4cCI6MTQ4MTM4MzYwM30.hhV0IVMWJAZGOixzsyNbluL4Eqe4cgtlk7NtA3SHaRI' | jq

{
  "statusCode": 200,
  "body": "{\"message\":\"Go Serverless v1.0! Your function executed successfully!\"}"
}

続いてすぐにGoodNight APIを叩くと、、403エラーになりました。

$ curl -X POST https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/good-night -H 'Authorization: Bearer dsdadasdasdasdasdas.eyJpZCI6InVzZXIiLCJpYXQiOjE0ODEzODAwMDMsImV4cCI6MTQ4MTM4MzYwM30.hhV0IVMWJAZGOixzsyNbluL4Eqe4cgtlk7NtA3SHaRI' | jq

{
  "Message": "User is not authorized to access this resource"
}

TTLの設定を追加する

これ本当になんでだよって凄い悩んだんですが、 TTLを設定するだけで解消する問題でした。 一度目の認証でキャッシュができ、2回目のアクセスでは同じトークンで認証できないようになっていました。 デフォルトの設定だと300秒になっているので(たしか。。)、この時間を短くします。

serverless.ymlにTTLを設定する

authorizerにresultTtlInSecondsを設定します。

authorizer:
  name: jwtAuthorizer
  resultTtlInSeconds: 1

これでデプロイすれば、2回目の認証も成功できます。 めでたし、めでたし!

※たまに他の人のコードで ttlを設定していますが、v1になってからなのか、もともとなのかはわかりませんが、 これだと設定できていません。騙されないように気をつけてください。
まあ、短い方がいいので、こっちで設定できたほうがいいんですけどね。

ttl: 1

まとめ

API Gatewayの Custom AuthはIAM認証よりも簡単に設定できるので、かなり使いやすいのではないかと思います。 ただ、Serverless Frameworkはまだまだドキュメントが少なく今回のようなTTLの設定だけでかなりハマったりするので、今後沢山の人が使い、事例ができてきて安定してくるといいなと思っています。

ではよいServerlessライフを。