サーバー間API認証 :APIキー、JWT、OAuth 2.0をサンプルコードで比較してみた

Security

マイクロサービスアーキテクチャや外部サービス連携が当たり前になった昨今、「サーバー間(M2M: Machine-to-Machine)のAPIリクエストをいかに安全に認証するか」は、すべての開発者にとって避けては通れない課題です。

「とりあえずBasic認証で…」と考えてしまうこともありますが、よりセキュアでモダンな方法はいくつも存在します。

この記事では、サーバー間API認証でよく使われる以下の3つの方式を取り上げ、Windows環境のPython (Flask) で実装したサーバーと、Windows環境のcurlを使ったクライアント(バッチファイル)の具体的なサンプルコードをすべてお見せしながら、それぞれの仕組み、メリット・デメリットを解説します。

  1. APIキー認証: シンプルで最も手軽な方式
  2. JWT (JSON Web Token) 認証: ステートレスでモダンなアーキテクチャに適した方式
  3. OAuth 2.0 (Client Credentials Grant): 標準化され、権限管理にも優れた方式

では行ってみましょう。

この記事の内容
  • APIキー認証の仕組みと実装方法を説明します。
  • JWT(JSON Web Token)認証の仕組みと実装方法を説明します。
  • OAuth2.0 (Clinet Credentitals Grant)の仕組みと実装方法を説明します。
開発・動作環境

この記事のプログラムは、以下の環境で開発および動作確認を行っています
・OS: Windows11
・言語: Python 3.13
・Webフレームワーク: Flask 3.1.1

1. APIキー認証 (API Key Authentication)

最もシンプルで直感的な認証方式です。サーバーとクライアントの間で事前に共有された「秘密の文字列(APIキー)」をリクエストに含めることで認証を行います。

仕組み

クライアントは、HTTPリクエストヘッダー(例: x-api-key)やクエリパラメータにAPIキーを付与してリクエストを送信します。サーバー側は、そのキーが登録されているものと一致するかを検証するだけです。

サンプルコード

実際に動かして試せるコードを見ていきましょう。

サーバー側: api_key_auth.py

シンプルなFlaskアプリケーションです。x-api-keyヘッダーで送られてきたAPIキーを検証するデコレーターを定義しています。

from flask import Flask, jsonify, request
import datetime
from functools import wraps

app = Flask(__name__)

# 本来は環境変数などから読み込むべき静的なAPIキー
API_KEY = "my-super-secret-api-key"

# APIキーを検証するデコレーター
def token_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        token = request.headers.get('x-api-key')
        if not token:
            return jsonify({'message': 'Token is missing!'}), 401
        if token != API_KEY:
            return jsonify({'message': 'Token is invalid!'}), 401
        return f(*args, **kwargs)
    return decorated

@app.route("/api/time")
@token_required
def get_time():
    """
    現在の日時と曜日を返すAPIエンドポイント
    """
    now = datetime.datetime.now()
    weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
    response = {
        "date": now.strftime('%Y-%m-%d'),
        "time": now.strftime('%H:%M:%S'),
        "weekday": weekdays[now.weekday()]
    }
    return jsonify(response)

if __name__ == '__main__':
    # 外部からアクセス可能にするために host='0.0.0.0' を指定
    app.run(debug=True, port=5001, host='0.0.0.0')

クライアント側: api_key_auth_curlclient.bat

curlを使ってAPIサーバーにリクエストを送信します。-H オプションで x-api-key ヘッダーに正しいAPIキーを指定しているのがポイントです。

 curl -X GET http://localhost:5001/api/time -H "x-api-key: my-super-secret-api-key"
pause

実行結果のイメージ

上記バッチファイルを実行すると、コマンドプロンプトに以下のようなJSONレスポンスが表示されます。

{
  "date": "2025-07-27",
  "time": "14:15:30",
  "weekday": "Sunday"
}

2. JWT認証 (JSON Web Token)

JWTは、認証情報をJSON形式で表現し、電子署名を付与することで改ざんを防ぐ仕様です。APIキーと異なり、トークン自体に有効期限やユーザー情報などのデータ(ペイロード)を含めることができます。

