AWS PR

API Gateway+Lambdaのバックエンド(Golang)へNext.jsからAWSSigv4でAPI認証してみた(CORS対応も!)

aws-sigv4-api-gateway-lambda-cors
記事内に商品プロモーションを含む場合があります

Webフロントエンド側をNextjs(Typescript)で
バックエンド側をAPIGateway+Lambda(Golang)で作っていて
API認証にはAWSSigV4を使っているのですが、
フロントエンドからバックエンドのAPIの認証を行うときに
どハマりして1日潰したので、今後のためにポイントをメモしておきます。

API Gateway+LambdaのバックエンドへNext.jsからAWSSigv4でAPI認証してみた(CORS対応も!)

最終的にNextjs側のコードは以下のような感じに、

import axios from 'axios';
import { HttpRequest } from '@aws-sdk/protocol-http';
import { Sha256 } from '@aws-crypto/sha256-universal';
import { SignatureV4 } from '@smithy/signature-v4';
import { Credentials } from 'aws-sdk'

    async APICall() {
        const apiUrl = new URL('https://api-gateway-url/prod/api/v1/users');
        const credential = new Credentials('DUMMY-ACCESS-KEY', 'DUMMY-SECRET');
        const signatureV4 = new SignatureV4({
          service: 'execute-api',
          region: 'ap-northeast-1',
          credentials: credential,
          sha256: Sha256,
        });
        const httpRequest = new HttpRequest({
          headers: {
            'x-api-key': 'DUMMY-API-KEY',
            host: apiUrl.hostname,
          },
          hostname: apiUrl.hostname,
          method: 'GET',
          path: apiUrl.pathname,
        });
      
        const signedRequest = await signatureV4.sign(httpRequest);

        axios.get(apiUrl.toString(),{
            headers: signedRequest.headers,
            method: 'GET',
        })
        .then((response) => {
            console.log(JSON.stringify(response.data));
            if (response.status === 200) {
                return response.data;
            }
        })
        .catch((error) => {
            console.log(error)
        });
      };

エラーとしては
「The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method.」
な感じで、こちらで計算した署名情報と
AWS側で計算した署名情報が異なっている感じでした。
※これらが一致しないと認証は通らない

ハマりポイント1

これは想定していたんですが、CORS制限にひかかりました。
開発側のフロントエンドはlocalhostで
アクセス先はAWSなのでドメインが違います。
ドメインが違うのにアクセスして良いの?というセキュリティ機能が働くんですね〜。
補足CORS

以下の対応を実施
AWS CDKでバックエンドを作っているので、
それでCORS系の設定を有効にします。

    const WebApi = new RestApi(this, 'WebApi', {
      restApiName: 'Web Api',
      defaultCorsPreflightOptions: {
        allowOrigins: Cors.ALL_ORIGINS,
        allowMethods: Cors.ALL_METHODS,
        allowHeaders: ['Content-Type', 'X-Amz-Date', 'Authorization', 'X-Api-Key', 'X-Amz-Security-Token', 'X-Amz-Content-Sha256'],
        statusCode: 200,
      },
      defaultMethodOptions: { apiKeyRequired: true },
    });

開発なのでどこからアクセスきてもOK
(allowOrigins: Cors.ALL_ORIGINS)にしてますが、
本番運用する場合はちゃんと制限しましょう〜。
メソッドも全て許可ですが(allowMethods: Cors.ALL_METHODS)、
GET/POSTなど使うものに制限してくださいね〜。

ハマりポイント2

次、ここでどハマりしました!
分かってみれば、なぜこんなことをした!って感じですが
HTTP GETなのに’content-type’: ‘application/json’を入れてました。

        const httpRequest = new HttpRequest({
          headers: {
            'x-api-key': 'DUMMY-API-KEY',
       'content-type': 'application/json',
            host: apiUrl.hostname,
          },
          hostname: apiUrl.hostname,
          method: 'GET',
          path: apiUrl.pathname,
        });

