MCPでOAuth認証を実装する方法|アクセストークンを安全に扱う設計

MCPでOAuth認証を実装する方法|アクセストークンを安全に扱う設計 AI開発

リモートMCPサーバーをインターネットや社内ネットワークへ公開する場合、誰でもtools/callを実行できる状態にはしたくありません。そこで利用できるのがOAuthです。

MCPでは、Streamable HTTPで公開するMCPサーバーをOAuthのResource Serverとして保護できます。ここで重要なのは、MCPサーバー自身がユーザーのパスワードを受け取り、ログイン画面を表示し、アクセストークンを発行する必要はないという点です。

現在のMCP Python SDKでも、Streamable HTTPのMCPサーバーはOAuth 2.1 Resource Serverとして動作し、クライアントから送られてきたBearer Tokenを検証する役割として説明されています。認可サーバーがユーザーをログインさせてTokenを発行し、MCPクライアントが取得したTokenをAuthorization: Bearer ...としてMCPサーバーへ送信します。概念的には次の構成です。

OAuthの基本構成
ユーザー
↓
MCP Client
↓
Authorization Server
↓
Access Tokenを取得
↓
Authorization: Bearer <access_token>
↓
MCP Server
↓
Tokenを検証
↓
tools/call

ただし、単に「JWTの署名が正しい」ことを確認するだけでは十分ではありません。そのTokenが本当にこのMCPサーバー向けに発行されたものなのか、必要なScopeを持っているのか、有効期限が切れていないのかを確認する必要があります。

この記事では、MCPでOAuth認証を実装する基本構成と、アクセストークンを安全に扱うためのAudience、Resource、Scope、Token保存、ログ、Refresh Token、外部API連携の考え方を解説します。OAuthの認可フロー全体やClient ID Metadata Documentの形式、Token Passthrough禁止といった基礎はClaude Code + MCP セキュリティ設計完全ガイドで解説しているため、この記事ではアクセストークンをどう安全に実装・運用するかにしぼります。HTTPSやReverse Proxyなどリモート公開のインフラ面はMCPサーバーをリモート公開する方法を参照してください。

スポンサーリンク
  1. MCPサーバーはOAuthのResource Serverになる
  2. OAuth認証はstdioではなくStreamable HTTPで利用する
  3. MCPクライアントはBearer Tokenを送信する
  4. まずTokenVerifierを実装する
  5. TokenVerifierとAuthSettingsはセットで設定する
  6. Protected Resource MetadataはSDKが公開できる
  7. JWTなら署名だけでなくissを確認する
  8. 最も重要なのはAudienceを確認すること
  9. Auth0やEntra独自のAudienceを使う場合
  10. Scopeで「何ができるか」を制限する
  11. ToolごとにScopeを確認する
  12. 認証と認可は別にする
  13. Tokenがない場合はTool Errorではなく401を返す
  14. Access Tokenをデータベースへ無条件に保存しない
  15. Access Tokenをログへ出さない
  16. Access TokenをTool Resultへ返さない
  17. Refresh TokenをMCP Serverへ送らない
  18. Client側でTokenを保存するなら平文ファイルを避ける
  19. Access Tokenは短命にする
  20. MCPのTokenを外部APIへそのまま転送しない
  21. 外部サービス用OAuth Tokenは別に管理する
  22. 外部OAuthにはURL Mode Elicitationも利用できる
  23. Protected Resource MetadataとAuthorization Server Metadataを分ける
  24. 401と403を使い分ける
  25. 最初からAdmin Scopeを全部付与しない
  26. Token検証結果をRequest単位で扱う
  27. 2026-07-28ではIssuer検証がより重要になった
  28. Dynamic Client Registrationは非推奨になった
  29. OAuth SecretをMCP Toolの引数にしない
  30. Tokenを.envに固定するだけでは本番OAuthにならない
  31. Tokenの失効も考える
  32. OAuthを導入してもHTTPSは必須と考える
  33. Access Tokenの安全な扱いは「LLMから遠ざける」が基本
  34. MCPでOAuth認証を実装する際によくある質問
  35. MCPのOAuthはToken発行よりToken検証から考える