仕組み

  1. ログイン: クライアントは、ID/パスワードなどの認証情報を使ってサーバーの「ログインエンドポイント」にリクエストを送信します。
  2. トークン発行: サーバーは認証情報が正しければ、ペイロード(ユーザーID、有効期限など)と秘密鍵を使ってJWTを生成し、クライアントに返します。
  3. APIアクセス: クライアントは、以降のAPIリクエストで Authorization: Bearer <JWT> という形式でHTTPヘッダーにJWTを付与します。
  4. トークン検証: サーバーは受け取ったJWTの署名を秘密鍵で検証し、有効期限などをチェックします。検証に成功すればリクエストを処理します。

サーバー側でセッション情報を持つ必要がないステートレスな認証が実現できるため、マイクロサービスのようにサーバーをスケールさせやすいアーキテクチャと非常に相性が良いです。

JWTの構造

出力されるJWTは次のような3パートの構造になっています:

xx.yy.zz
│ │ └署名(秘密鍵で生成)
│ ペイロード(user_id,exp等)
ヘッダー(alg: HS256, typ: JWT)

実際のリクエスト例

GET /api/time HTTP/1.1
Host: example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

この eyJhbGci... がJWT(署名付きのトークン)です。

下記のサンプルソースのjwt.encode() 関数の引数をご覧ください。

  • app.config['SECRET_KEY']: これが署名に使われる秘密鍵です。サーバーだけが知っているこの鍵を使って署名することで、トークンが正規に発行されたものであることを保証します。
  • algorithm="HS256": これは署名アルゴリズムを指定しています。HS256(HMAC using SHA-256)という共通鍵暗号方式を使って、トークンのヘッダーとペイロードから署名を作成します。

この署名があるおかげで、APIサーバーは受け取ったJWTが改ざんされていないか、また本当に自分が発行したものであるかを検証できるのです。

検証も署名込み

APIアクセス時、以下のように署名を秘密鍵を使って検証しています:

data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])

サンプルコード

サーバー側: jwt_auth.py

/login エンドポイントでJWTを発行し、/api/time エンドポイントでそのJWTを検証します。PyJWTライブラリを使用しています。

from flask import Flask, jsonify, request
from datetime import datetime, timezone, timedelta
import jwt
from functools import wraps

app = Flask(__name__)

# JWTの署名に使う秘密鍵(本来は環境変数などで厳重に管理)
app.config['SECRET_KEY'] = 'your-very-secret-key'

# 認証を必要とするAPIのためのデコレーター
def token_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        token = request.headers.get('Authorization')
        if not token:
            return jsonify({'message': 'Token is missing!'}), 401
        
        # "Bearer "プレフィックスを削除
        if token.startswith('Bearer '):
            token = token.split(' ')[1]
        else:
            return jsonify({'message': 'Token format is invalid!'}), 401

        try:
            # トークンをデコードしてペイロードを検証
            data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])
            current_user = data['user_id'] # 例としてユーザーIDを取得
        except jwt.ExpiredSignatureError:
            return jsonify({'message': 'Token has expired!'}), 401
        except jwt.InvalidTokenError:
            return jsonify({'message': 'Token is invalid!'}), 401

        return f(current_user, *args, **kwargs)
    return decorated

# ログインエンドポイント(JWTを生成して返す)
@app.route('/login', methods=['POST'])
def login():
    auth = request.authorization
    # 簡単なユーザー名とパスワードのチェック(本来はDBなどで検証)
    if auth and auth.username == 'user' and auth.password == 'password':
        token = jwt.encode({
            'user_id': 'testuser',
            'exp': datetime.now(timezone.utc) + timedelta(minutes=30) # 30分で有効期限切れ
        }, app.config['SECRET_KEY'], algorithm="HS256")
        
        return jsonify({'token': token})

    return jsonify({'message': 'Could not verify!'}), 401, {'WWW-Authenticate': 'Basic realm="Login required!"'}

# 保護されたAPIエンドポイント
@app.route("/api/time")
@token_required
def get_time(current_user):
    """
    現在の日時と曜日を返すAPIエンドポイント
    """
    now = datetime.now()
    weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
    response = {
        "date": now.strftime('%Y-%m-%d'),
        "time": now.strftime('%H:%M:%S'),
        "weekday": weekdays[now.weekday()],
        "requester": current_user
    }
    return jsonify(response)

