メインコンテンツへスキップ
Demonstrating Proof-of-Possession (DPoP) は、アプリケーション層で非対称暗号方式と (JWT) を使用して、をバインドし、送信者制約を適用するための OAuth 2.0 のフレームワーク拡張 です。DPoP により、アクセストークンを要求し、その秘密鍵を保持しているクライアントアプリケーションだけが、そのトークンを使用できるようになります。これにより、盗まれたトークンの悪用を防止できます。 DPoP では、公開鍵と秘密鍵のペアを使用して、署名付き JSON Web Token (JWT) として DPoP Proof を作成します。DPoP Proof には、次の内容が含まれます。
  • クライアントの公開鍵 (jwk) 。
  • メソッド (htm) と URI (htu) を含む、アクセストークンリクエストを参照するペイロード。
  • クライアントの秘密鍵を使用して生成された署名。
  • リプレイ防止のための一意の ID (jti) 。
  • すべての API リクエストに対する、アクセストークンの base64url エンコード済み SHA-256 ハッシュ (ath) 。
  • 任意: の場合、クライアントアプリケーションが DPoP Proof JWT を最近生成したことを保証するための nonce claim。
クライアントアプリケーションは、アクセストークンリクエストで DPoP Proof JWT を Auth0 の に送信します。Auth0 の認可サーバーは DPoP Proof JWT を検証した後、発行するアクセストークンをクライアントの公開鍵にバインドします。

一般的なユースケース

代表的な DPoP のユースケースをいくつか紹介します。
  • シングルページアプリケーション (SPA) とモバイルアプリケーション: パブリッククライアントである SPA やモバイルアプリケーションには、バックエンドサーバーのように を安全に保存できる、信頼された機密環境がありません。そのため、トークンの窃取に対して脆弱です。DPoP は、アクセストークンをクライアントアプリケーションの公開鍵にバインドして DPoP Proof JWT を生成することで、このセキュリティ上の脆弱性に対処します。クライアントアプリケーションは秘密鍵で DPoP Proof JWT に署名し、認可リクエストに含めて送信します。Auth0 の認可サーバーは DPoP Proof JWT を検証し、有効であれば発行されたアクセストークンをクライアントの公開鍵にバインドします。
  • サードパーティ API との統合: クライアントアプリケーションと統合された AI エージェントが、DPoP Proof JWT を使用してユーザーに代わってサードパーティ API を呼び出す場合、は、そのリクエストが認可されていない第三者ではなく AI エージェントから送信されたものであることを、暗号学的に検証できます。

サポートされているアプリケーションのグラントタイプ

Auth0 は、DPoP の送信者制約で次の アプリケーションのグラントタイプ をサポートしています。
グラントタイプ説明
authorization_code認可コードグラント
client_credentialsクライアントクレデンシャルグラント
passwordリソースオーナーパスワードグラント
refresh_tokenリフレッシュトークングラント
urn:ietf:params:oauth:grant-type:device_codeデバイス認可グラント
http://auth0.com/oauth/grant-type/password-realm特定のレルムを指定できる、リソースオーナーパスワードグラントに類似した拡張グラントを使用します
http://auth0.com/oauth/grant-type/passwordless/otpパスワードレス グラントリクエスト
http://auth0.com/oauth/grant-type/mfa-oob多要素認証 OOB グラントリクエスト
http://auth0.com/oauth/grant-type/mfa-otp多要素認証 OTP グラントリクエスト
http://auth0.com/oauth/grant-type/mfa-recovery-code多要素認証のリカバリーコード グラントリクエスト
urn:ietf:params:oauth:grant-type:token-exchangeCustom Token Exchange グラントリクエスト
urn:okta:params:oauth:grant-type:webauthnWebAuthn グラントリクエスト

仕組み

