Auth0 + API Gateway でM2M認証・認可をやってみた

2019.06.24

おつかれさまです。サーバーレス開発部の新井です。

今回は、Auth0のM2M認証で払いだされたアクセストークンを、API GatewayのLambda Authorizerで認可するまでの処理を解説します。

ちなみにM2M認証と書いてますが、0Auth2.0で言うところのClient Credentials Grantにあたります。

Auth0ではM2Mでの認証フローとして紹介されています。

  • https://auth0.com/blog/jp-using-m2m-authorization/
  • https://auth0.com/docs/flows/concepts/client-credentials
  • https://auth0.com/docs/api/authentication?http#client-credentials-flow

また、今回最終的に作成したサンプルコードをGitHubにまとめてあるので、先にURLだけ載せておきます。GitHubサンプルコード

前置きが長くなりましたが、さっそく始めていきたいと思います!

概要図

やってみる

下準備

まずは、API Gatewayを先に作成しておく必要があります。

今回も、AWS SAMでさくっとデプロイしちゃいます。

# setup
$ sam --version
SAM CLI, version 0.17.0
$ sam init --runtime nodejs10.x --name <your_project_name>

# inside project
$ tree -L 2
.
├── event.json
├── hello-world
│   ├── app.js
│   ├── package.json
│   └── tests
├── packaged.yaml
├── package-lock.json
├── README.md
└── template.yaml

# create s3 bucket for your artifacts
$ aws s3 mb s3://<your_bucket_name>
make_bucket: <your_bucket_name>

# start deploy
$ sam build
$ sam package --s3-bucket <your_bucket_name> --output-template-file packaged.yaml
$ sam deploy --template-file packaged.yaml --stack-name <your_stack_name> --capabilities CAPABILITY_IAM

デプロイが完了した後、コンソールからAPI Gatewayが作成されたのを確認して、URLを控えておきます。

Auth0側の設定

コンソールにログインして、新規APIを作成します。

必要情報を入力していきます。Identifierに先ほど控えたAPI GatewayのURLを入力します。

APIの登録が完了すると、テスト用のアプリケーションが自動で作成されます。

ここまでこれば、下記のコマンドでアクセストークンを取得できるようになります。

$ curl --request POST \
  --url 'https://<YOUR_DOMAIN>/oauth/token' \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data 'audience=<API_IDENTIFIER>&grant_type=client_credentials&client_id=<YOUR_CLIENT_ID>&client_secret=<YOUR_CLIENT_SECRET>'
{"access_token":<token>,"expires_in":86400,"token_type":"Bearer"}

入力に必要な情報は、Applicationsに作成されているAPI GatewayのSettingsから確認できます。(※audienceには先ほど入力した、Identifierが入ります。)

これでクライアントアプリケーションがM2M認証でアクセストークンを取得できるようになりましたね!

Auth0のM2M認証の設定については、こちらのブログでも紹介されているので、参考にどうぞ。

AWS側の設定

ここからは、AWS側での認可処理をAPI GatewayのLambda Authorizerで実装していきます。

まずは、Lambda Authorizer用にtemplate.yamlを少し変更を加えます。AWS SAMでLambda Authorizerの設定方法はこちらに記載があります。

...
Parameters:
  JwksUri:
    Type: String
  Audience:
    Type: String
  TokenIssuer:
    Type: String
Resources:
  ServerlessAPI:
    Type: AWS::Serverless::Api
    Properties:
      StageName: Dev
      Auth: # API GatewayにLambda Authorizerの設定を追加
        DefaultAuthorizer: Auth0Authorizer
        Authorizers:
          Auth0Authorizer:
            FunctionPayloadType: TOKEN
            FunctionArn: !GetAtt Auth0AuthorizerFunction.Arn
            Identity:
              Header: Authorization
              ValidationExpression: ^Bearer [-0-9a-zA-Z\._]*$
              ReauthorizeEvery: 0
  Auth0AuthorizerFunction: # Lambda Authorizerを追加
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello-world/
      Handler: auth.lambdaHandler
      Runtime: nodejs10.x
      Environment:
        Variables:
          JWKS_URI: !Ref JwksUri
          AUDIENCE: !Ref Audience
          TOKEN_ISSUER: !Ref TokenIssuer
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello-world/
      Handler: app.lambdaHandler
      Runtime: nodejs10.x
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get
            RestApiId: !Ref ServerlessAPI