MCPサーバーはOAuthのResource Serverになる

OAuthでは、認証に関係する役割が複数あります。MCP Server側から見ると重要なのは、

OAuthの役割
Authorization Server
MCP Client
Resource Server

の関係です。Authorization Serverは、ユーザーを認証してAccess Tokenを発行します。MCP ClientはTokenを取得します。MCP ServerはTokenを受け取り、検証します。

MCP Python SDKの現行ドキュメントでも、MCPサーバーはResource Serverであり、ユーザーをログインさせたりTokenを発行したりせず、各HTTP RequestのAuthorization Headerを検証する役割とされています。つまりMCPサーバーへ/login/password・ユーザーDB・Token発行処理をすべて実装する必要はありません。既存のAuth0、Microsoft Entra ID、Keycloak、社内IdP、独自OAuth Authorization Serverなどを利用できます。

OAuth認証はstdioではなくStreamable HTTPで利用する

MCPのOAuth認証はHTTPレイヤーで行われます。ClientからはAuthorization: Bearer eyJ...のようなHTTP Headerが送られます。そのためOAuth認証を利用するのは基本的にStreamable HTTPです。stdioとStreamable HTTPの違い自体はMCPのstdioとStreamable HTTPの違いで解説しています。

MCP Python SDKでも、AuthorizationはHTTP Transportに存在する仕組みであり、stdioではAuthorization Headerが存在しないためtoken_verifierは呼び出されないと説明されています。ローカルstdio MCP Serverなら、OSユーザー・Process・Filesystem Permission・Claude CodeのPermission・SandboxなどがSecurity Boundaryになります。一方、Remote MCPではOAuthなどのHTTP認証が必要になります。

MCPクライアントはBearer Tokenを送信する

認証済みMCP ClientはRequestごとにAccess Tokenを送信します。概念的には次の形です。

リクエスト例
POST /mcp HTTP/1.1
Host: mcp.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Content-Type: application/json

MCP ServerはこのTokenを検証してからJSON-RPC Requestを処理します。Tokenが存在しない、無効、期限切れなどの場合は、HTTP/1.1 401 Unauthorizedを返します。MCP Python SDKでは、認証なしまたはToken Verifierが無効と判断したRequestはMCP Requestを解析する前に拒否され、WWW-Authenticate HeaderからProtected Resource Metadataへ誘導されます。つまりtools/callの内部で「ログインしてください」と返すより前に、HTTPレイヤーで認証を処理します。

まずTokenVerifierを実装する

MCP Python SDKでは、受け取ったAccess Tokenが有効かどうかをTokenVerifierで判断できます。基本構造は次のようになります。

server.py
from pydantic import AnyHttpUrl

from mcp.server import MCPServer
from mcp.server.auth.provider import AccessToken, TokenVerifier
from mcp.server.auth.settings import AuthSettings


RESOURCE = "https://mcp.example.com/mcp"


class MyTokenVerifier(TokenVerifier):
    async def verify_token(
        self,
        token: str
    ) -> AccessToken | None:
        result = await verify_access_token(token)

        if not result.valid:
            return None

        return AccessToken(
            token=token,
            client_id=result.client_id,
            scopes=result.scopes,
            resource=result.resource,
        )


mcp = MCPServer(
    "Example MCP",
    token_verifier=MyTokenVerifier(),
    auth=AuthSettings(
        issuer_url=AnyHttpUrl(
            "https://auth.example.com"
        ),
        resource_server_url=AnyHttpUrl(
            RESOURCE
        ),
        required_scopes=[
            "mcp:read"
        ],
        validate_token_resource=True,
    ),
)

TokenVerifierAuthorization Headerから取り出された生のTokenを受け取り、有効ならAccessToken、無効ならNoneを返します。本番実装では、JWTの署名を検証するか、Authorization ServerのToken Introspection Endpointへ問い合わせる形が一般的です。MCP Python SDKも、実際のVerifierではJWT Signatureを検証するかRFC 7662のToken Introspectionを利用する構成を案内しています。