次のシーケンス図は、Auth0 DPoPフローの概要を示しています。
  1. Auth0 認可サーバーにアクセストークンをリクエストするとき、クライアントアプリケーションは一意の暗号学的キーペアを生成し、公開鍵を使って秘密鍵を保有していることを証明します。
  2. クライアントアプリケーションは DPoP Proof JWT を生成し、それを Auth0 認可サーバーの /token エンドポイントに送信します。
  3. Auth0 認可サーバーは DPoP Proof JWT を検証し、有効であればアクセストークンを発行して、クライアントの公開鍵にバインドします。
  4. Customer API を呼び出す前に、クライアントアプリケーションは新しい DPoP Proof JWT を生成し、トークンに関連付けられた秘密鍵を保有していることを証明します。クライアントアプリケーションは、DPoP Proof JWT と sender-constrained アクセストークンをリソースサーバーに送信します。
  5. リソースサーバーは DPoP Proof JWT を検証し、トークンの正当な所有者、つまり元のクライアントアプリケーションだけが、それを使って保護されたリソースに正常にアクセスできるようにします。リフレッシュトークンからアクセストークンをリクエストするには、クライアントアプリケーションは新しい DPoP Proof JWT を生成し、リフレッシュトークンがクライアントの公開鍵にバインドされていることを保証します。

Auth0 で DPoP を使用してトークンに送信者制約を適用する

次の図は、Auth0 で DPoP を使用してトークンに送信者制約を適用するエンドツーエンドのフローを示しています。
以下のセクションでは、実装用のコードサンプルとともに、Auth0 での DPoP フローを手順に沿って説明します。

前提条件

開始する前に、次のことを確認してください。
  • クライアントアプリケーションとリソースサーバーに対して、送信者制約を設定していること。

ステップ 1: クライアントアプリケーションが DPoP キーペアを生成する

DPoP では、クライアントアプリケーションが非対称暗号のキーペアを生成する必要があります。Auth0 は、ES256 キーで使用されるような楕円曲線の利用をサポートしています。このキーペアはクライアントアプリケーションごとに固有であり、たとえばハードウェアベースのキーストアに安全に保管する必要があります。 クライアントアプリケーションは秘密鍵を安全に保持しつつ、ステップ 2 で「所持の証明」として機能する DPoP Proof JSON Web Token (JWT) に公開鍵を含めます。

ステップ 2: クライアントアプリケーションが DPoP Proof JWT を作成する

Auth0 認可サーバーの /token エンドポイントに DPoP にバインドされたアクセストークンをリクエストする前に、クライアントアプリケーションは DPoP Proof JWT を作成する必要があります。DPoP Proof JWT は、クライアントの秘密鍵で署名された JSON Web Token (JWT) であり、「所持証明」として機能します。 DPoP Proof JWT は、JWT ヘッダーと、トークンリクエストに関連付けられたクレームを含むペイロードで構成されます:

JWT ヘッダークレーム

DPoP Proof JWT クレーム説明
typdpop+jwt に設定します。
algRS256ES256 などの非対称署名アルゴリズムです。
jwkクライアントの公開鍵を表す JSON Web Key (JWK) です。

JWT ペイロードのクレーム

DPoP Proof JWT クレーム説明
jtiリプレイ攻撃を防ぐための JWT の一意の識別子です。
htmDPoP Proof の対象となるリクエストの HTTP メソッドです。たとえば、トークンリクエストでは POST、API 呼び出しでは GET です。
htuDPoP Proof JWT の対象となるリクエストの HTTP URI です。フラグメントとクエリパラメーターは含まれません。たとえば、https://api.example.com/data?param=1#section1https://api.example.com/data になります。
iatJWT の作成時刻を表すタイムスタンプです。
athアクセストークンを使用する API 呼び出しでは、アクセストークンを base64url エンコードした SHA-256 ハッシュです。
noncenonce を必要とするパブリッククライアントでは、サーバーから提供される nonce 値です。
クライアントアプリケーションで DPoP Proof JWT を作成したら、ステップ 1 で生成した秘密鍵を使って DPoP Proof JWT に署名します。 次のコードサンプルは、クライアントアプリケーションで DPoP Proof JWT を作成して署名する方法を示しています。
import { generateKeyPairSync, randomBytes } from 'node:crypto';
import jwt from 'jsonwebtoken';