...

次に、Lambda Authorizerの処理を実装していきます。

まず、hello-world/直下に移動し、新しく下記のライブラリをインストールします。

$ npm i jwks-rsa jsonwebtoken util --save

次に、auth.jsというファイル名で、Lambda Authorizerの中身の処理を書いていきます。

'use strict';

const jwksClient = require('jwks-rsa');
const jwt = require('jsonwebtoken');
const util = require('util');

const getToken = (params) => { // eventからToken情報を取り出す
  if (!params.type || params.type !== 'TOKEN') {
    throw new Error('Expected "event.type" parameter to have value "TOKEN"');
  }
  const tokenString = params.authorizationToken;
  if (!tokenString) {
    throw new Error('Expected "event.authorizationToken" parameter to be set');
  }
  const match = tokenString.match(/^Bearer (.*)$/);
  if (!match || match.length < 2) {
    throw new Error(`Invalid Authorization token - ${tokenString} does not match "Bearer .*"`);
  }
  return match[1];
};

const getAuthentication = async (token) => { // Tokenの検証
  try{
    const decoded = jwt.decode(token, { complete: true });
    if (!decoded || !decoded.header || !decoded.header.kid) {
      throw new jwt.JsonWebTokenError('invalid token');
    }

    const client = jwksClient({ jwksUri: process.env.JWKS_URI });
    const getSigningKey = util.promisify(client.getSigningKey);
    const key = await getSigningKey(decoded.header.kid);
    const signingKey = key.publicKey || key.rsaPublicKey;
    const tokenInfo = await jwt.verify(token, signingKey, {
      audience: process.env.AUDIENCE,
      issuer: process.env.TOKEN_ISSUER
    });
    return tokenInfo;
  } catch (error) {
    if (error instanceof jwt.TokenExpiredError) {
      console.info(error);
      return null;
    } else if (error instanceof jwt.JsonWebTokenError) {
      console.info(error);
      return null;
    } else {
      throw error;
    }
  }
};

const generatePolicy = async (principalId, effect, resource, context) => { // ポリシーの生成
  return {
    principalId: principalId,
    policyDocument: {
      Version: '2012-10-17',
      Statement: [
        {
          Action: 'execute-api:Invoke',
          Effect: effect,
          Resource: resource
        }
      ]
    },
    context: context
  };
};

exports.lambdaHandler = async (event) => {
  try {
    console.log(event);
    const token = await getToken(event);
    const res = await getAuthentication(token);
    let policy;
    if (!res){
      policy = await generatePolicy('', 'Deny', event.methodArn, { msg: 'failure' });
    } else{
      policy = await generatePolicy(res.sub, 'Allow', event.methodArn, { msg: 'success' });
    }
    console.log(policy);
    return policy;
  } catch (error) {
    console.error(error);
    throw error;
  }
};

今回はAuth0がまとめてくれているサンプルコードを参考にしています。また、Lambda Authorizerから返却するポリシーに関しては公式ドキュメントを参考にしてます。

実装がもろもろ終わったら再度デプロイします。 API Gatewayで設定がされていることを確認します。

動かしてみる

まずは、先ほどの取得したアクセストークンを、Authorizationヘッダーにセットして、API Gatewayへリクエストを行います。

$ curl https://<your_apigw_url>/Dev/hello -H "Authorization: Bearer <your_token>"
{"message":"hello world"}

認可処理が行われ、後段のLambdaからのレスポンスが返却されていることが、確認できます。

また試しに、デタラメなTokenでリクエストすると以下の様な403 Forbiddenメッセージが返却されます。

$ curl https://<your_apigw_url>/Dev/hello -H "Authorization: Bearer InvalidToken"
{"Message":"User is not authorized to access this resource with an explicit deny"}

Tokenの検証に失敗し、アクセスが拒否されているのがわかります。

まとめ

いかがだったでしょうか。

今回Auth0を始めて使ったのですが、ダッシュボードでの操作がいろいろ便利で、とても分かりやすかったです。

以上、どなたかの役に立てば幸いです。お疲れ様でした!