概要

①最近受験した資格試験(DVA)で学習した認証・認可の仕組みに関してより理解を深めること
②最近よく触れているSAM(Serverless Application Model)の知識を活かして何か自分なりに考え、アウトプットすることを目的に、
SAMを用いて、API Gateway LambdaAuthorizerを設定する手順をまとめました。

そもそも認証・認可とは

用語こそ似ていますが、意味は異なります。
・認証(Authentication: AuthN)・・・個人を一意に特定すること
★「その人が誰であるかを証明する証明書のようなもの」
例)ユーザーID、PW →その人が誰であるかが一意に決まる

・認可(Authorization: AuthZ)・・・個人に権限を付与すること
★「所持していることを確認しアクセスを許可するもの」 
例)鍵→現実世界においては、どこか(家・施設など)に入室する権限を付与するもの
  インターネットの世界においてはサイトへアクセスする権限を付与するもの

上記2点のイメージを図にまとめました。
※認証と認可を同時に行うケースもあると思いますが、厳密に区別するとするならば、という観点で整理をしています。

※HTTPステータスコード400番台(クライアントエラーレスポンス)の中に401「UnAuthorized」403「Forbidden」がありますが、401は認証の失敗、403は認可の失敗を表します。

具体的な設定方法

認証、認可について整理ができたところで本題に入ります。
「Lambda Authorizer」、というタイトルにもあるように、API Gateway Lambda Authorizerは認可を行うためのAPI Gatewayの機能です。
※他にも、認可処理を行うことができるAWSサービスとして、 Cognitoがあります。(本記事ではこちらには触れません。こちらの記事参照)

今回はSAMを使って、簡単な認可の機能を盛り込んだAPIを作成したいと思います。

# 前提・試した環境
・ AWS CLI、SAM CLIがインストール済み、(実施時のバージョンはそれぞれ2.13.9,.1.99.0 )
・aws configureコマンドで認証情報設定済み
・ランタイム:Python3.11(→2023年7月よりサポートされるようになったそうです)
・環境:Windows10

手順は下記の流れになります。ポイントは②の部分なので、②をより重点的に説明します。
① sam init
→ダイアログが出てくるので回答していきます。今回は下記のように、AWS側で用意されたテンプレートを修正して認可の仕組みを作っていく方針にします。

> Which Template source would you like to use?
> →1 - AWS Quick Start Templates
> 
> Choose an AWS Quick Start application template
> →1 - Hello World Example
>  ・・・・中略
 What package type would you like to use? 
 → 1  - Zip
 Project name : sam-auth-test

②コードの編集
・作成されたHello World Application のディレクトリ構成は下記になっています。

・まずはtemplate.yamlを修正していきます。。Authorizersプロパティを追加するのがポイントです。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-auth-test

  Sample SAM Template for sam-auth-test

Globals:
  Function:
    Timeout: 30

Resources:
  MyApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: dev
      MethodSettings:
      - HttpMethod: get
      Auth:
        DefaultAuthorizer: MyLambadaAuthorizer
        Authorizers:
          MyLambadaAuthorizer:
            FunctionArn: !GetAtt AuthorizerFunction.Arn
            Identity:
              ReauthorizeEvery: 0

  AuthorizerFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: authorizer.lambda_handler
      Runtime: python3.11
      Timeout: 30

  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.11
      Architectures: 
         - x86_64
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get
            RestApiId: !Ref MyApi
Outputs:
  HelloWorldApi:
    Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.http://amazonaws.com/dev/hello/"

・続いてhello_world配下にあるapp.pyを確認します。処理自体には手を加えませんが、eventの中身を出力するよう修正を加えます。(CloudWatchロググループでの確認のため)

# app.py
import json
from logging import getLogger, INFO
...

logger = getLogger(__name__)
logger.setlevel(INFO)

def lambda_handler(event, context):
    ...
    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "hello world",
            ...
        }),
    }

・hello_worldの階層に、新たにauthorizer.pyを作成します。(名前は任意)

# authorizer.py
import json
from logging import getLogger, INFO
...

def lambda_handler(event, context):
    print("============ event の出力 ============")
    logger.info(json.dumps(event))
   ...
   # リクエストからトークンを取得
   token = event['authorizationToken']
   effect = 'Deny'

   # トークンが一致していたら許可(デフォルトは拒否)
   #  戻り値にはAPIを実行するIAMポリシーをJSON形式で渡してあげる
   if token == 'abc':
        effect = 'Allow'
    return {
        'principalId': '*',
        'policyDocument': {
            'Version': '2012-10-17',
            'Statement': [
                {
                    'Action': 'execute-api:Invoke',
                    'Effect': effect,
                    'Resource': event['methodArn']
                }
            ]
        }
    }

③sam build・・・デプロイするための準備を行います。
(厳密にいうと→アプリケーションのコードと依存関係をパッケージ化)

④sam deploy
デプロイ後、マネジメントコンソールでAPI Gateway>オーソライザーを確認すると、
MyLambdaAuthorizerというオーソライザーが設定されていることが確認できました。

⑤API疎通確認
Postmanにて、Authorization Headerをつけた時とつけない時、誤ったHeaderをセットした時でレスポンスに違いがあることが確認できます。(下記参照)
【Authorization Headerをセットした時】
レスポンスとして帰ってくるのは「hello world」

