6.1 サーバレスアーキテクチャと認証 Lambdaでの J W T認証について

6.1 サーバレスアーキテクチャと認証: LambdaでのJWT認証について #

サーバレスアーキテクチャでは伝統的なセッションベースの認証の代わりに、JSON Web Token(JWT)のような方法で認証をする必要があります。ここではその理由と解決策について解説します。

サーバレス環境とは何か #

まず最初に、サーバレス環境とは何かを理解することから始めます。サーバレスアーキテクチャ(FaaS、Function as a Serviceとも呼ばれます)では、開発者は個々の関数を書き、それらの関数がイベント(HTTPリクエストなど)に応じて自動的にスケーリングされ、実行されます。そのため、サーバの起動や維持について心配する必要がありません。

AWS Lambdaは、このようなサーバレスアーキテクチャの一例であり、開発者はコードのロジックに専念することができます。

サーバレスアーキテクチャでの認証 #

しかし、この種のアーキテクチャには、セッションベースの認証に関していくつかの課題があります。セッションベースの認証では、認証情報はサーバ内のセッションストアに保存され、そのセッションIDがクライアントに送り返されます。クライアントはその後、そのセッションIDを使用してさらなるリクエストを行います。

しかし、サーバレス環境では、リクエストごとに新しいコンテナ(またはランタイム環境)が作成され、リクエストの終了後に破棄されます。これは、サーバの状態を保存することが困難であることを意味し、それゆえにセッションベースの認証が動作しません。

JWT認証の利点 #

その代わりとなる方法として、JWT認証が推奨されます。JSON Web Token(JWT)は、クライアントとサーバ間で情報を安全に交換するためのコンパクトな方法です。これは、デジタル署名されたセキュアなトークンで、これによって認証や情報交換が可能になります。

JWTの利点は以下のとおりです:

  1. ステートレス: JWTは自己完結型であり、トークン自体が認証情報を保持します。したがって、サーバはセッション情報を保持する必要がありません。この性質がサーバレスアーキテクチャと相性が良いのです。

  2. スケーラビリティ: JWTはクライアントサイドで保存されるため、バックエンドサービスがステートレスであることを保証します。これにより、アプリケーションは容易にスケールアップすることができます。

  3. セキュリティ: JWTはデジタル署名によって安全に保護されています。署名は秘密鍵を用いて行われるため、トークンの信頼性を確認することができます。

JWT認証のフロー #

以下に、JWT認証の基本的なフローを示します:

  1. ユーザーが認証情報(通常はユーザ名とパスワード)を用いてログインします。
  2. サーバは認証情報を確認し、そのユーザーのためのJWTを生成します。JWTには、ユーザーIDや有効期限などの情報(ペイロード)が含まれます。このトークンは、秘密鍵を用いて署名されます。
  3. JWTはクライアントに返され、クライアントはそれを保存します(通常はHTTP Only CookieやLocalStorageなどに)。
  4. クライアントは後続のリクエストにJWTを添付します(通常はHTTPヘッダのAuthorizationフィールド)。
  5. サーバは受け取ったJWTを検証し、そのペイロードからユーザー情報を取得します。

リクエストは認証され、レスポンスがクライアントに返されます。

flask-jwt-extendedを用いたJWT認証 #

FlaskでのJWT認証の基本的な実装は次のようになります。

from flask import Flask, jsonify, request
from flask_jwt_extended import JWTManager, jwt_required, create_access_token

app = Flask(__name__)
app.config['JWT_SECRET_KEY'] = 'your-secret-key'  # シークレットキーを設定します
jwt = JWTManager(app)  # JWTManagerインスタンスを作成します

admin_user_id = 'your-user-id' # 管理者ユーザーID
admin_password = 'your-password' # 管理者パスワード
users = {admin_user_id: {"user_id": admin_user_id, "password": admin_password}}

# ログインページ
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        user_id = request.form['user_id']
        password = request.form['password']
        user = users.get(user_id)
        if not user or password != user['password']:
            return "Invalid username or password", 401
        # ログイン処理
        access_token = create_access_token(identity=user_id)
        return jsonify(access_token=access_token), 200

    return render_template('login.html')