// DPoP キーペアを生成する
const keyPair = generateKeyPairSync('ec', {
  namedCurve: 'P-256',
});

// トークンリクエスト用の DPoP Proof JWT を構築する
const jti = randomBytes(16).toString('base64url');
const jwk = keyPair.publicKey.export({ format: 'jwk' });
const dpopHeader = jwt.sign({
    jti,
    htm: 'POST',
    htu: 'https://[TENANT]/oauth/token',
    iat: Date.now() / 1000,
  },
  keyPair.privateKey,
  {
    algorithm: 'ES256',
    header: {
      typ: 'dpop+jwt',
      jwk,
    },
  });

ステップ 3: クライアントアプリケーションが DPoP にバインドされたトークンをリクエストする

クライアントアプリケーションが Auth0 の認可サーバーの /token エンドポイントにアクセストークンをリクエストする際、HTTP リクエストヘッダーに DPoP Proof JWT を含めます:
DPoP: {DPoP_proof_JWT_value}
以下は、DPoP Proof JWT を設定した DPoP HTTP ヘッダーを含むアクセストークンリクエストの例です。
POST /oauth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
DPoP: {DPoP Proof JWT}
Authorization: Basic Y2xpZW50MTIzOm15c2VjcmV0
Cache-Control: no-cache
grant_type=client_credentials&client_id=client123
クライアントアプリケーションで DPoP にバインドされたアクセストークンをリクエストする処理を実装するには、次のコードサンプルを使用します。このコードサンプルでは、以下を行います。
  1. 署名済みの DPoP Proof JWT を使用して、DPoP HTTP ヘッダーを設定します。
  2. 署名済みの DPoP Proof JWT を含む DPoP HTTP ヘッダーを、/token エンドポイントへのアクセストークンリクエストとともに送信します。
  3. Auth0 認可サーバーからのレスポンスを処理します。
// /oauth/token エンドポイントにリクエストを送信する
// [...] を実際の grant_type、client_id、テナント URL に置き換えてください
const response = await fetch('https://[TENANT]/oauth/token', {
    method: 'POST',
    body: new URLSearchParams({
      grant_type: '...',
      client_id: '...',
      // その他のボディパラメーターをここに記述
    }),
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      // DPoP ヘッダーを追加する
      dpop: dpopHeader
    }
  });

// Auth0 認可サーバーからのレスポンスを処理する
const result = await response.json();
console.log('Initial token request result:', result);

Public clients

シングルページアプリケーション (SPA) やモバイルアプリなどのパブリッククライアントが DPoP にバインドされたアクセストークンを要求する場合、クライアントシークレットやその他のクライアント認証パラメーターは使用できません。この場合、RFC 9449 に従い、Auth0 では、クライアントアプリケーションが直近に DPoP Proof JWT を生成したことを確認するため、DPoP HTTP ヘッダーに 値を含める必要があります。これは想定どおりの動作です。これにより、認可サーバーは DPoP proof が最新であることを確認し、その proof を使用できる時間枠を制限できます。 パブリッククライアントが /token リクエストを行い、DPoP HTTP ヘッダーに nonce 値を含めない場合、Auth0 は HTTP 400 コードと、次のようなエラーメッセージを返します。
{
  error: 'use_dpop_nonce',
  error_description: '認証サーバーはDPoP証明にnonceを要求します'
}
Auth0 は、レスポンスヘッダーに DPoP-Nonce ヘッダーを含めます。これは、DPoP specification で定義されている標準的な”チャレンジレスポンス”フローに従うものです。DPoP-Nonce ヘッダーの値を使用して DPoP proof を再生成し (Step 2 と同様) 、その値を持つ nonce claim を含めたうえで、リクエストを /token エンドポイントに再送信する必要があります。 次のコードサンプルは、パブリッククライアントから nonce claim を含む /token リクエストを送信し、その後再試行するまでの一連のフローを示しています。
import { generateKeyPairSync, randomBytes } from 'node:crypto';
import jwt from 'jsonwebtoken';

