M1 MacでAWS Lambda と Auth0 を使用したカスタム認証
AWS Lambda と Auth0 を使用した APIGatewayのカスタム認証を実装しました。
全体のソースコードはgithubに載せています。
M1チップのMacで作業するため、Arm64で動かせる形で作成しました。
カスタム認証について以下の記事を参考にさせていただきました。
API Gateway + Web Socket で Auth0 の認証をするカスタムオーソライザーをつくってみた
記事の概要
- M1 MacでServerless Framework を使って AWS Lambda をデプロイする
- API GatewayにAuth0のJWT(JSON Web Token)を使用したカスタム認証を設定する
Auth0 の概要
Auth0
はJWT(JSON Web Token)を使用した認証をサポートしたクラウドベースの認証プラットフォームです。
事前準備
準備として、以下のものが必要です。
- Node.js: Serverless Framework を使用するには、Node.js が必要です。
- AWS アカウント: Serverless Framework を使用するには、AWS のアカウントが必要です。
- AWS アクセスキーとシークレットアクセスキー: Serverless Framework から AWS のサービスを呼び出すには、アクセスキーとシークレットアクセスキーが必要です。
- IAMでロールを作成し、アクセスキーとシークレットアクセスキーを控えます。
AWS CLI を設定する環境変数 - コンソール上で以下のコマンドを実行することで環境変数の設定が可能です。
- IAMでロールを作成し、アクセスキーとシークレットアクセスキーを控えます。
export AWS_ACCESS_KEY_ID=xxxxx
export AWS_SECRET_ACCESS_KEY=xxxxx
export AWS_DEFAULT_REGION=ap-northeast-1
-
Auth0のアカウント: 認証にAuth0を使用します
-
Serverless Framework のインストール
- Serverless Framework を使用するには、まず、Node.js をインストールし、その上で、以下のコマンドを実行して、Serverless Framework をグローバルインストールします。
npm install -g serverless
プロジェクトの作成
Serverless Framework では、プロジェクトを作成するとともに、必要なリソースを自動で構築することができます。以下のコマンドを実行することで、新しいプロジェクトを作成できます。
sls create --template aws-python3 --path my-project
このコマンドを実行すると、my-project という名前のディレクトリが作成され、その中に、サンプルの AWS Lambda 関数のファイルhandler.pyや、serverlessの設定ファイルserverless.ymlなどが配置されます。
Lambda 関数のコード
Auth0 の JWT トークンを検証する関数や、API Gateway のポリシーを生成する関数、テーブルのデータを操作する関数を作成します。
Dockerで動かすため、handler/auth,handler/test等のフォルダに分けて作成します。
handler/auth/auth.py
import json
import os
import jwt
import requests
from cryptography.hazmat.primitives import serialization
from jwt.algorithms import RSAAlgorithm
AUTH0_AUDIENCE = os.getenv('AUTH0_AUDIENCE')
AUTH0_JWKS_URL = os.getenv('AUTH0_JWKS_URL')
jwks_json_str = json.dumps(json.loads(requests.get(AUTH0_JWKS_URL).text)['keys'][0])
public_key = RSAAlgorithm.from_jwk(jwks_json_str)
pem = public_key.public_bytes(encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo)
def auth_handler(event, context):
print(event)
auth_token = event["headers"]["Authorization"].replace("Bearer ", "")
try:
principal_id = jwt_verify(auth_token, pem)
policy = generate_policy(principal_id, 'Allow', "arn:aws:execute-api:*:*:*/*/*/*")
return policy
except Exception as e:
raise Exception('Unauthorized')
def jwt_verify(auth_token, pub_key):
payload = jwt.decode(auth_token, pub_key, algorithms=['RS256'], audience=AUTH0_AUDIENCE)
return payload['sub']
def generate_policy(principal_id, effect, resource):
return {
'principalId': principal_id,
'policyDocument': {
'Version': '2012-10-17',
'Statement': [
{
"Action": "execute-api:Invoke",
"Effect": effect,
"Resource": resource
}
]
}
}
- リクエストから、Authorization ヘッダーを取得し、Bearer スキームを除いた JWT トークンを取得します。
- JWKS URL を使用して、Auth0 の公開鍵を取得します。
- 取得した公開鍵を使用して、JWT トークンを検証します。
- JWT トークンの検証が成功すると、トークンのペイロードから sub フィールドの値を取得し(ユーザ識別子)、これを使用して、Amazon API Gateway のカスタムオーソライザー用のポリシーを生成します。
generate_policyに渡されるresourceにはアクションを許可・拒否する対象のリソースを指定することができます。
generate_policy関数で返されるpolicyDocumentのStatementオブジェクトは、Action オプションで API Gateway で実行可能なアクションを、Effect オプションでそのアクションを許可するか拒否するかを、Resource オプションでどのリソースに対して適用するかを指定しています。
- /handler/auth/Dockerfile
FROM public.ecr.aws/lambda/python:3.8-arm64
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY *.py ./
- /handler/auth/requirements.txt
requests
pyjwt
cryptography
Arm64でビルドするためには、FROM public.ecr.aws/lambda/python:3.8-arm64
の記載が必要です。
handler/test/test.py
テーブルのデータを作成・更新・削除・読み取りをする関数です。
テーブルに登録する、データを取得する際に使用するuser_idを認証情報から取得しています。
Dockerで動かすため、Dockerfile等の作成が必要です。
こちらを参考にしてください
import json
import boto3
import os
from decimal import Decimal
from boto3.dynamodb.conditions import Key, Attr ##Keyオブジェクトを利用できるようにする
#Dynamodbアクセスのためのオブジェクト取得
dynamodb = boto3.resource(
"dynamodb",
region_name="ap-northeast-1",
endpoint_url=os.environ.get("DYNAMODB_ENDPOINT_URL", "https://dynamodb.ap-northeast-1.amazonaws.com"),
)
table = dynamodb.Table("test_table") ##指定テーブルのアクセスオブジェクト取得
LPI_COMMON_HEADER = {
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "OPTIONS,POST,GET,DELETE",
}
def test_handler(event, context):
# TODO implement
try:
print(event)
user_id = event["requestContext"]["authorizer"]["principalId"]
if event["body"] :
body = json.loads(event["body"])
if event['httpMethod']=='GET' :
# scanData = table.scan()
response = table.query(
KeyConditionExpression=Key('user_id').eq(user_id)
)
items=response['Items']
print(items)
return {
'statusCode': 200,
'body': json.dumps(items ,default=decimal_to_int),
"headers": LPI_COMMON_HEADER,
}
if event['httpMethod']=='POST' :
putResponse = table.put_item( ##put_item()メソッドで追加・更新レコードを設定
Item={ ##追加・更新対象レコードのカラムリストを設定
'user_id': user_id,
'memo': body["memo"],
'date': body["date"],
'star': body["star"],
}
)
return {
'statusCode': 200,
'body': json.dumps('Hello from PostLambda!'),
"headers": LPI_COMMON_HEADER,
}
if event["httpMethod"] == "DELETE":
delResponse = table.delete_item(
Key={
'user_id': user_id,
'date': body["date"],
}
)
return {
'body': json.dumps('Hello from Lambda!'),
'statusCode': 200,
"headers": LPI_COMMON_HEADER,
}
except Exception as e:
return {
'statusCode': 500,
'body': json.dumps(str(e)),
"headers": LPI_COMMON_HEADER,
}
return {
'statusCode': 404,
'body': json.dumps('function not found!'),
"headers": LPI_COMMON_HEADER,
}
def decimal_to_int(obj):
if isinstance(obj, Decimal):
return int(obj)
serverless.yaml ファイルの設定
service: test
useDotenv: true
frameworkVersion: "3.26.0"
provider:
name: aws
architecture: arm64 # デフォルトはx86_64
lambdaHashingVersion: 20201221
# you can overwrite defaults here
# stage: dev
region: ap-northeast-1
iam:
role:
statements:
- Effect: "Allow"
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
- dynamodb:Query
Resource: "arn:aws:dynamodb:*:*:table/test_table"
environment:
AUTH0_AUDIENCE: ${env:AUTH0_AUDIENCE}
AUTH0_JWKS_URL: ${env:AUTH0_JWKS_URL}
ecr:
images:
auth:
file: Dockerfile
path: handler/auth
platform: linux/arm64
test:
file: Dockerfile
path: handler/test
platform: linux/arm64
resources:
Resources:
test:
Type: AWS::DynamoDB::Table
Properties:
TableName: test_table
AttributeDefinitions:
- AttributeName: user_id
AttributeType: S
- AttributeName: date
AttributeType: N
KeySchema: [
{
KeyType: "HASH",
AttributeName: "user_id",
},
{
KeyType: "RANGE",
AttributeName: "date",
}]
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
functions:
auth:
image:
name: auth
command: auth.auth_handler
test:
image:
name: test
command: test.test_handler
events:
- http:
path: /test
method: get
integration: lambda-proxy
cors: true
authorizer:
name: auth
identitySource: method.request.header.Authorization
type: request
- http:
path: /test
method: post
integration: lambda-proxy
cors: true
authorizer:
name: auth
identitySource: method.request.header.Authorization
type: request
- http:
path: /test
method: delete
integration: lambda-proxy
cors: true
authorizer:
name: auth
identitySource: method.request.header.Authorization
type: request
- 環境変数を使用するため、useDotenv: trueの指定、environment:のフィールドで変数の設定をしています。
- ecr:Docker イメージを AWS Elastic Container Registry (ECR) にプッシュするための設定を行います。
- functions:ではLambda関数の設定しており、ecr:で設定したimageのnameを指定し、commandには実行する関数を設定します。
- events:のフィールドでは、httpリクエストのメソッド毎にfunctions:で設定したauthの関数がカスタムオーソライザーとして呼び出される指定をしています。
- Authorization ヘッダーで検証を行うため、
identitySource: method.request.header.Authorization
を指定しています。
- Authorization ヘッダーで検証を行うため、
slsのデプロイ
Auth0の認証情報を確認する際に環境変数を使用しているため、.envファイルを作成します。
Auth0の設定を行う必要がありますが、Auth0の設定時にAPIのエンドポイントのURLを使用したいため、serverless.yamlと同じ階層にひとまず定義だけ記述したファイルを作成しslsのデプロイを行います。
.env
AUTH0_AUDIENCE=
AUTH0_JWKS_URL=
sls deploy
デプロイ時にターミナルに表示されるエンドポイントを確認します。
Auth0の設定
APIの作成
- Auth0 のアカウントを作成するか、ログインします。
- Auth0 管理画面の [Applications]>[APIs] メニューから、[Create API] ボタンをクリックします。
- API 名を入力し、[Identifier] フィールドに API のエンドポイントのURLを設定します。
デプロイした際に表示されたAPIのEndpointを設定します。 - [Create] ボタンをクリックして、API を作成します。
作成するとApplicationsにAPIのテスト用のM2Mアプリケーションが追加されます。
Auth0関連の環境変数を設定
- AUTH0_AUDIENCE: APIの作成時に設定したIdentifierの値を設定します
- デプロイされたAPI Gatewayのエンドポイント
- AUTH0_JWKS_URL: テストアプリケーションの[Settings]>[Advanced Settings]>[Endpoints]のJSON Web Key Setに記載のURLです。
基本的には以下のようなURLとなっています。
https://{Auth0のアプリケーションのドメイン名}/.well-known/jwks.json
設定し終えたら、再度sls deploy
で反映します。
カスタム認証ができているか確認する
curlコマンドで、認証情報をヘッダーにつけずにAPIのエンドポイントにリクエストすると、未認証となることが確認できます。
cd my-project
curl 'APIGatewayのエンドポイント' -XGET
{"message":"Unauthorized"}
ヘッダーに認証済みTokenを付与することで認証が通り関数が動くことが確認できます。
Auth0ではTokenを取得できるテスト用のcurlコマンド等が用意されています。
Auth0の[Applications]>[APIs]で作成したAPIをクリックし、[Test]のタブを表示することで確認することができます。
TEST用のTokenを環境変数に入れることで確認しやすくすることができます。
以下ではTest用のToken取得用のcurlコマンドの末尾にjqコマンドを使ってレスポンスからaccess_tokenのみを取得し、環境変数TOKENに格納しています。
jqがインストールされていない場合は、Brewでインストールすることが必要です。
cd my-project
export TOKEN=$(curl -s --request POST --url 'Token取得用のURL' --header 'content-type: application/json' --data '{"client_id":"テスト用M2MアプリケーションのClient ID" ,"audience":"APIのaudience(APIGatewayのエンドポイント)" ,"grant_type":"client_credentials"}'|jq .access_token -r)
[Applications]>[APIs]>作成したAPI>[Test]のタブからcurlコマンドをコピーし、末尾に|jq .access_token -r)
を接続するとスムーズです。
cd my-project
curl 'APIGatewayのエンドポイント' -XGET -H "Authorization : $TOKEN" -H "Content-Type: application/json"
参考
API Gateway + Web Socket で Auth0 の認証をするカスタムオーソライザーをつくってみた
Discussion