この記事は公開されてから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を始めて使ったのですが、ダッシュボードでの操作がいろいろ便利で、とても分かりやすかったです。
以上、どなたかの役に立てば幸いです。お疲れ様でした!