TokenVerifierとAuthSettingsはセットで設定する

Python SDKでは、token_verifier=MyTokenVerifier()だけ設定すればよいわけではありません。AuthSettingsも設定します。現在のMCP Python SDKではtoken_verifierauthはセットで利用する必要があり、一方だけを渡すとServerがRequestを処理する前にエラーになります。

issuer_urlにはTokenを発行するAuthorization Serverを指定します。resource_server_urlにはClientが実際に接続するMCP Serverの公開URLを指定します。ここをhttp://127.0.0.1:8000/mcpのような内部URLにしたまま本番へDeployしないよう注意してください。公開URLがhttps://mcp.example.com/mcpなら、resource_server_urlも同じPublic URLにします。

Protected Resource MetadataはSDKが公開できる

MCP Clientは最初から「どのAuthorization ServerからTokenを取ればよいか」を知っているとは限りません。そこでMCP ServerはProtected Resource Metadataを公開します。MCP Python SDKでAuthSettingsを設定すると、たとえば次のようなEndpointが自動的に用意されます。

公開されるEndpoint
/.well-known/oauth-protected-resource/mcp

ここから概念的には次の情報を取得できます。

Protected Resource Metadataの例
{
  "resource": "https://mcp.example.com/mcp",
  "authorization_servers": [
    "https://auth.example.com/"
  ],
  "scopes_supported": [
    "mcp:read"
  ],
  "bearer_methods_supported": [
    "header"
  ]
}

現在のPython SDKではRFC 9728 Protected Resource MetadataをAuthSettingsから生成でき、認証されていないRequestにはWWW-Authenticate HeaderからMetadata URLが通知されます。その結果、MCP Clientでは概念的に次のFlowを構築できます。

Client側のFlow
/mcpへRequest
↓
401 Unauthorized
↓
WWW-Authenticateを確認
↓
Protected Resource Metadataを取得
↓
Authorization Serverを発見
↓
OAuth認証
↓
Access Token取得
↓
/mcpへ再Request

JWTなら署名だけでなくissを確認する

JWT Access Tokenを使っている場合、「Signatureが正しい」だけで認証を完了させてはいけません。少なくともissaudexpscopeなどを確認する必要があります。

issはTokenを発行したIssuerを示します。自分が信頼しているAuthorization Server以外が発行したTokenを受け付けないようにします。2026-07-28 MCP仕様ではAuthorization周辺がさらに強化され、Authorization ServerがRFC 9207のissを返し、Client側でもIssuerを検証することが求められるようになりました。Authorization Server間でCredentialを使い回さないための変更も導入されています。

OAuth実装では「Tokenが暗号学的に正しい」だけではなく、「信頼しているIssuerが発行したToken」であることを確認します。

最も重要なのはAudienceを確認すること

OAuth実装で特に重要なのがAudienceです。たとえば同じAuthorization Serverが、

向けのAccess Tokenを発行しているとします。署名はすべて同じIssuerによって行われる可能性があります。そのため「署名が正しい」だけでは「MCP Server向けのToken」とは限りません。別API向けTokenをMCP Serverでも利用できてしまう状態は避ける必要があります。

Python SDKでは、validate_token_resource=Trueを設定すると、AccessToken.resourceresource_server_urlを比較し、このMCP Server向けではないTokenを拒否できます。MCP Clientが接続しているPublic URLとresource_server_urlを一致させることも重要です。

Auth0やEntra独自のAudienceを使う場合

Authorization Serverによっては、MCP URLそのものではなく独自のAudience Identifierを利用します。たとえばAuth0ならhttps://example-apiのようなAPI Identifierを使うことがあります。Microsoft Entra IDならApplication IDなどがAudienceとして使われる構成があります。

この場合、validate_token_resource=Trueによる単純比較ではなく、自分のTokenVerifierでJWTのaudを確認します。現在のPython SDKでも、Authorization Server独自のAudience Identifierを使う場合はvalidate_token_resourceに任せるのではなく、Verifier側でaudを検証し、このServer向けではないTokenならNoneを返す方式が案内されています。重要なのは、Audience Validationそのものを省略しないことです。