if __name__ == '__main__':
    # 外部からアクセス可能にするために host='0.0.0.0' を指定
    app.run(debug=True, port=5002, host='0.0.0.0')

クライアント側: jwt_auth_curlclient.bat

まず /login にBasic認証でリクエストを送り、返ってきたJSONからトークンを抽出。次にそのトークンを Authorization: Bearer ヘッダーにセットして /api/time にアクセスしています。

@echo off
rem 本ファイルをUTF-8で保存する前提で、CMDのコードページをUTF-8に切り替える。
rem これが無いとCMDはShifJISと認識して日本語表示するので、文字化けする。
chcp 65001
setlocal enabledelayedexpansion


echo === ステップ1: JWT トークンの取得 ===

rem 一時ファイルに保存
curl -s -X POST http://localhost:5002/login -u user:password > tmp_response.json

rem 改行を取り除いて1行にまとめる
set response=
for /f "usebackq delims=" %%i in (`type tmp_response.json`) do (
    set "line=%%i"
    set "response=!response!!line!"
)

rem レスポンス全体の表示
echo [取得したレスポンス(整形済み)]
echo !response!
echo.

echo === ステップ2: トークンの抽出 ===
rem {"token":"xxxxx.yyy.zzz"} から:を区切り文字として2つ目の要素を取得する
for /f "tokens=2 delims=:" %%a in ("!response!") do (
    set "token_raw=%%a"
)

echo [トークン抽出1]!token_raw!
rem 余計な " と } を削除
set "token=!token_raw:}=!"
set "token=!token:"=!"
set "token=!token: =!"
echo [トークン抽出2]!token!
echo.

echo === ステップ3: トークンを使ってAPI呼び出し ===
curl -s -X GET http://localhost:5002/api/time -H "Authorization: Bearer !token!"


rem 後始末
del tmp_response.json >nul 2>&1

pause

実行結果のイメージ

=== ステップ1: JWT トークンの取得 ===
[取得したレスポンス(整形済み)]
{  "token": "xxx.yyy.xxx"}

=== ステップ2: トークンの抽出 ===
[トークン抽出1] "xxx.yyy.xxx"}
[トークン抽出2]xxx.yyy.xxx

=== ステップ3: トークンを使ってAPI呼び出し ===
{
  "date": "2025-08-11",
  "requester": "testuser",
  "time": "12:17:50",
  "weekday": "Monday"
}
Press any key to continue . . .  

3. OAuth 2.0 (Client Credentials Grant)

OAuth 2.0は、APIの権限管理における現在のデファクトスタンダード(事実上の標準)です。これは単なる認証方式ではなく、安全に権限を委譲(認可)するための「フレームワーク」であり、特に外部のアプリケーションに安全なアクセスを許可する場面で真価を発揮します。様々な利用シーンに対応するため複数のフロー(Grant Type)が定義されており、サーバー間通信のようにユーザーが介在しない場合にはクライアント クレデンシャル グラント (Client Credentials Grant) というフローが用いられます。

RFC 6749: The OAuth 2.0 Authorization Framework
The OAuth 2.0 authorization framework enables a third-party application to obtain limited access to an HTTP service, eit...

仕組み

  1. クライアント登録: 事前にAPIを提供するサーバー(認可サーバー)にクライアントアプリを登録し、「クライアントID」と「クライアントシークレット」を発行してもらいます。
  2. トークンリクエスト: クライアントは、クライアントIDとシークレットを使って認可サーバーの「トークンエンドポイント」にリクエストを送信します。
  3. アクセストークン発行: 認可サーバーはIDとシークレットを検証し、正しければ「アクセストークン」を発行します。
  4. APIアクセス: クライアントは、JWTの時と同様に Authorization: Bearer <アクセストークン> ヘッダーを付与して、保護されたAPI(リソースサーバー)にアクセスします。

JWTと流れは似ていますが、OAuth 2.0は標準化された仕様(RFC 6749)であり、多くのライブラリやサービスでサポートされています。また、スコープ(権限範囲)の概念を導入することで、トークンに与える権限を細かく制御できるのが大きな特徴です。

サンプルコード

サーバー側: oauth_auth.py

クライアントID/シークレットを検証してアクセストークンを発行する /oauth/token エンドポイントと、そのトークンを検証する /api/profile エンドポイントを実装しています。