// DPoP キーペアを生成する
const keyPair = generateKeyPairSync('ec', {
  namedCurve: 'P-256',
});

/**
 * DPoP Proof JWT を生成するヘルパー関数。
 * @param {string} method - HTTP メソッド(例: 'POST', 'GET')。
 * @param {string} url - リクエストの完全な URL。
 * @param {string} [nonce] - サーバーから取得するオプションの DPoP-Nonce 値。
 * @param {string} [accessToken] - 'ath' クレームのハッシュ化に使用するオプションのアクセストークン。
 * @returns {string} 署名済みの DPoP Proof JWT。
 */
function generateDPoPHeader(method, url, nonce) {
  const jti = randomBytes(16).toString('base64url');
  const jwk = keyPair.publicKey.export({ format: 'jwk' });
  return jwt.sign({
      jti,
      htm: method,
      htu: url,
      iat: Date.now() / 1000,
      nonce
    },
    keyPair.privateKey,
    {
      algorithm: 'ES256',
      header: {
        typ: 'dpop+jwt',
        jwk,
      },
    });
  }

// 最初は nonce なしでアクセストークンをリクエストする 
async function getTokens(nonce) {
  const response = await fetch('https://[TENANT]/oauth/token', {
      method: 'POST',
      body: new URLSearchParams({
        grant_type: '...',
        client_id: '...',
        // その他のボディパラメーターをここに記述
      }),
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        dpop: generateDPoPHeader('POST', 'https://[TENANT]/oauth/token', nonce),
      }
    });

  const result = await response.json();
  return { response, result };
}

// 初回のトークンリクエスト時は nonce を持っていない
let { response, result } = await getTokens(); 
console.log('Initial token request result:', result);

if (response.status === 400 && result.error === 'use_dpop_nonce') {
  const nonce = response.headers.get('dpop-nonce');

  console.log('Received nonce:', nonce);

  // nonce を使用して再試行する
  ({ response, result } = await getTokens(nonce)); 

  console.log('Tokens received:', result);
}

ステップ 4: Auth0 認可サーバーが DPoP Proof JWT を検証する

Auth0 認可サーバーはトークンリクエストを受信すると、次の処理を行います。
  • DPoP Proof JWT、その公開鍵、署名を抽出します。
  • 提供された公開鍵を使用して署名を検証します。
  • htmhtujti,iat の各クレームを検証します。
  • 有効な場合は、アクセストークンを発行します。Auth0 認可サーバーは、確認用クレーム cnf をアクセストークンに含めます。cnf クレームには、DPoP Proof JWT から取得した公開鍵のサムプリント (ハッシュ値) が含まれます。これをアクセストークンに含めることで、Auth0 認可サーバーはアクセストークンをその特定の公開鍵に紐付けます。つまり、アクセストークンに「sender constraint」を適用します。
  • トークンレスポンスでは、Authorization ヘッダー内の token_typeBearer ではなく DPoP に設定します。従来、アクセストークンを Authorization ヘッダーで渡す場合は Bearer に設定されます。しかし、ここでは DPoP を使用して公開鍵に紐付けられたアクセストークンを渡すため、代わりに DPoP に設定されます。
  • その後、Auth0 認可サーバーは DPoP sender-constrained アクセストークンをクライアントアプリケーションに発行します。

ステップ 5: クライアントアプリケーションが DPoP にバインドされたトークンと DPoP Proof JWT を使用して API を呼び出す

DPoP を強制するリソースサーバーに対するすべての API 呼び出しで、クライアントアプリケーションは、DPoP にバインドされたアクセストークンと新しい DPoP Proof JWT の両方を提示する必要があります。 DPoP は、すべての API リクエストで DPoP Proof JWT を要求することで、秘密鍵を保持するクライアントアプリケーションだけがアクセストークンを使用できるようにします。 新しい API リクエストごとに、クライアントアプリケーションは次の操作を行います。
  1. 次の Claims を含む新しい DPoP Proof JWT を生成します。
  • htm claim は、GETPOST などの API リクエストの HTTP メソッドです。
  • htu claim は、API リクエストの URI です。
  • ath claim は、ステップ 3 で受け取った DPoP にバインドされたアクセストークンの、base64url エンコードされた SHA-256 ハッシュです。
  1. クライアントの秘密鍵を使用して、新しい DPoP Proof JWT に暗号学的に署名します。
  2. DPoP 認証スキームを使用して、DPoP にバインドされたアクセストークンを Authorization ヘッダーに含めます。