Scopeで「何ができるか」を制限する

Audienceが「どのServer向けのTokenか」を表すのに対し、Scopeは「そのServerで何ができるか」を表します。たとえばnotes:readnotes:writenotes:deleteのように分離できます。読み取りだけ許可されたTokenならnotes:readだけを持たせます。

MCP Python SDKのAuthSettingsではrequired_scopes=["notes:read"]を指定できます。この場合、必要なScopeを持っていないTokenを拒否できます。ただしServer全体へ同じScopeを要求するだけでは、細かなTool Authorizationには足りない場合があります。

ToolごとにScopeを確認する

たとえば次のToolがあるとします。

Scopeを分けたいTool例
list_notes
create_note
delete_note

list_notesnotes:readで利用できるようにします。create_notenotes:writeを要求します。delete_notenotes:deleteを要求します。MCP Python SDKではHandler内部から現在のRequestの認証情報を取得できます。

server.py
from mcp.server.auth.middleware.auth_context import (
    get_access_token,
)


@mcp.tool()
def list_notes() -> list[str]:
    token = get_access_token()

    if token is None:
        raise PermissionError(
            "authentication required"
        )

    if "notes:read" not in token.scopes:
        raise PermissionError(
            "notes:read scope required"
        )

    return [
        "note-a",
        "note-b",
    ]

get_access_token()は、そのRequestでToken Verifierが返したAccessTokenを取得します。Client ID、Scope、Subjectなどを利用してTool単位のAuthorizationを実装できます。

認証と認可は別にする

OAuthを導入すると、「Tokenが有効」だけで安心してしまうことがあります。しかしToken Validationは主に認証の入口です。実際のTool実行では、「このユーザーがこのResourceにこの操作をしてよいか」も確認する必要があります。

たとえばuser_id=100tenant_id=Aのユーザーが、tenant_id=Bの顧客情報を取得してはいけません。これはOAuth Tokenの署名検証だけでは防げません。Tool内部でもSubject・Tenant・Role・Scope・Resource Ownerなどを確認します。特にMulti-tenant MCPでは、「AIにTenant IDを間違えないよう指示する」だけではなく、Server側で強制してください。

Tokenがない場合はTool Errorではなく401を返す

Protected MCP Serverで認証されていない場合、Tool ResultのErrorとして「Please login」のような内容を返したくなるかもしれません。しかしOAuth認証はTool LayerではなくHTTP Layerで処理するのが基本です。

Protected Resourceへ有効なBearer Tokenなしでアクセスされた場合は、HTTP/1.1 401 Unauthorizedを返します。MCP Appsの公式Authorizationドキュメントでも、Protected Toolの認証はHTTP Boundaryで行い、未認証RequestはTool Level ErrorではなくHTTP 401として返す必要があると説明されています。これによりMCP HostがOAuth Flowを開始できます。

Access Tokenをデータベースへ無条件に保存しない

MCP Serverへ送られてくるBearer Tokenを「念のため保存しておこう」とDatabaseへ書き込む必要は通常ありません。Resource Serverの役割はRequestごとにTokenを検証することです。Token Verifierからclient_idsubjectscoperesourceexpires_atなど必要な認証情報を取り出して処理すれば十分です。

生のAccess Tokenそのものを長期保存すると、Database Leak時にTokenを悪用される範囲が広がります。特別な理由がない限り、Request受信→Token検証→Claimsを取得→Request処理終了→生Tokenは保持しない、という設計が分かりやすくなります。

Access Tokenをログへ出さない

特に危険なのがLoggingです。Authorization Headerをそのままログへ出す実装は避けます。Logは、Cloud Logging・APM・Error Tracking・Backup・開発者PCなど多くの場所へ複製される可能性があります。

Tokenを丸ごと保存するのではなく、client_idsubjectscoperequest_idtool_namestatusなど、調査に必要な情報だけ記録します。どうしてもTokenを識別する必要があるなら、生Tokenではなく不可逆Hashなどを利用して相関を取る設計を検討します。

Access TokenをTool Resultへ返さない