# -*- coding: utf-8 -*-
from flask import Flask, request, jsonify
import secrets
import time
import datetime

app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False

# --- OAuth 2.0 Mock Data ---
# 本来はデータベースで管理します
clients = {
    "my-client": {
        "client_secret": "my-client-secret",
        "scope": "time"
    }
}
access_tokens = {}
# -------------------------

@app.route('/oauth/token', methods=['POST'])
def token():
    """トークンエンドポイント。クライアントクレデンシャルをアクセストークンと交換します。"""
    client_id = request.form.get('client_id')
    client_secret = request.form.get('client_secret')
    grant_type = request.form.get('grant_type')

    if grant_type != 'client_credentials':
        return jsonify({"error": "unsupported_grant_type"}), 400

    if not all([client_id, client_secret]):
        return jsonify({"error": "invalid_request"}), 400

    if (client_id not in clients or
            clients[client_id]['client_secret'] != client_secret):
        return jsonify({"error": "invalid_client"}), 401

    # アクセストークンを生成
    access_token = secrets.token_urlsafe(32)
    access_tokens[access_token] = {
        "client_id": client_id,
        "expires_at": time.time() + 3600  # 1時間有効
    }

    return jsonify({
        "access_token": access_token,
        "token_type": "Bearer",
        "expires_in": 3600
    })

@app.route('/api/profile', methods=['GET'])
def profile():
    """保護されたリソース。アクセストークンを検証します。"""
    auth_header = request.headers.get('Authorization')
    if not auth_header or not auth_header.startswith('Bearer '):
        return jsonify({"error": "unauthorized"}), 401

    token = auth_header.split(' ')[1]

    token_info = access_tokens.get(token)
    if not token_info or token_info['expires_at'] < time.time():
        return jsonify({"error": "invalid_token"}), 401

    client_id = token_info["client_id"]

    # 実際のAPIロジック
    now = datetime.datetime.now()
    weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
    response = {
        "user_id": client_id,
        "date": now.strftime('%Y-%m-%d'),
        "time": now.strftime('%H:%M:%S'),
        "weekday": weekdays[now.weekday()]
    }
    return jsonify(response)

if __name__ == '__main__':
    print("OAuth 2.0 Client Credentials Mock Server starting...")
    print("Token URL: http://localhost:5003/oauth/token")
    print("API URL: http://localhost:5003/api/profile")
    app.run(debug=True, port=5003, host='0.0.0.0')

クライアント側: oauth_auth_curlclient.bat

クライアントIDとシークレットをPOSTデータの -d オプションで送信し、アクセストークンを取得。その後、取得したトークンを使ってAPIにアクセスします。

@echo off
setlocal enabledelayedexpansion

set CLIENT_ID=my-client
set CLIENT_SECRET=my-client-secret
set AUTH_SERVER_URL=http://localhost:5003

echo === OAuth 2.0 Client Credentials Flow Simulation ===
echo.
echo 1. Exchange Client Credentials for Access Token
echo --------------------------------------------------

rem Get token and save to a temporary file
curl -s -X POST %AUTH_SERVER_URL%/oauth/token -d client_id=%CLIENT_ID% -d client_secret=%CLIENT_SECRET% -d grant_type=client_credentials > tmp_token_response.json

echo Response from token endpoint:
type tmp_token_response.json
echo.

rem Extract access token using findstr and simple parsing
for /f "tokens=2 delims=:," %%a in ('findstr /c:"access_token" tmp_token_response.json') do (
    set "ACCESS_TOKEN_RAW=%%a"
)

rem Remove quotes and trim spaces
set "ACCESS_TOKEN=!ACCESS_TOKEN_RAW:"=!"
for /f "tokens=*" %%b in ("!ACCESS_TOKEN!") do set "ACCESS_TOKEN=%%b"

echo Extracted Access Token: !ACCESS_TOKEN!
echo.

echo 2. Access Protected Resource
echo --------------------------------------------------

curl -s -X GET %AUTH_SERVER_URL%/api/profile -H "Authorization: Bearer !ACCESS_TOKEN!"

echo.

rem Cleanup
del tmp_token_response.json >nul 2>&1

pause

実行結果のイメージ

=== OAuth 2.0 Client Credentials Flow Simulation ===