// DPoP スキームは認可サーバーから受け取った token_type に合わせます
Authorization: DPoP {access_token}
  1. 新たに生成した DPoP Proof JWT を DPoP HTTP ヘッダーに含めます:
DPoP: {new_dpop_proof_jwt}
DPoP HTTP ヘッダーには、追加の ath クレームを含める必要があります。ath クレームは、発行されたアクセストークンの SHA256 ハッシュを base64url エンコードしたものです。 リソースサーバーは、次の処理を行います。
  • API リクエストを受信し、アクセストークン、DPoP JWT プルーフ、公開鍵、署名を抽出します。
  • jwk ヘッダー内の公開鍵を使用して、DPoP Proof JWT の署名を検証します。
  • htmhtujtiiatath の各クレームを検証します。
  • DPoP Proof JWT の jwk ヘッダーで示された公開鍵が、アクセストークン内の cnf.jkt クレームによってそのアクセストークンに関連付けられた公開鍵と一致することを検証します。
すべての検証に成功した場合、リソースサーバーはリクエストを認可します。そうでない場合は、リクエストを拒否し、アクセスを拒否します。 次のコードサンプルは、DPoP を使用して Auth0 からアクセストークンを取得し、その後、DPoP にバインドされたアクセストークンを使用して /userinfo エンドポイントを呼び出します。
import { generateKeyPairSync, randomBytes, createHash } from 'node:crypto';
import jwt from 'jsonwebtoken';

const keyPair = generateKeyPairSync('ec', {
  namedCurve: 'P-256',
});

function hashToken(token) {
  return createHash('sha256').update(token).digest('base64url');
}

function generateDPoPHeader(method, url, nonce, accessToken) {
  const jti = randomBytes(16).toString('base64url');
  const jwk = keyPair.publicKey.export({ format: 'jwk' });
  return jwt.sign({
      jti,
      htm: method,
      htu: url,
      iat: Date.now() / 1000,
      nonce,

      // オプションで、アクセストークンのハッシュを含む `ath` クレームを含める
      ...(accessToken ? { ath: hashToken(accessToken) } : {}),
    },
    keyPair.privateKey,
    {
      algorithm: 'ES256',
      header: {
        typ: 'dpop+jwt',
        jwk,
      },
    });
  }

async function getTokens(nonce) {
  const response = await fetch('https://[TENANT]/oauth/token', {
      method: 'POST',
      body: new URLSearchParams({
        grant_type: '...',
        client_id: '...',
        // その他のボディパラメーターをここに記述
      }),
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        dpop: generateDPoPHeader('POST', 'https://test1.local.dev.auth0.com/oauth/token', nonce),
      }
    });

  const result = await response.json();
  return { response, result };
}

// 初回実行時はnonceがない
let { response, result } = await getTokens(); 
console.log('Initial token request result:', result);

if (response.status === 400 && result.error === 'use_dpop_nonce') {
  const nonce = response.headers.get('dpop-nonce');
  console.log('Received nonce:', nonce);
  ({ response, result } = await getTokens(nonce)); // nonceを使ってリトライ
  console.log('Tokens received:', result);
}

// DPoPを使って /userinfo を呼び出す
const userInfoResponse = await fetch('https://[TENANT]/userinfo', {
  method: 'GET',
  headers: {
    // DPoP認可スキームを使ってアクセストークンを渡す
    Authorization: `DPoP ${result.access_token}`,

    // DPoPヘッダーを含める(今回はアクセストークンのハッシュ付き)
    dpop: generateDPoPHeader('GET', 'https://[TENANT]/userinfo', nonce, result.access_token),
  },
});