さらに避けたいのが、認証確認用のTool内で生のAccess Tokenをそのまま返す実装です。これではAccess TokenがLLM Contextへ入り込む可能性があります。TokenがConversation HistoryやTraceへ残れば、Security上の範囲が大きくなります。

認証確認用Toolを作るなら、次のようにします。

server.py
@mcp.tool()
def whoami() -> dict:
    token = get_access_token()

    if token is None:
        return {
            "authenticated": False
        }

    return {
        "authenticated": True,
        "client_id": token.client_id,
        "scopes": token.scopes,
    }

Python SDK公式例でもget_access_token()からClient IDとScopeを取得する例はありますが、生Token自体をモデルへ返す必要はありません。

Refresh TokenをMCP Serverへ送らない

OAuthのAccess Tokenは比較的短い有効期限にすることがあります。期限切れになったらRefresh Tokenを使って新しいAccess Tokenを取得できます。ここで重要なのは、通常のMCP Resource ServerがRefresh Tokenを必要としないことです。概念的には、

Refresh Tokenの流れ
MCP Client
↓
Refresh Token
↓
Authorization Server
↓
新しいAccess Token
↓
MCP Server

となります。MCP Serverへ送るのはAccess Tokenです。Refresh TokenはAuthorization ServerとのToken更新に利用する秘密情報なので、MCP Tool Argument・MCP Serverへの通常Request・Conversationへ載せる必要はありません。現在のMCP OAuth Client実装でも、Refresh Tokenが保存されている場合はClient側がToken Endpointで更新し、その後MCP Requestを再試行する設計になっています。

Client側でTokenを保存するなら平文ファイルを避ける

自作MCP Clientを作る場合は、Access TokenやRefresh Tokenを永続化したくなることがあります。開発中ならJSON Fileへ保存する実装でも動きます。しかし本番では、oauth.jsontokens.json.envへRefresh Tokenをそのまま保存する設計は避けたほうが安全です。

Desktop ApplicationならOSのCredential StoreやKeychainを利用できます。Server ApplicationならSecret Manager、暗号化Database、KMSなどを利用します。特にRefresh TokenはAccess Tokenより長期間利用できることがあるため、より強い保護が必要です。Repository内へToken Fileを置くことは避けてください。.gitignoreへ追加していても、File System上では読み取り可能だからです。

Access Tokenは短命にする

Token Leakの影響を小さくするにはAccess Tokenを短命にする考え方が有効です。Access Tokenが永続的に使える設計では、一度漏えいすると長期間悪用される可能性があります。短いExpirationを設定し、必要に応じてClientがRefresh Tokenを利用して新しいAccess Tokenを取得する構成にします。

Resource Server側でもexpを必ず確認します。Python SDKのBearer Auth Middlewareでも、AccessToken.expires_atが現在時刻より過去ならTokenを無効として扱います。

MCPのTokenを外部APIへそのまま転送しない

これは非常に重要です。たとえばMCP Serverに「GitHubを検索するTool」があるとします。ClientからMCP Serverへ「MCP用Access Token」が送られてきます。そのTokenをそのままAuthorization: Bearer <MCPのToken>としてGitHub APIへ送ってはいけません。

MCP Server向けに発行されたTokenとGitHub向けTokenは別物です。このようなToken PassthroughはAudience Boundaryを壊します。この問題自体はMCP仕様で明示的な禁止事項として扱われており、詳細はClaude Code + MCP セキュリティ設計完全ガイドで解説しています。外部APIへ接続する場合は、「MCP Client → MCP Server用Token」と「MCP Server → GitHub用Token」を分離してください。

外部サービス用OAuth Tokenは別に管理する

たとえばMCP ServerからGoogle Driveへ接続する場合、「MCP Server自身へのOAuth」と「Google DriveへのOAuth」は別の認証です。概念的には、

外部サービス連携の構成
MCP Client
↓
MCP Access Token
↓
MCP Server
↓
Google Access Token
↓
Google API