1. Exchange Client Credentials for Access Token
--------------------------------------------------
Response from token endpoint:
{
  "access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "expires_in": 3600,
  "token_type": "Bearer"
}

Extracted Access Token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

2. Access Protected Resource
--------------------------------------------------
{
  "date": "2025-07-27",
  "email": "taro.yamada@example.com",
  "name": "Taro Yamada",
  "time": "14:25:10",
  "user_id": "testuser",
  "weekday": "Sunday"
}

続行するには何かキーを押してください . . .


サンプルコード2

サーバー側: oauth_oauthlib.py

サンプルコード2は、上のサンプルコートとやっている事は同じですが、今回はoauthlibでOAuth 2.0のロジックを組み込んでいます。

# -*- coding: utf-8 -*-
from flask import Flask, request, jsonify
from oauthlib.oauth2 import RequestValidator, BackendApplicationServer
import time
import datetime
from types import SimpleNamespace

# --- OAuth 2.0 Mock Data ---
# 本来はデータベースで管理します
clients = {
    "my-client": {
        "client_secret": "my-client-secret",
        "scope": "time"
    }
}
access_tokens = {}
# -------------------------

class MyRequestValidator(RequestValidator):
    """oauthlibが必要とするバリデーションを実装します。"""

    def authenticate_client(self, request, *args, **kwargs):
        """クライアントIDとシークレットを検証します。"""
        client_id = request.body.get('client_id')
        client_secret = request.body.get('client_secret')

        if not client_id or not client_secret:
            return False

        client = clients.get(client_id)
        if client and client['client_secret'] == client_secret:
            # oauthlibはrequest.clientに.client_id属性を持つオブジェクトを期待します。
            request.client = SimpleNamespace(client_id=client_id)
            return True

        return False

    def validate_grant_type(self, client_id, grant_type, client, request, *args, **kwargs):
        """許可されたグラントタイプか検証します。"""
        return grant_type == 'client_credentials'

    def get_default_scopes(self, client_id, request, *args, **kwargs):
        """クライアントのデフォルトスコープを返します。"""
        client = clients.get(client_id)
        if client:
            return client.get("scope", "").split()
        return []

    def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
        """要求されたスコープがクライアントに許可されているか検証します。"""
        client = clients.get(client_id)
        if not client:
            return False
        
        allowed = set(client.get("scope", "").split())
        return set(scopes).issubset(allowed)

    def save_bearer_token(self, token, request, *args, **kwargs):
        """発行したアクセストークンを保存します。"""
        access_tokens[token['access_token']] = {
            "client_id": request.client.client_id,
            "expires_at": time.time() + token['expires_in']
        }
        return "http://localhost:5004/api/profile" # Default redirect_uri

    def validate_bearer_token(self, token, scopes, request):
        """保護されたリソースへのアクセストークンを検証します。"""
        token_info = access_tokens.get(token)
        if token_info and token_info['expires_at'] > time.time():
            request.client_id = token_info['client_id']
            return True
        return False

app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False
server = BackendApplicationServer(MyRequestValidator())

@app.route('/oauth/token', methods=['POST'])
def token():
    """トークンエンドポイント。oauthlibを使用してトークンを発行します。"""
    headers, body, status = server.create_token_response(request.url, request.method, request.form, request.headers)
    return app.response_class(response=body, status=status, headers=headers)

@app.route('/api/profile', methods=['GET'])
def profile():
    """保護されたリソース。oauthlibを使用してトークンを検証します。"""
    is_valid, credential = server.verify_request(request.url, request.method, None, request.headers)

    if not is_valid:
        return jsonify({"error": "invalid_token"}), 401

    # 実際のAPIロジック
    now = datetime.datetime.now()
    weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
    response = {
        "user_id": credential.client_id,
        "date": now.strftime('%Y-%m-%d'),
        "time": now.strftime('%H:%M:%S'),
        "weekday": weekdays[now.weekday()]
    }
    return jsonify(response)

if __name__ == '__main__':
    print("OAuth 2.0 (oauthlib) Client Credentials Server starting...")
    print("Token URL: http://localhost:5004/oauth/token")
    print("API URL: http://localhost:5004/api/profile")
    app.run(debug=True, port=5004, host='0.0.0.0')

コードの解説

1. MyRequestValidator クラス