このままaxiosでコールすると
Cotent-Typeがnullになり(AWS側はContent-Typeなしで計算)、
こちらで計算した署名には’application/json’が存在するので
不一致の判定になった模様・・・。
使うライブラリを色々変えたりしましたが、
まさかこんな所でミスしているとは・・・。
※Developer Toolでログは見ていたものの見逃しており、
 気づいた時はこれか!?とハッとしました笑

このヘッダを削除して解決!!
AWS公式も見たりもしてました

ハマりポイント3

API GatewayとLambdaのIntegration(統合)方法
次第で方法が変わりますが、バックエンドでも対応が必要です。
統合方法の補足

今回はプロキシ統合 (Proxy Integration)を選択しているため、
API Gatewayはレスポンスに関与しませんので、
Lambdaでレスポンスヘッダーを設定してやる必要がありました。
Lambdaは今回、Golangで作っているので、以下のような感じです。

type Response struct {
	StatusCode int               `json:"statusCode"`
	Body       string            `json:"body"`
	Headers    map[string]string `json:"headers"`
}

func main() {
	lambda.Start(Handler)
}

func Handler() (Response, error) {

	body := h.readData() //DBからデータを取得

	return Response{
		StatusCode: 200,
		Body:       string(body),
		Headers: map[string]string{
			"Access-Control-Allow-Origin": "*",
		},
	}, nil
}

補足

CORSについて

CORS(Cross-Origin Resource Sharing、クロスオリジンリソース共有)は、
ウェブページが異なるオリジン(ドメイン、スキーム、またはポート)のリソースに
アクセスする際のセキュリティ制限を緩和するためのメカニズムです。
CORSは、ウェブブラウザの「同一オリジンポリシー」に基づいており、
これはウェブページが異なるオリジンのリソースにアクセスすることを制限するセキュリティ機能です。

同一オリジンポリシー

同一オリジンポリシーは、
ウェブセキュリティの基本的な概念で、
ウェブページが異なるオリジン
(異なるドメイン、プロトコル、ポート)のリソースに
アクセスすることを制限します。

このポリシーにより、
悪意のあるスクリプトが異なるオリジンのデータにアクセスし、
それを操作することが防がれます。

CORSの動作

CORSは、ウェブサーバーが特定のHTTPヘッダーを使用して、
異なるオリジンからのリクエストを許可するかどうかをブラウザに指示することで機能します。
これにより、サーバーは安全に異なるオリジンからのリクエストを受け入れることができます。

CORSの実装

CORSの実装は、以下のステップで行われます:

  1. プリフライトリクエスト
    ブラウザは、特定の条件(例えば、カスタムヘッダーを使用する、
    または特定のHTTPメソッドを使用する)の下で、
    実際のリクエストを送信する前に「プリフライトリクエスト」を送信します。
    これは、OPTIONS メソッドを使用して行われ、
    サーバーに対して、実際のリクエストが安全かどうかを確認します。
  2. CORSヘッダー
    サーバーは、プリフライトリクエストに対して、
    CORSポリシーを定義するHTTPヘッダー
    (例:Access-Control-Allow-Origin)を含むレスポンスを返します。
    これにより、ブラウザはリクエストが許可されているかどうかを判断します。
  3. 実際のリクエスト
    プリフライトリクエストが成功し、
    CORSポリシーに基づいてリクエストが許可されると、
    ブラウザは実際のリクエストを送信します。

CORSヘッダーの例

  • Access-Control-Allow-Origin
    このヘッダーは、
    指定されたオリジンからのリクエストを許可します。
    特定のオリジン、または *(すべてのオリジン)を指定できます。
  • Access-Control-Allow-Methods
    どのHTTPメソッドが許可されているかを指定します。
  • Access-Control-Allow-Headers
    リクエストで許可されているHTTPヘッダーを指定します。

