こんにちは。エンジニアの @Taku です。
期間が空いてしまいましたが、過去にLambda オーソライザーを使った認証を作成した続きで、今回は認可の設定を行っていきたいと思います。
※本記事の内容はプロダクトと関係なく、個人的にやってみたものになります。
前回までにやったこと
前回までで、
①API GatewayとLambdaオーソライザーを利用した認証の作成 kaminashi-developer.hatenablog.jp
をやった後、
② ①の認証をAWS Samを用いて構築 kaminashi-developer.hatenablog.jp をやってみました。
よろしければ上記もご参照いただけますと幸いです。
今回やること
今回は認可の設定として、ログイン後のリソースのアクセス制御を行っていきます。
認可の設計パターンにつきましては、以下記事を参考にさせていただきました。 please-sleep.cou929.nu
これまでの実装のパターンとしては、上記記事でのJWT 利用 + API Gateway パターンが近いかと思いまして、当てはめると - TokenStore = Firebase Auth - AuthService = Lambda Authorizer関数 - Service X = SAMで定義したLambda関数 になるかと思います。
認可の設計パターンについてはいくつか考えられますが例として、
- サービス側で権限を制御するパターン
- 中央管理するパターン
があり、これがベストという方式はないようなのですが、今回はAPI GatewayとLambdaオーソライザーを利用するため、2の中央管理するパターンで実装していきます。
認可の設定
アクセス制御のパターンは、以下3ユーザーで
- 管理者(admin) :全リソースにアクセスできる
- リーダー(leader):リーダー及び、他のユーザーのリソースにアクセスできる
- ユーザー(user) :自身の情報にのみアクセスできる
といった形で制御したいと思います。
API GatewayとLambda オーソライザーを利用した認可の方法
以後、過去からの続きで恐縮ですが、サンプルを元にGolangでLambdaオーソライザーの関数を作成した場合、 IAM policyを作成する箇所でユーザー毎にアクセスできるリソース(パス)を制御することができます。
具体的には events.APIGatewayCustomAuthorizerResponse
を作成している箇所で、
events.APIGatewayCustomAuthorizerPolicy
の Resource
で、ポリシーを付与されたユーザーはここに記載されているパスにのみアクセスすることができます。
// generatePolicy IAM policyを生成する func generatePolicy(principalId, effect string, resources []string) events.APIGatewayCustomAuthorizerResponse { authResponse := events.APIGatewayCustomAuthorizerResponse{PrincipalID: principalId} if effect != "" && len(resources) > 0 { authResponse.PolicyDocument = events.APIGatewayCustomAuthorizerPolicy{ Version: "2012-10-17", Statement: []events.IAMPolicyStatement{ { Action: []string{"execute-api:Invoke"}, Effect: effect, Resource: resources, // アクセスOKなパス }, }, } } return authResponse }
ですので、 generatePolicy
を利用してポリシーを作成する際、各権限別にアクセス可能なパスを設定することでアクセス可能なリソースを制御することができます。
実装
続いて具体的な設定です。
API Gatewayの設定
本題から外れるため詳細は省く&設定も極小にしておりますが、前回のSAMのtemplate.yamlを修正し、
- GET: /admin (管理者のページ)
- GET: /leaders (リーダーのページ)
- GET: /users/XXXXXXXX (一般ユーザー個人のページ)
というリソースを追加しました。
前回のAWA SAMを使った構築で作成した
template.yaml
を以下のように修正します。
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: Sample API Gateway Lambda Authorizer by Golang. Resources: SampleRestApi: Type: AWS::Serverless::Api Properties: StageName: dev Description: "This is Sample Rest API" Cors: AllowMethods: "'GET, OPTIONS'" AllowHeaders: "'*'" AllowOrigin: "'*'" Auth: DefaultAuthorizer: MyLambdaAuthorizerFunction Authorizers: MyLambdaAuthorizerFunction: FunctionArn: !GetAtt LambdaAuthorizerFunction.Arn RootFunction: Type: AWS::Serverless::Function Properties: Events: ApiEvent: Type: Api Properties: Path: / Method: get RestApiId: Ref: SampleRestApi Runtime: python3.7 Handler: index.handler InlineCode: | def handler(event, context): return { 'statusCode': 200, } # 追加ここから AdminFunction: Type: AWS::Serverless::Function Properties: Events: ApiEvent: Type: Api Properties: Path: /admin Method: get RestApiId: Ref: SampleRestApi Runtime: python3.7 Handler: index.handler InlineCode: | import json def handler(event, context): return { 'statusCode': 200, 'body': json.dumps({ 'message': 'admin情報', }), } LeaderFunction: Type: AWS::Serverless::Function Properties: Events: ApiEvent: Type: Api Properties: Path: /leaders Method: get RestApiId: Ref: SampleRestApi Runtime: python3.7 Handler: index.handler InlineCode: | import json def handler(event, context): return { 'statusCode': 200, 'body': json.dumps({ 'message': 'leader情報', }), } UserFunction: Type: AWS::Serverless::Function Properties: Events: ApiEvent: Type: Api Properties: Path: /users/XXXXXXXX # userのuid Method: get RestApiId: Ref: SampleRestApi Runtime: python3.7 Handler: index.handler InlineCode: | import json def handler(event, context): return { 'statusCode': 200, 'body': json.dumps({ 'message': 'user情報', }), } # 追加ここまで LambdaAuthorizerFunction: Type: AWS::Serverless::Function Properties: FunctionName: samAuth CodeUri: ./lambda_auth # buildされるが、configがアップされなかったので、`/build/関数名`のディレクトリに手動で配置 Handler: app.lambda_handler Runtime: go1.x Outputs: SampleRestApi: Description: "Sample API Gateway Lambda Authorizer by Golang." Value: !Sub "https://${SampleRestApi}.execute-api.${AWS::Region}.amazonaws.com/dev/"
Lambdaオーソライザー関数の設定
前々回のLambda fanctionを修正し、generetePolicyを呼んでいた箇所でリソースの設定を行います。
以前はgeneretePolicy
の第3引数にevent.MethodArn
を入れており、Lambdaオーソライザー関数を呼んだ際のパスが入るようになっていたため認証後はそのままアクセスできていたのですが、今回はユーザー単位でアクセスできるパスを設定します。
apiArn := "arn:aws:execute-api:ap-northeast-1:XXXXXXXXXXXX:XXXXXXXXXX/dev" // API Gatewayのパス switch email { case "admin@example.com": return generatePolicy("admin", "Allow", []string{apiArn + "/GET/*"}), nil // 全リソースにアクセス可能 case "leader@example.com": return generatePolicy("leader", "Allow", []string{apiArn + "/GET/leaders", apiArn + "/GET/leaders*", apiArn + "/GET/users*"}), nil // leaderとuserの全リソースにアクセス可能 case "user@example.com": userID := token.UID return generatePolicy("user", "Allow", []string{apiArn + "/GET/users/" + userID}), nil // 自分のリソースにのみアクセス可能 case "deny@example.com": return generatePolicy("deny", "Deny", []string{}), nil case "unauthorized": return events.APIGatewayCustomAuthorizerResponse{}, errors.New("Unauthorized") // Return a 401 Unauthorized response default: return events.APIGatewayCustomAuthorizerResponse{}, errors.New("Error: Invalid token") }
リソースに設定するパスは、コンソールでAPI Gatewayの設定を見ることで確認可能です。
generetePolicyの第3引数はSliceで、複数パスを入れることができ、 *
を入力して配下の全リソースにアクセス可能とすることもできます。
が、 leaders/*
のように /
を挟むと別リソースと判定されるようで、 leaders*
とする必要があったためパス設計には注意が必要そうでした。
今回はメールアドレスでの判定ですが、Firebaseでアカウントを作成する際に情報を組み込むようにすれば、それらを基に制御をすることも可能かと思います。 (コンソールからだとカスタマイズができなかったため割愛)
検証
FirebaseAuthで以下の3ユーザーを作成し、アクセスを試みます
検証にはこれまた雑ですが、ログイン後、各API Gatewayのパスにリクエストを投げ、どのユーザーでどのレスポンスが返ってくるかHTMLファイルを利用して確認します。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Lambda Authorizer Sample</title> </head> <body> <h1>...Please wait</h1> <div id="email"></div> <div id="token"></div> <br> <div></div> <a id="admin"></a> <br> <div></div> <a id="leader"></a> <br> <div></div> <a id="users"></a> <!-- FirebaseSDKのインポート --> <script src="https://www.gstatic.com/firebasejs/8.2.6/firebase-app.js"></script> <!-- FirebaseAuthのインポート --> <script src="https://www.gstatic.com/firebasejs/8.2.6/firebase-auth.js"></script> <!-- configファイルのインポート --> <script src="./js/config.js"></script> <!-- axiosのインポート --> <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js" integrity="sha512-bZS47S7sPOxkjU/4Bt0zrhEtWx0y0CRkhEp8IckzK+ltifIIE9EMIMTuT/mEzoIMewUINruDBIR/jJnbguonqQ==" crossorigin="anonymous"></script> <script> // User情報の表示 firebase.auth().onAuthStateChanged(function(user) { let h1 = document.querySelector('h1'); let email = document.querySelector('#email'); let token = document.querySelector('#token'); let admin = document.querySelector('#admin'); let leader = document.querySelector('#leader'); let users = document.querySelector('#users'); // API Gatewayで作成したエンドポイントを設定 // const url = "http://localhost:3000" const adminUrl = "https://x7ow8vkh09.execute-api.ap-northeast-1.amazonaws.com/dev/admin" const leaderUrl = "https://x7ow8vkh09.execute-api.ap-northeast-1.amazonaws.com/dev/leaders" const userUrl = "https://x7ow8vkh09.execute-api.ap-northeast-1.amazonaws.com/dev/users" if (user) { console.log('user', user) h1.innerText = 'ログインしました'; email.innerHTML = `メールアドレス:${user.email}`; user.getIdToken().then(function(accessToken) { console.log('accessToken', accessToken) token.innerHTML = `JWTトークン:${accessToken}`; // リクエスト送信 axios.get(adminUrl, { headers: { 'Authorization':accessToken, } }).then(res => { admin.innerHTML = `管理者: ${res.data.message}`; }).catch(error => { admin.innerHTML = `管理者: ${error}`; }); axios.get(leaderUrl, { headers: { 'Authorization':accessToken, } }).then(res => { leader.innerHTML = `リーダー: ${res.data.message}`; }).catch(error => { leader.innerHTML = `リーダー: ${error}`; }); axios.get(userUrl + "/" + "8eMzOlFSDcVJU8U4zjwNA9pyMlr1", { headers: { 'Authorization':accessToken, } }).then(res => { users.innerHTML = `ユーザー: ${res.data.message}`; }).catch(error => { users.innerHTML = `ユーザー: ${error}`; }); }, null, ' '); } }); </script> </body> </html>
各ユーザーでログインしたところ、以下の通りアクセス可能となっているリソースのみ取得できている形になってます。
admin@example.com
- 全情報が取得できている
- 全情報が取得できている
leader@example.com
- leader, user情報のみ取得できている
- leader, user情報のみ取得できている
user@example.com
- user情報のみ取得できている
- user情報のみ取得できている
終わりに
以上、簡単ではありますがAPI GatewayとLambdaを利用した認可設定のご紹介でした。 URLの設計が必要ですが、API GatewayとLambdaを利用することで、中央管理するパターンであれば簡単にロールベースのアクセス制御ができるように感じました。
少しでも参考になれば幸いです。