このクラスがoauthlibの心臓部です。OAuth 2.0の各ステップで「何を検証し」「何を許可するか」という具体的なロジックを私たちが定義する場所です。

  • authenticate_client: トークン発行時、リクエストで送られてきたclient_idclient_secretが正しいか検証します。
  • validate_grant_type: 今回はclient_credentialsというグラントタイプのみを許可するように設定しています。
  • validate_scopes: リクエストされた権限(scope)をクライアントが持ってて良いか検証します。
  • save_bearer_token: 発行したアクセストークンを(今回は辞書に)保存します。有効期限などの情報も一緒に保存するのがポイントです。
  • validate_bearer_token: 保護されたAPIへのアクセス時、リクエストヘッダーに含まれるアクセストークンが有効か(存在するか、有効期限内か)を検証します。

2. Flaskのエンドポイント

Flaskを使って2つのURL(エンドポイント)を用意しています。

  • /oauth/token: このエンドポイントはserver.create_token_response()を呼び出すだけです。実際のトークン発行処理の大部分は、先ほど定義したMyRequestValidatorとoauthlibのBackendApplicationServerが裏側で連携して行ってくれます。
  • /api/profile: 保護したいAPIです。まずserver.verify_request()を呼び出してトークンの検証を行います。検証に成功(is_validがTrue)した場合のみ、後続のAPI処理を実行します。失敗した場合は401エラーを返します。

クライアントサイドの実装 (curl)

次に、作成したAPIサーバーにアクセスするためのクライアント側のスクリプトです。今回はWindowsのバッチファイルで、curlコマンドを使ってAPIを叩きます。

@echo off
setlocal enabledelayedexpansion

set CLIENT_ID=my-client
set CLIENT_SECRET=my-client-secret
set AUTH_SERVER_URL=http://localhost:5004

echo === OAuth 2.0 Client Credentials Flow Simulation (oauthlib) ===
echo.
echo 1. Exchange Client Credentials for Access Token
echo --------------------------------------------------

rem Get token and save to a temporary file
curl -s -X POST %AUTH_SERVER_URL%/oauth/token --data-urlencode "client_id=%CLIENT_ID%" --data-urlencode "client_secret=%CLIENT_SECRET%" --data-urlencode "grant_type=client_credentials" > tmp_token_response.json

echo Response from token endpoint:
type tmp_token_response.json
echo.
rem Extract access token using findstr and simple parsing
for /f "tokens=2 delims=:," %%a in ('findstr /c:"access_token" tmp_token_response.json') do (
    set "ACCESS_TOKEN_RAW=%%a"
)

rem Remove quotes and trim spaces
set "ACCESS_TOKEN=!ACCESS_TOKEN_RAW:"=!"
for /f "tokens=*" %%b in ("!ACCESS_TOKEN!") do set "ACCESS_TOKEN=%%b"

echo Extracted Access Token: !ACCESS_TOKEN!
echo.

echo 2. Access Protected Resource
echo --------------------------------------------------

curl -s -X GET %AUTH_SERVER_URL%/api/profile -H "Authorization: Bearer !ACCESS_TOKEN!"


rem Cleanup
del tmp_token_response.json >nul 2>&1

pause

スクリプトの解説

このスクリプトは、OAuth 2.0のClient Credentialsフローを忠実に再現しています。

  1. アクセストークンの取得: /oauth/tokenに対し、client_idclient_secretをPOSTリクエストのボディに含めて送信します。サーバーはこれを検証し、問題なければアクセストークンを返します。
  2. 保護されたリソースへのアクセス: 受け取ったアクセストークンを、HTTPのAuthorizationヘッダーにBearer <トークン>という形式で設定し、/api/profileにGETリクエストを送信します。

4. 補足:認証トークンの用語整理 (APIキー, Bearerトークン, JWT)

