
Auth0 + API Gateway でM2M認証・認可をやってみた
この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
おつかれさまです。サーバーレス開発部の新井です。
今回は、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を始めて使ったのですが、ダッシュボードでの操作がいろいろ便利で、とても分かりやすかったです。
以上、どなたかの役に立てば幸いです。お疲れ様でした!