# 認証が必要なページ
@app.route('/api/protected-page', methods=['GET'])
@jwt_required()  # このエンドポイントはJWTが必要です
def protected_page():
    return jsonify({'auth': 'ok'}), 200

if __name__ == '__main__':
    app.run()

このコードでは、/loginエンドポイントでユーザーIDとパスワードを受け取り、認証に成功した場合にはJWTを返します。/api/protected-pageエンドポイントでは、@jwt_required()デコレータによってJWTが必要であることを示しています。

クライアントは、保護されたエンドポイント(この例では/api/protected-page)にアクセスするために、このトークンを使用します。このエンドポイントにアクセスするためには、HTTPヘッダのAuthorizationフィールドにBearer <アクセストークン>を設定する必要があります。このトークンが検証され、有効であると確認された場合にのみ

バックエンドでは以上のような処理が行われます、クライアント側ではどのような処理が行われるのでしょうか?

フロントエンドでのJWT認証を使用したログイン #

ユーザがログインする際に、Flaskバックエンドの/loginエンドポイントに対してPOSTリクエストを送ります。成功すると、このリクエストはJWTを含むレスポンスを返します。

このJWTをブラウザのlocalStorageに保存します。これにより、ユーザが再度サイトを訪れたときに、トークンを再度取得する必要がなくなります。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="robots" content="noindex, nofollow">
    <title>NanoCMS | ログイン</title>
    <script>
        var rootURL = "{{ url_for('index') }}";
        function onSubmit(e) {
            e.preventDefault();
            let form = e.target;
            let formData = new FormData(form);
            fetch(form.action, {
                method: 'POST',
                body: formData
            })
            .then(response => response.json())
            .then(data => {
                if(data.access_token) {
                    localStorage.setItem('access_token', data.access_token);
                    window.location.href = rootURL;
                } else {
                    alert('ログインに失敗しました');
                }
            })
            .catch((error) => {
                console.error('Error:');
            });
        }
    </script>
</head>
<body>
    <h1>ログイン</h1>
    <p>ログインしてください。</p>
    <form action="{{ url_for('login') }}" method="post" onsubmit="onSubmit(event)">
        <label for="user_id">ユーザーネーム</label>
        <input type="text" name="user_id" id="user_id" placeholder="mailaddress@example.com" required>
        <label for="password">パスワード</label>
        <input type="password" name="password" id="password" placeholder="*********" required>
        <button type="submit">ログイン</button>
    </form>
</body>
</html>

保護されたページにアクセスした際に認証を走らせて、認証に失敗したらログインページにリダイレクトする #

base.htmlに以下のようなスクリプトを追加します。

    window.onload = function() {
      var jwt = localStorage.getItem('access_token');

      var loginURL = "{{ url_for('login') }}";
      var apiURL = "{{ url_for('protected_page') }}";

      if (!jwt) {
        console.log('No JWT found in localStorage');
        // If there's no JWT in localStorage, redirect to the login page
        window.location.href = loginURL;
      } else {
        // If there's a JWT, fetch the protected page content
        fetch(apiURL, {
          method: 'POST',
          headers: {
            'Authorization': 'Bearer ' + jwt,
          },
        })
        .then(function(response) {
          if (response.ok) {
            return response.text();
          } else {
            localStorage.removeItem('access_token');
            throw new Error('Failed to fetch protected page');
          }
        })
        .catch(function(error) {
          console.error('There has been a problem with your fetch operation:', error);
          localStorage.removeItem('access_token');
        });
      }
    };

ページを表示する際に、localStorageに保存されているJWTをAuthorizationヘッダに設定して送信します。 失敗したら、ログインページにリダイレクトします。 認証に失敗したらローカルストレージを削除しています。

ログアウト処理を追加する #

base.htmlに以下のようなスクリプトを追加します。

    function logout() {
        localStorage.removeItem('access_token');
        var loginURL = "{{ url_for('login') }}";
        window.location.href = loginURL;
    }

まとめ #

この方法を用いることで、AWS Lambdaのようなサーバレス環境でも、セッションベースの認証をJWT認証に置き換えることができます。これにより、アプリケーションはスケーラビリティとセキュリティを確保しつつ、認証を効率的に行うことが可能になります。