ここで少し、認証で使われるトークン関連の用語を整理しておきましょう。「APIキー」と「Bearerトークン」は何が違うのか、そして「JWT」はどこに位置するのかを明確にします。

  • トークンベース認証: 認証に何らかの「トークン(しるし)」を使う方式全般を指す広い言葉です。APIキーもJWTもこの一種です。
  • APIキー: 主にクライアントが誰であるかを識別するための、静的で比較的長い期間有効な「文字列」です。送信方法に決まったルールはありません。
  • Bearerトークン: 「Bearer」は「持参人」を意味し、「このトークンを持っている人(持参人)はアクセスを許可する」という考え方に基づいたトークンの使い方を指します。Authorization: Bearer <token> という標準化された形式で送信するのが特徴で、通常は短命なトークンが使われます。
    OAuth 2.0 (Client Credentials Grant)もBearerトークンを使います。
    BearerトークンにはJWTを使う事もできます。
  • JWT (JSON Web Token): トークン自体の仕様・フォーマットの一つです。電子署名を持ち、有効期限などの情報(クレーム)を内部に保持できるという特徴があります。

これらの関係性をまとめると以下のようになります。

観点APIキーBearerトークン
目的誰か(クライアント)を識別する何ができるか(権限)を証明する
送信形式独自 (例: x-api-key: ...)標準 (Authorization: Bearer ...)
性質静的・長期間有効動的・短期間有効が多い
中身単なる秘密の文字列様々な形式のトークン(JWTなど)
JWTとの関係直接の関係はないJWTは、Bearerトークンとして利用される代表的な形式の一つ

つまり、「JWTという形式のトークンを、Bearerトークンという使い方でAPIサーバーに渡す」というのが、この記事のJWT認証やOAuth 2.0で実現していることです。APIキーはこれらとは異なる、よりシンプルな識別子の仕組みと言えます。

5. OAuthの補足

OAuthのサーバー間通信にOpenID Connectは不要なのか?

OAuth 2.0と共によく語られるプロトコルにOpenID Connect (OIDC) がありますが、この記事で解説しているサーバー間通信のシナリオでは基本的に不要です。その理由は、それぞれの目的の違いにあります。

  • OAuth 2.0 (認可): 「アプリケーション」に、リソース(データ)へのアクセス権限を与えるプロトコルです。「何ができるか」を定義します。
  • OpenID Connect (認証): OAuth 2.0を拡張し、「ログインしているエンドユーザーが誰か」を認証し、身元情報を証明するためのプロトコルです。「誰であるか」を定義します。

サーバー間通信の主体は「アプリケーション」であり、ログイン操作を行う人間の「エンドユーザー」は存在しません。そのため、ユーザーの本人確認を目的とするOIDCの出番はなく、アプリケーション自体を認可するOAuth 2.0のクライアントクレデンシャル・グラントが最適な選択肢となるのです。

Client Credentials Grant はclinet_idとclient_secretを渡して認証しているので、apiトークンと変わらない?

OAuthのClient Credentials Grantは本質的にclient_idとclient_secret(パスワード)を使った認証で、一見するとAPIトークンと似ています。

ただし、重要な違いがいくつかあります:

技術的な違い:

  • OAuthのClient Credentials Grantではアクセストークンに有効期限があり、定期的に更新される
  • 一方、従来のAPIトークンは多くの場合、明示的に無効化するまで永続的

セキュリティ上の利点:

  • OAuthのClient Credentials Grantのアクセストークンの有効期限は短い(通常数時間〜数日)ため、漏洩時の影響を限定できる
  • トークンが盗まれても、時間が経てば自動的に使用不可になる
  • 認証サーバー側でトークンの即座な無効化が可能

運用面での違い:

  • OAuth 2.0の標準仕様に準拠しているため、様々なライブラリやツールでサポートされている
  • トークンの管理(取得、更新、無効化)が標準化されている

確かに「client_secretを知っていれば認証できる」という点では、長期間有効なAPIトークンと本質的には同じリスクがあります。しかし、トークンのライフサイクル管理と標準化された仕組みによって、より安全な運用が可能になっているというのが主な違いです。

OAuth 2.0 (Client Credentials Grant)はHTTPSが必須?

HTTSは必須です。

OAuth 2.0、特にクライアントシークレットのような機密情報を扱うClient Credentials Grantでは、HTTPS (TLS) を使用することがセキュリティ上の絶対的な要件とされています。