console.log('User info response status:', userInfoResponse.status);
console.log('User info result:', await userInfoResponse.json());

ステップ 6: DPoP を使用したトークンの更新を処理する

DPoP にバインドされたアクセストークンの有効期限が切れた場合は、を使用して新しいアクセストークンを取得できます。リフレッシュトークンリクエストでは、元のトークンリクエストで使用したものと同じキーペアを使って生成した DPoP Proof JWT が必要です。 以下では、Auth0 における DPoP を使用したリフレッシュトークンフローについて説明します。 クライアントアプリケーションは次の処理を行います。
  • Auth0 認可サーバーの /token エンドポイントにリフレッシュトークンリクエストを送信します。
  • リフレッシュトークンリクエスト用の DPoP Proof JWT を生成します (ステップ 2 と同様ですが、htmPOSThtuの URI です) 。
  • DPoP HTTP ヘッダーに DPoP Proof JWT を含めます。
Auth0 認可サーバーは次の処理を行います。
  • DPoP Proof JWT を検証し (ステップ 4 と同様) 、新しい DPoP にバインドされたアクセストークンを発行します。

重要な考慮事項

クライアントアプリケーションに DPoP を実装する際は、次の点を考慮してください。
  • 秘密鍵のセキュリティ: DPoP 実装のセキュリティはクライアントの秘密鍵の保護に依存するため、不正アクセスから確実に保護する必要があります。秘密鍵は、ハードウェアに裏打ちされた保存領域で生成および保管し、エクスポート不可に設定する必要があります。
  • リプレイ保護 (jti** および dpop-nonce):** DPoP Proof JWT の jti クレームは、/userinfo エンドポイントなどの保護対象リソースに対するリプレイ攻撃の防止に役立ちます。現在、Auth0 認可サーバーは /userinfo エンドポイントでの jti の再利用を確認しません。Auth0 認可サーバーはレスポンスで DPoP-Nonce HTTP ヘッダーを返します。パブリッククライアントは、リプレイ保護を強化するため、後続の DPoP Proof JWT にこれを nonce クレームとして含める必要があります。
  • レート制限: DPoP のチャレンジレスポンスフローでは、最初のリクエストの後に、サーバーから提供された nonce を付けて再試行が必要になる場合があるため、各やり取りは実質的に Auth0 テナントのレート制限 に対して 2 リクエスト分としてカウントされます。アプリケーションのリクエスト量がこのオーバーヘッドを考慮していることを確認してください。
  • エラー処理: invalid_dpop_proofuse_dpop_nonce など、Auth0 認可サーバーまたはリソースサーバーから返される DPoP 固有のエラーを処理するロジックは、お客様側で実装する必要があります。
  • クライアントの種類: クライアントシークレットを安全に保存できない、シングルページアプリケーション (SPA) やモバイルアプリなどのパブリッククライアントでは DPoP を使用してください。では、クライアントシークレットを持つバックエンドサービスなどに対して DPoP は追加のセキュリティ層となりますが、すでに他の sender-constraining の仕組みがあります。
  • パフォーマンス: API 呼び出しごとに DPoP Proof JWT を生成して署名するとわずかなオーバーヘッドが生じるため、クライアントアプリケーションの暗号処理が効率的であることを確認してください。
  • 鍵のローテーション: セキュリティを強化するために、DPoP キーペアをローテーションする戦略を実装してください。同じセッションでは同じキーペアを使用するようにしてください。
  • 永続化: セッションを維持し、DPoP にバインドされたアクセストークンを再利用する必要があるクライアントアプリケーション (長期間動作する SPA など) では、最初に生成したキーペアをアプリケーションの再読み込み後も安全に保存し、再取得できるようにしてください。新しいキーペアが生成されたり、別のキーペアが使用されたりすると、DPoP にバインドされたアクセストークンは無効になります。これは、そのトークンが元のキーペアの公開鍵に暗号学的に結び付けられているためです。たとえば、ブラウザーの IndexedDB やモバイルアプリのセキュアストレージにキーペアを保存できます。

詳細情報