【Authorization Headerが未セットの時】
レスポンスとして帰ってくるのは「401 UnAuthorized」

【誤ったAuthorization Headerをセットした時】
レスポンスとして帰ってくるのは「403 Forbidden」

※CloudWatchでeventの中身を確認すると下記のようになっています。(認可成功時)

{
    "type": "TOKEN",
    "methodArn": "arn-aws:execute-api:ap-northeast-1:XXXXXX:(api-id)/(STAGE)/(METHOD)",
    "authorizationToken": "abc"
}

event,contextについて理解する

上記に示した例で、認可の仕組みはある程度理解できたのですが、「もしかしたら、これcontext使っていない??」と素朴に思いました。contextについて理解がしっかりできていないと思い、Lambdaの構造を再度理解するためにも、handlerの引数である「event」「context」について整理してみたいと思います。

【event,contextについての情報整理】
★event→Lambda関数が呼び出された時に受け取る入力データ
★context→Lambda関数自体に関する情報(関数名、実行されているリージョン、バージョン、タイムアウトの時間など)

context.(キー名)

という形でcontext内のデータにアクセスすることができる(詳細はPython の AWS Lambda context オブジェクト参照)
例)context.function_version(→関数のバージョン), context.function_name(→関数名)

contextでデータを他の関数に渡す

contextは、関数内で、カスタムのプロパティを含めることもできるようです。
つまり、Authorizerで認可を行った上で、そのユーザの情報をcontextで後続のLambda関数(今回の例で言うところのHelloworldFunction)に渡すこともできます。

既出の例で出てきたコードを少しいじって、AuthorizerFunctionからHelloWorldFunctionにcontextを渡すことができることを確認したいと思います。
・authorizer.pyとapp.pyを下記のように修正

# authorizer.py
import json
from logging import getLogger, INFO
import base64
...

def lambda_handler(event, context):
    print("============ event の出力 ============")
    logger.info(json.dumps(event))

    token = event['authorizationToken']
    effect = 'Deny'
   #contextを追加
    context = {}

    if token == 'abc':
        effect = 'Allow'
        context = {
            "id": "hoge",
            "name": "hogehoge"
        }
    json_context = json.dumps(context)
    base64_context = base64.b64encode(json_context.encode('utf-8'))
    return {
        'principalId': '*',
        'policyDocument': {
            'Version': '2012-10-17',
            'Statement': [
                {
                    'Action': 'execute-api:Invoke',
                    'Effect': effect,
                    'Resource': event['methodArn']
                }
            ]
        },
        'context': {
            'data_to_transfer': base64_context
        }
    }
# app.py
import json
from logging import getLogger, INFO
import base64
...

def lambda_handler(event, context):
    print("============ event の出力 ============")
    logger.info(json.dumps(event))

   # data_to_transfer のデータをデコード
    data_to_transfer = event["requestContext"]["authorizer"]["data_to_transfer"]
    decoded_data = base64.b64decode(data_to_transfer).decode('utf-8')
    return {
        'statusCode': 200,
        'body': json.dumps({
            'message': 'hello world',
            'context': decoded_data
        }),
    }

CloudWatchでログを確認すると、HelloworldFunctionのeventは下記のような辞書形式で返却されています。(長いので必要な部分のみを切り取っています。)

event = { 
    "resource" : "/hello",
    "path": "/hello",
    ...(中略)
    "requestContext": 
        "authorizer": {
            "principalId": "*",
            "integrationLatency": 250,
            "data_to_transfer": "eyJwYXJlbnQxIjogInZhbDEiLCAicGFyZW50MiI6ICJ2YWwyIn0="
    },
    ...(中略),
    "body": "null",
    "isBase64Encoded": false
}

上記の結果を踏まえると、contextの値は、
event[‘requestContext’][‘authorizer’]で取り出すことができるようになっています。

今回はauthorizer.pyで、

{
    "id": "hoge",
    "name": "hogehoge"
}

というデータをエンコードしてHelloworldFunctionに渡す→それをapp.pyで取り出しデコードしレスポンスとして返す、ということを行なっています。
この時のレスポンスですが、写真のようにcontextの中身がレスポンスとして取得できていることが分かります。

【図で整理】
自身の理解のためにも、今回設定したLambda Authorizerの仕組みについて、図で簡単に整理しました。

終わりに

「認可のキャッシュ」「Cognito、外部認証基盤を使った認証・認可」「TokenをDBに保存する場合の認証・認可はどうなるか」など、
まだまだ認証、認可について個人的に気になることは沢山あったのですが、今回は基礎的な部分の理解にフォーカスをしたかったので、この辺りに留めておきます。
また、認証・認可を理解する上で、
第114回 雲勉 セキュリティの『わからない』が『ちょっと分かる』に変わる60分(46分11秒あたり)が非常に分かりやすかったのでこちらも掲載しておきます。

参考文献

https://dev.classmethod.jp/articles/authentication-and-authorization/
https://qiita.com/sugimount-a/items/0079a79b94e442204d6f
https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-property-api-lambdatokenauthorizer.html
https://developer.mozilla.org/ja/docs/Web/HTTP/Status/403
https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-property-api-apiauth.html#sam-api-apiauth-authorizers
https://qiita.com/GiantPanda/items/df0737540c5140d490cd
https://qiita.com/satour/items/7412bb19095e10a734a1