となります。MCP Clientから受け取ったTokenをGoogleへ転送しません。Google用TokenはMCP Server側のCredential Storeでユーザーごとに管理します。これにより「MCP ResourceのAudience」と「Google APIのAudience」を明確に分離できます。

外部OAuthにはURL Mode Elicitationも利用できる

MCP Serverが外部サービスへのCredentialを必要とする場合、「GitHub Tokenを入力してください」とTool Argumentで秘密情報をLLMへ渡す設計は避けたいところです。

MCPでは、ユーザーを安全なBrowser上のOAuth Flowへ誘導する方法も用意されています。2025-11-25仕様で導入されたURL Mode Elicitationでは、MCP Clientを経由してAPI KeyやOAuth Credentialを入力させるのではなく、外部Browser上でCredential Acquisitionを行い、Server側が直接Tokenを管理できます。MCP公式Blogでも、これによって外部OAuthをToken Passthroughなしで実装できると説明されています。

つまり「LLMにCredentialを渡す」のではなく、「ユーザー→Browser→外部OAuth→MCP ServerがCredentialを保存」という構成にできます。

Protected Resource MetadataとAuthorization Server Metadataを分ける

OAuth Discoveryでは、「MCP ServerがどのAuthorization Serverに守られているか」を表すMetadataと、「Authorization ServerがどのEndpointを提供しているか」を表すMetadataがあります。MCP Server側ではProtected Resource Metadataを公開します。Authorization Server側ではAuthorization Server Metadataを公開します。

MCP ClientはまずMCP Server側のMetadataを見てAuthorization Serverを特定し、その後Authorization ServerのMetadataからauthorization_endpointtoken_endpointscopesなどを取得します。現在のMCP SDKでも、401のWWW-AuthenticateからProtected Resource Metadataを発見し、その情報からAuthorization Serverを探索するFlowが実装されています。

401と403を使い分ける

認証関連ではHTTP Status Codeも重要です。Tokenがない、Tokenが無効、期限切れなら401 Unauthorizedです。Token自体は有効だが、必要なScopeがない場合は403 Forbiddeninsufficient_scopeを使えるケースがあります。

現在のMCP OAuth Client実装では、403WWW-Authenticateerror="insufficient_scope"が含まれる場合、必要なScopeを追加して再認可するStep-up Authorizationにも対応しています。たとえばnotes:readしか持っていないClientがdelete_noteを実行しようとした場合です。いきなり全ユーザーへ強いScopeを要求するのではなく、必要になった時点で追加権限を要求する設計も可能です。

最初からAdmin Scopeを全部付与しない

OAuthを導入しても、「全部のToolが必要だからadminを付ける」という設計ではLeast Privilegeになりません。たとえばsearchだけ利用するClientへ、deletedeploybillingadminまで許可する必要はありません。

Scopeを細分化し、readwritedeleteadminなど、必要な権限だけ要求します。AI Agentは自動的にToolを選択するため、意図しないTool Callの影響を抑える意味でも権限の最小化は重要です。

Token検証結果をRequest単位で扱う

2026-07-28のMCP CoreはStatelessになっています。従来のProtocol SessionへUser Identityを固定しておくのではなく、Requestごとに必要な情報を扱う方向になっています。各Requestが自己完結し、通常のRound-robin Load Balancerへ配送できる設計です。

認証も同様に、「このSessionはAlice」と隠れた状態へ依存するより、Request→Bearer Token検証→Identity取得→Tool実行とするほうが分かりやすくなります。Python SDKのAuth ContextもRequestごとのContextとしてToken情報を保持します。

2026-07-28ではIssuer検証がより重要になった

MCP 2026-07-28ではOAuth周辺に複数のSecurity Hardeningが入りました。Authorization ServerはRFC 9207に沿ってissを返し、ClientはAuthorization CodeをTokenへ交換する前にIssuerを検証します。さらにClient Credentialは、それを発行したIssuerへ結び付けられ、別のAuthorization Serverへ使い回さない設計が導入されています。これはAuthorization Server Mix-upなどを防ぐために重要です。古いMCP OAuth実装をそのままコピーしている場合は、2026-07-28対応SDKへ更新したうえでIssuer Validationを確認してください。

