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