CORSは、ウェブアプリケーションのセキュリティを維持しつつ、
異なるオリジン間でのリソース共有を可能にする重要なメカニズムです。

API Gateway & Lambda Integration

AWS API Gatewayでの「非プロキシ統合」と「プロキシ統合」は、
API Gatewayがバックエンド(例えばLambda関数やHTTPエンドポイント)
とどのように通信するかに関する異なるアプローチを指します。

非プロキシ統合 (Non-Proxy Integration)

非プロキシ統合では、
API Gatewayはバックエンドとの間で
リクエストとレスポンスのマッピングを管理します。
これにより、リクエストとレスポンスの形式を細かく制御できます。

  • リクエストマッピング
    API Gatewayは、
    入力されたリクエストをバックエンドが理解できる形式に変換するための
    マッピングテンプレートを使用します。
  • レスポンスマッピング
    バックエンドからのレスポンスをクライアントに返す前に、
    API Gatewayはレスポンスマッピングテンプレートを使用してレスポンスの形式を変更できます。
  • 細かい制御
    非プロキシ統合を使用すると、
    リクエストとレスポンスの両方で
    カスタムマッピングを行うことができるため、細かい制御が可能です。

プロキシ統合 (Proxy Integration)

プロキシ統合では、
API Gatewayはリクエストとレスポンスを
そのままバックエンドに転送します。
これにより、バックエンドが
リクエストの処理とレスポンスの生成を完全に制御できます。

  • リクエストの透過的な転送
    クライアントからのリクエストは、
    API Gatewayによって変更されずにそのままバックエンドに転送されます。
  • レスポンスの透過的な転送
    バックエンドからのレスポンスは、
    API Gatewayによって変更されずにそのままクライアントに返されます。
  • 簡素化された設定
    プロキシ統合は設定が簡単で、
    リクエストとレスポンスのマッピングについて心配する必要がありません。

選択の基準

  • プロキシ統合は、
    バックエンドで完全な制御を行いたい場合や、
    設定を簡素化したい場合に適しています。
    特に、Lambda関数をバックエンドとして使用する場合に一般的です。
  • 非プロキシ統合は、
    API Gatewayでリクエストやレスポンスを細かく制御したい場合に適しています。
    これは、特定のバックエンド要件がある場合や、
    レガシーシステムとの統合が必要な場合に有用です。

どちらの統合タイプも、特定のユースケースや要件に基づいて選択することが重要です。

まとめ

  1. 開発環境の概要
    このブログでは、WebフロントエンドをNext.js (Typescript) と、
    バックエンドをAWSのAPI GatewayとLambda (Golang) で、
    AWSのSignature Version 4 (AWSSigV4) を使用したAPI認証のプロセスに焦点を当てています。
  2. 主要な課題とハマりポイント
    • CORSの問題
      ローカルホストからAWSの異なるドメインへのアクセスにおける
      CORS制限の問題に直面し、AWS CDKを使用してCORS設定を行うことで解決しました。
    • ヘッダーの不一致
      HTTP GETリクエストで'content-type': 'application/json'を誤って含めたことで、
      署名の不一致が発生しました。このヘッダーを削除することで問題を解決しました。
    • API GatewayとLambdaの統合
      プロキシ統合を選択したため、
      Lambda関数でレスポンスヘッダーを適切に設定する必要がありました。
  3. 解決策の実装
    最終的に、AWSの認証プロセスを正しく行うために、
    Next.js側のコードでaxios、@aws-sdk/protocol-http、@aws-crypto/sha256-universal、@smithy/signature-v4、aws-sdkなどのライブラリを使用しました。
  4. エラーへの対処
    「The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method.」
    というエラーメッセージに直面した際の対処方法を共有しました。

どハマりしたものの、1日でなんとかなって良かったです!
同様の技術スタックを使用する方々にとって有益な情報源となれば幸いです!