Dynamic Client Registrationは非推奨になった

古いMCP OAuthの記事では、ClientがAuthorization ServerへDynamic Client Registrationを行う例が多くあります。しかし2026-07-28仕様ではDynamic Client Registration、DCRは非推奨となり、Client ID Metadata Documents、CIMDへ移行する方針になりました。

CIMDではClient自身がHTTPS URLでMetadata Documentを公開し、そのURL自体をclient_idとして利用できます。これによりAuthorization ServerごとにClient Registration Requestを繰り返す必要を減らせます。CIMDの具体的なJSON形式はClaude Code + MCP セキュリティ設計完全ガイドにサンプルがあります。現在はBackward CompatibilityのためDCRも残っていますが、新規のOAuth Clientを設計するならCIMD対応を確認する価値があります。

OAuth SecretをMCP Toolの引数にしない

Tool ArgumentへAccess Tokenを直接渡す設計は避けたい実装です。このTokenはLLM Context・Tool Call Log・Trace・Conversation履歴などに残る可能性があります。TokenやPasswordをAIモデルへ入力させる必要はありません。認証はHTTP Authorization LayerまたはBrowser OAuth Flowで行い、Toolには業務上必要な値だけを渡します。

Tokenを.envに固定するだけでは本番OAuthにならない

開発中はMCP_TOKEN=abc123のような固定Tokenで動作確認する方法があります。これはToken Verificationの仕組みを試す目的では便利です。Python SDK公式ドキュメントにもStatic Tokenを利用した簡単なExampleがあります。

しかし本番環境で全ユーザー共通のTokenを長期間使う設計では、誰が利用したか分からない・Scopeを分けられない・個別に失効できない・漏えい時に全利用者へ影響するという問題があります。複数ユーザーへ提供するRemote MCPでは、Authorization Serverからユーザー・Client単位でTokenを発行するほうが管理しやすくなります。

Tokenの失効も考える

JWTをLocal Validationする構成では、署名とExpirationが正しければTokenを有効として扱うことが一般的です。しかし「ユーザーを無効化した」「権限を取り消した」「Tokenを緊急失効した」という変更を即時反映したい場合があります。

この場合は短いAccess Token Lifetimeを使うか、Authorization ServerのToken Introspection Endpointで状態を確認する設計を検討できます。MCP Python SDKの公式Authorizationガイドでも、ProductionのToken VerifierとしてJWT Signature ValidationまたはRFC 7662 Token Introspectionを利用する形が想定されています。どちらを選ぶかは、Latency・失効反映速度・可用性・Token形式・IdPの仕様によって変わります。

OAuthを導入してもHTTPSは必須と考える

Bearer Tokenは、その値を持っている主体が利用できるCredentialです。そのためHTTP平文通信で送信するのは避けます。OAuth = 誰がアクセスできるか、HTTPS = 通信内容を保護するという別の役割です。HTTPS・Reverse Proxy・Host Allowlistなどリモート公開のインフラ構成はMCPサーバーをリモート公開する方法で詳しく解説しています。

Access Tokenの安全な扱いは「LLMから遠ざける」が基本

MCPでOAuthを安全に実装するとき、最も分かりやすい考え方は「TokenをできるだけLLMのContextへ入れない」ことです。ClientはOAuth LayerでAccess Tokenを取得し、HTTP HeaderとしてMCP Serverへ送信します。MCP ServerはMiddlewareでTokenを検証します。Tool Handlerには必要なIdentity、Scope、Subjectなどだけを渡します。外部サービスのCredentialが必要なら、それもMCP Server側で別に管理します。

この構成なら、User Prompt・Model Context・Tool Arguments・Tool Resultへ秘密情報が流れ込む範囲を減らせます。

MCPでOAuth認証を実装する際によくある質問

QMCPサーバー自身にログイン画面やユーザーDBを実装する必要がありますか

A必要ありません。MCPサーバーはOAuthのResource Serverとして実装するのが基本です。ユーザーのログインとToken発行はAuth0やEntra ID、社内Identity ProviderなどのAuthorization Serverに任せ、MCPサーバー側はTokenVerifierでAuthorization HeaderのTokenを検証するだけです。