理由は、通信路上での情報の盗聴や改ざんを防ぐためです。

  1. クライアントシークレットの保護
    • /oauth/tokenエンドポイントへのリクエストには、パスワードに等しいclient_secretが含まれます。HTTPSを使わないと、このシークレットが平文のままネットワークを流れてしまい、攻撃者に簡単に盗まれてしまいます。
  2. アクセストークンの保護
    • サーバーから返されるaccess_tokenも同様です。アクセストークンは「Bearer(持参人)」トークンなので、これを盗まれると、攻撃者はクライアントになりすましてAPIを不正に利用できてしまいます。

なお、今回紹介したAPIトークンを利用する場合も、JWTを利用する場合も、HTTPSは必須です。

6. 3方式の比較:メリットとデメリット

それぞれの方式の特徴を、メリット・デメリットの観点で表にまとめました。

項目APIキー認証JWT認証OAuth 2.0 (Client Credentials)
実装の容易さ◎ 非常に容易○ 比較的容易 (ライブラリ利用)△ やや複雑 (フローの理解が必要)
セキュリティ△ (キー漏洩リスク, 有効期限なし)○ (署名, 有効期限)◎ (標準化, スコープによる権限分離)
ステートどちらでも可 (通常ステートレス)◎ ステートレス○ (トークン管理でステートフルになりがち)
標準化× (ヘッダー名など独自仕様)○ (RFC 7519)◎ (RFC 6749)
トークン失効○ (サーバー側でキーを削除)△ (有効期限まで有効。失効リスト管理が必要)○ (サーバー側でトークンを無効化)
ユースケース内部向け、個人開発のシンプルなAPIマイクロサービス、ステートレスなAPI外部公開API、サードパーティ連携、権限管理

7. OpenAPI (Swagger) への準拠


APIの仕様を記述するための標準的なフォーマットであるOpenAPI(バージョン3以降)は、APIのセキュリティ要件を明確に定義する強力な機能を提供します。これは securitySchemes というオブジェクトを用いて実現されます。

元々Swaggerとして知られていたこの仕様を用いることで、APIの利用者はどのような認証情報(APIキー、OAuth2トークンなど)が必要なのかをドキュTメントから正確に理解できます。

securitySchemes を利用するメリット:

ツールの活用による開発効率の向上: Swagger UI や Redoc といったツールが securitySchemes の定義を解釈し、インタラクティブなAPIドキュメントを自動で生成します。これにより、開発者はUI上から直接認証情報を入力し、APIを試すことが可能になります。

ドキュメントの明確化: 認証・認可の方式が仕様として標準化されるため、誰が読んでも理解しやすいドキュメントになります。

APIキーの場合

in でキーを渡す場所(header, query, cookie)、name でキーの名前を指定します。

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
security:
  - ApiKeyAuth: []

JWTの場合

typehttpschemebearerbearerFormatJWT を指定するのが一般的です。

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
security:
  - BearerAuth: []

OAuth 2.0 (Client Credentials) の場合

typeoauth2 を指定し、flows の中に clientCredentials フローを定義します。トークンエンドポイントのURLや、利用可能なスコープも記述できます。

components:
  securitySchemes:
    OAuth2ClientCredentials:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: http://localhost:5003/oauth/token
          scopes:
            profile: Read user profile data
security:
  - OAuth2ClientCredentials: [profile]

8. まとめ:どの認証方式を選ぶべきか?

最後に、どのような場合にどの方式を選択すべきかの指針をまとめます。

  • APIキー認証を選ぶ場合:
    • 開発の初期段階で、とにかく早く動くものが必要な場合。
    • 完全に信頼できるネットワーク内の、ごく小規模な内部API。
    • セキュリティ要件が非常に低い場合。
  • JWT認証を選ぶ場合:
    • マイクロサービスアーキテクチャを採用している場合。
    • サーバー側をステートレスに保ち、スケーラビリティを重視したい場合。
    • 認証と同時に、トークン内に役割(Role)などの情報を持たせたい場合。
  • OAuth 2.0を選ぶ場合:
    • APIを外部のサードパーティに公開する場合。
    • クライアントごとに「読み取り専用」「書き込み可能」といった細かい権限(スコープ)を制御したい場合。
    • 業界標準に準拠し、堅牢で信頼性の高い認証基盤を構築したい場合。

サーバー間認証は、APIの信頼性と安全性を支える重要な基盤です。それぞれの方式の特性を正しく理解し、プロジェクトの要件に最適なものを選んで、セキュアなシステムを構築しましょう。

タイトルとURLをコピーしました