マイクロサービスアーキテクチャや外部サービス連携が当たり前になった昨今、「サーバー間(M2M: Machine-to-Machine)のAPIリクエストをいかに安全に認証するか」は、すべての開発者にとって避けては通れない課題です。
「とりあえずBasic認証で…」と考えてしまうこともありますが、よりセキュアでモダンな方法はいくつも存在します。
この記事では、サーバー間API認証でよく使われる以下の3つの方式を取り上げ、Windows環境のPython (Flask) で実装したサーバーと、Windows環境のcurlを使ったクライアント(バッチファイル)の具体的なサンプルコードをすべてお見せしながら、それぞれの仕組み、メリット・デメリットを解説します。
- APIキー認証: シンプルで最も手軽な方式
- JWT (JSON Web Token) 認証: ステートレスでモダンなアーキテクチャに適した方式
- 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キーと異なり、トークン自体に有効期限やユーザー情報などのデータ(ペイロード)を含めることができます。
仕組み
- ログイン: クライアントは、ID/パスワードなどの認証情報を使ってサーバーの「ログインエンドポイント」にリクエストを送信します。
- トークン発行: サーバーは認証情報が正しければ、ペイロード(ユーザーID、有効期限など)と秘密鍵を使ってJWTを生成し、クライアントに返します。
- APIアクセス: クライアントは、以降のAPIリクエストで
Authorization: Bearer <JWT>という形式でHTTPヘッダーにJWTを付与します。 - トークン検証: サーバーは受け取った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) というフローが用いられます。

仕組み
- クライアント登録: 事前にAPIを提供するサーバー(認可サーバー)にクライアントアプリを登録し、「クライアントID」と「クライアントシークレット」を発行してもらいます。
- トークンリクエスト: クライアントは、クライアントIDとシークレットを使って認可サーバーの「トークンエンドポイント」にリクエストを送信します。
- アクセストークン発行: 認可サーバーはIDとシークレットを検証し、正しければ「アクセストークン」を発行します。
- 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_idとclient_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フローを忠実に再現しています。
- アクセストークンの取得:
/oauth/tokenに対し、client_idとclient_secretをPOSTリクエストのボディに含めて送信します。サーバーはこれを検証し、問題なければアクセストークンを返します。 - 保護されたリソースへのアクセス: 受け取ったアクセストークンを、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) を使用することがセキュリティ上の絶対的な要件とされています。
理由は、通信路上での情報の盗聴や改ざんを防ぐためです。
- クライアントシークレットの保護
/oauth/tokenエンドポイントへのリクエストには、パスワードに等しいclient_secretが含まれます。HTTPSを使わないと、このシークレットが平文のままネットワークを流れてしまい、攻撃者に簡単に盗まれてしまいます。
- アクセストークンの保護
- サーバーから返される
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の場合
type に http、scheme に bearer、bearerFormat に JWT を指定するのが一般的です。
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- BearerAuth: []
OAuth 2.0 (Client Credentials) の場合
type に oauth2 を指定し、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の信頼性と安全性を支える重要な基盤です。それぞれの方式の特性を正しく理解し、プロジェクトの要件に最適なものを選んで、セキュアなシステムを構築しましょう。