QJWTの署名が正しければ認証は完了ですか

A完了しません。署名が正しいだけでは、そのTokenが本当にこのMCPサーバー向けに発行されたものかは分かりません。iss(発行者)、aud/resource(対象)、exp(期限)、scope(権限)を確認する必要があります。特にAudienceの確認を省略すると、別API向けのTokenをそのまま受け付けてしまう可能性があります。

QAccess Tokenをデータベースやログに保存してもよいですか

A基本的には避けてください。MCPサーバーの役割はRequestごとにTokenを検証することで、生のTokenを長期保存する必要は通常ありません。ログにはclient_idやscope、request_idなど調査に必要な情報だけを残し、Authorizationヘッダーの値そのものは出力しないようにします。

QMCPサーバーが受け取ったTokenを外部API(GitHubやGoogleなど)へそのまま転送してもよいですか

Aしてはいけません。これはToken Passthroughと呼ばれ、MCP仕様で明示的に禁止されているパターンです。MCPサーバー向けのTokenと外部API向けのTokenはAudienceが異なるため、外部APIへ接続する場合はそのAPI用の認証情報を別に取得・管理する必要があります。

QRefresh TokenはMCPサーバーへ送る必要がありますか

A送る必要はありません。MCPサーバーへ送るのはAccess Tokenだけです。Refresh TokenはMCP ClientとAuthorization Serverの間でAccess Tokenを更新するために使う秘密情報であり、MCP Tool ArgumentやMCP Serverへの通常のRequestに載せるものではありません。

Q固定のAPIキー1本でOAuthの代わりにしてもよいですか

A少人数・限定ネットワークでの検証目的なら技術的には可能です。ただし全ユーザー共通で有効期限もScopeもない固定Tokenは、利用者識別や個別失効ができず、漏えい時の影響が全利用者に及びます。複数ユーザーや外部提供を想定するなら、OAuthベースのAccess Tokenを使うほうが安全に運用できます。

MCPのOAuthはToken発行よりToken検証から考える

MCPでOAuth認証を実装するときは、「MCP Serverにログイン機能を作る」ところから始める必要はありません。Streamable HTTP上のMCP ServerはOAuth Resource Serverとして動作し、Clientから届くBearer Tokenを検証します。現在のPython SDKでも、Resource ServerはTokenを発行せず、TokenVerifierを通してRequestごとのTokenを検証する構造です。

実装ではtoken_verifier=MyTokenVerifier()auth=AuthSettings(...)を設定します。JWTを使う場合は署名、Issuer、ExpirationだけでなくAudienceを必ず確認します。MCP Server向けではないTokenを受け付けないことが、Token Passthroughを防ぐうえでも重要です。

必要な操作はScopeで制限し、Tool Handler内ではget_access_token()からCaller Identityを取得してResource単位のAuthorizationを行います。Access TokenはDatabaseやLogへ無条件に保存せず、Tool ArgumentやTool Resultにも載せないようにします。Refresh TokenはMCP ServerではなくOAuth ClientとAuthorization Serverの間で扱い、MCP Serverへ通常Requestとして送信する必要はありません。

またMCP ServerからGitHubやGoogleなど別のAPIへ接続する場合、MCP Server用のAccess Tokenを外部APIへそのまま転送してはいけません。外部サービス向けOAuthは別に実施し、それぞれのAudienceに対応したCredentialを利用します。2026-07-28 MCPではIssuer ValidationやCredential Isolationが強化され、Dynamic Client RegistrationからClient ID Metadata Documentsへ移行する方針も示されています。

MCPのOAuth認証を安全に設計するポイントは、「Tokenを持っているから通す」のではなく、「誰が発行し、どのResource向けで、どのScopeを持ち、現在も有効なのか」をRequestごとに確認することです。さらにTokenそのものをLLMから切り離し、HTTP Authentication LayerとServer-side Credential Storeの中だけで扱う構成にすると、Remote MCPを安全に運用しやすくなります。