メインコンテンツに移動

セッションと権限の管理

プレイヤーとその親が同意フローを完了した後(VPCウィジェットまたはカスタム年齢ゲートのいずれかを通じて)、ゲームはプレイヤーの権限を含むセッションを受け取ります。ただし、権限は時間の経過とともに変化する可能性があります:親がFamily Connectを通じて設定を調整したり、プレイヤーが誕生日を迎えて新しい年齢カテゴリに移動したり、誰かが追加機能へのアクセスをリクエストしたりする可能性があります。

このガイドでは、これらの変更を検出し、適切に対応し、プレイヤーに明確に伝達して、常に現在の機能アクセスを理解できるようにする方法を説明します。

なぜこれが重要か

ゲームが閉じている間(または実行中でも)に権限が変更されると、機能が突然表示されたり消えたりすると、プレイヤーは混乱する可能性があります。明確な伝達がないと、これらの変更はバグのように見える可能性があり、親からの意図的な更新や年齢関連の調整とは見えません。

前提条件

始める前に、以下を確認してください:

  1. 完了した同意フロー:プレイヤーはVPCクイックスタートまたはカスタム年齢ゲートクイックスタートを完了してアクティブなセッションを持っている必要があります
  2. k-IDプロダクトk-ID Compliance Studioプロダクトを作成して設定
  3. APIキーCompliance Studioのプロダクトの開発者設定ページからAPIキーを生成
  4. Webhookエンドポイント(推奨):セッションイベントを受信するためのセキュアなHTTPSエンドポイントを設定。詳細については、Webhooksを参照してください。

セッションとチャレンジの理解

プレイヤーが信頼できる大人からアクセスを許可されると、プロダクトごとに正確に1つのセッションを持ちます。権限が変更されると(親の変更、年齢アップイベント、または権限のアップグレードを介して)、同じセッションIDが新しい権限で更新されます。新しいセッションは作成されません。既存のセッションがプレイヤーのアクセスの現在の状態を反映します。ただし、セッションが取り消され、信頼できる大人が再度同意フローを実行すると、新しいセッションIDを持つ新しいセッションが作成されます。

チャレンジは、親の承認を必要とする同意リクエストです。チャレンジが正常に完了すると(PASS)、セッションが作成されるか、付与された権限で更新されます。逆に、セッションが削除されると、そのプレイヤーのすべての未完了のチャレンジが自動的にFAILに設定されます。チャレンジの詳細については、Challenges概念ガイドを参照してください。

権限が変更される方法

権限の変更は、いくつかの理由で発生する可能性があります:

変更タイプ説明検出方法
親が権限を変更親がFamily Connectを使用して機能を有効または無効にするWebhookまたはセッション比較
プレイヤーが年齢アップ誕生日によりプレイヤーが新しい年齢カテゴリに移動。プレイヤーが親の同意を必要としなくなる年齢に達すると、権限のmanagedByPLAYERに設定され、親の同意なしでプレイヤーが直接制御できるようになりますセッション比較のみ
セッション削除親がFamily Connectを通じてアクセスを取り消し、セッションが削除される(400を返す)Webhookまたはセッション比較(400を返す)

これらのシナリオを理解することで、適切な検出戦略を実装できます。

ステップ1:検出アプローチを選択

権限の変更を検出するには、2つのアプローチがあります。ほとんどの実装では、両方を使用し、Webhookを主要な方法として、セッション比較をフォールバックとして使用する必要があります。

HTTPコールバックを受信できるサーバーがある場合は、Webhookを使用してください。

このアプローチでは、親が権限を変更すると、k-IDがすぐにサーバーに通知します。サーバーは独自の状態を更新し、ゲームは次回の起動時または機能アクセス時に更新された状態を読み取ります。

利点:

  • 変更の検出に遅延のないリアルタイム通知
  • 一定のポーリングよりもリソース使用量が少ない
  • プレイヤーへの積極的な通知を可能にする

仕組み:

アプローチB:再起動時のセッション比較

Webhookがない場合、またはWebhookと併用してフォールバックとして使用する場合は、セッション比較を使用してください。

このアプローチでは、ゲームが最後に知られているセッションをキャッシュし、各再起動時(またはゲームプレイ中に定期的に)k-IDからの現在のセッションと比較します。

利点:

  • Webhook設定が不要
  • 年齢アップの変更をキャッチ(Webhookをトリガーしない)
  • 実装が簡単

仕組み:

ステップ2:Webhookベースの検出を実装

ゲームにサーバーがある場合は、リアルタイムの権限更新のためにWebhookベースの検出を実装します。

サーバー側のAPI呼び出しが必要

すべてのk-ID API呼び出しは、クライアント側のコードではなく、サーバーから行う必要があります。APIキーをゲームクライアントに公開してはいけません。このガイドの例は、サーバー側のコード(Node.jsとExpress)を示しています。ゲームクライアントは独自のサーバーと通信し、そのサーバーがk-ID APIを呼び出します。

Webhookエンドポイントを設定

Compliance Studioで、プロダクトの開発者設定の下にWebhook URLを設定します。エンドポイントが以下を受信できることを確認してください:

Session.ChangePermissions Webhookを処理

k-IDがこのWebhookを発火したら、サーバーの状態を更新して、セッションが変更されたことをフラグ付けします:

app.post("/webhook/k-id", (req, res) => {
const { eventType, data } = req.body;

switch (eventType) {
case "Session.ChangePermissions":
// Flag this session as having changed permissions
markSessionAsChanged(data.id, {
changeType: "permissions_updated",
changedAt: new Date().toISOString()
});
break;

case "Session.Delete":
// Parent revoked access - session has been deleted
// Note: All incomplete challenges for this player are automatically set to FAIL
markSessionAsDeleted(data.id);
break;
}

res.status(200).send("OK");
});

権限を取得して比較

ゲームクライアントがサーバーに接続したら、変更フラグを確認し、k-IDから更新されたセッションを取得し、保存されたバージョンと比較して、何が変更されたかを判断します:

// Server-side endpoint that your game client calls
app.get("/api/check-permissions/:sessionId", async (req, res) => {
const { sessionId } = req.params;

const changeInfo = await getChangeFlag(sessionId);
if (!changeInfo.hasChanged) {
return res.json({ hasChanged: false });
}

const cachedSession = await getStoredSession(sessionId);
const response = await fetch(
`https://game-api.k-id.com/api/v1/session/get?sessionId=${sessionId}`,
{ headers: { "Authorization": `Bearer ${process.env.KID_API_KEY}` } }
);

const { session: newSession } = await response.json();
const changes = comparePermissions(cachedSession, newSession);
await storeSession(sessionId, newSession);
await clearChangeFlag(sessionId);

res.json({
hasChanged: true,
changes: changes,
session: newSession
});
});

comparePermissions関数(ステップ3で示す)は、どの権限が変更されたかを正確に識別し、プレイヤーに特定のメッセージを表示できるようにします。

ステップ3:セッション比較を実装

フォールバック(またはWebhookがない場合は主要な方法)としてセッション比較を実装します。このアプローチは、Webhookをトリガーしない年齢アップの変更を検出するためにも不可欠です。

比較が必要な理由

Session.ChangePermissions Webhookは、権限が変更されたことを通知しますが、何が変更されたかは含まれません。どの特定の権限が有効または無効になったかを判断するには、更新されたセッションを取得し、キャッシュされたバージョンと比較する必要があります。この比較ロジックは、Webhookまたはポーリングのいずれを使用する場合でも必要です。

セッションを保存

セッションを受け取ったら、サーバーに保存します(データベース、キャッシュ、またはプレイヤーに関連付けられたその他の永続ストレージに):

// Server-side session storage
async function storeSession(sessionId, session) {
await db.sessions.upsert({
sessionId: sessionId,
session: session,
updatedAt: new Date().toISOString()
});
}

async function getStoredSession(sessionId) {
const record = await db.sessions.findOne({ sessionId });
return record?.session || null;
}

ゲーム開始時にセッションを比較

ゲームクライアントが起動したら、サーバーを呼び出してセッションの変更を確認する必要があります。サーバーはk-IDから現在のセッションを取得し、比較します:

// Server-side endpoint that your game client calls on startup
app.get("/api/session/:sessionId", async (req, res) => {
const { sessionId } = req.params;

const cachedSession = await getStoredSession(sessionId);
if (!cachedSession) {
return res.json({ needsConsent: true });
}

const response = await fetch(
`https://game-api.k-id.com/api/v1/session/get?sessionId=${sessionId}&etag=${cachedSession.etag}`,
{ headers: { "Authorization": `Bearer ${process.env.KID_API_KEY}` } }
);

if (response.status === 304) {
return res.json({ hasChanged: false, session: cachedSession });
}

const { session: currentSession } = await response.json();
const changes = comparePermissions(cachedSession, currentSession);
await storeSession(sessionId, currentSession);

res.json({
hasChanged: changes.length > 0,
changes: changes,
session: currentSession
});
});

権限の違いを検出

古いセッションと新しいセッションを比較して、特定の変更を識別します:

function comparePermissions(oldSession, newSession) {
const changes = [];

for (const newPerm of newSession.permissions) {
const oldPerm = oldSession.permissions.find(p => p.name === newPerm.name);

if (!oldPerm) {
changes.push({ type: "added", permission: newPerm.name, enabled: newPerm.enabled });
} else if (oldPerm.enabled !== newPerm.enabled) {
changes.push({
type: newPerm.enabled ? "enabled" : "disabled",
permission: newPerm.name,
previousState: oldPerm.enabled
});
} else if (oldPerm.managedBy !== newPerm.managedBy) {
changes.push({
type: "management_changed",
permission: newPerm.name,
previousManagedBy: oldPerm.managedBy,
newManagedBy: newPerm.managedBy
});
}
}

if (oldSession.ageStatus !== newSession.ageStatus) {
changes.push({
type: "age_status_changed",
previousStatus: oldSession.ageStatus,
newStatus: newSession.ageStatus
});
}

return changes;
}
年齢アップとプレイヤー管理の権限

プレイヤーが年齢アップし、親の同意を必要としなくなると、k-IDはWebhook通知を送信しません。ただし、セッションを比較すると、以前はmanagedBy: "GUARDIAN"だった権限がmanagedBy: "PLAYER"に変更されていることに気づくでしょう。

権限がプレイヤー管理になると、プレイヤーは親の同意なしで直接制御できます。親に依頼するのではなく、プレイヤー自身がこれらの権限を有効または無効にできるようにUIを更新する必要があります。プレイヤーが/session/upgrade APIを介してPLAYER管理の権限を有効にすることをリクエストすると、チャレンジを作成せずに自動的に有効になります。ステップ4のupdateFeatureAccess関数は、UIでPLAYER管理の権限を処理する方法を示しています。

ステップ4:変更をプレイヤーに伝達

変更を検出したら、機能が変更された理由をプレイヤーに明確に伝達します。これは重要です:プレイヤーが何かが壊れていると思ってはいけません。

UXガイドライン

権限の変更を伝達し、無効な機能を表示し、権限リクエストを処理するための詳細な設計推奨事項については、UXガイドラインを参照してください。

情報提供ダイアログを表示

クライアント側で、何が変更されたか、なぜ変更されたかを説明するダイアログを表示します(サーバーから返されたchanges配列を使用):

function showPermissionChangeDialog(changes) {
const disabledFeatures = changes
.filter(c => c.type === "disabled")
.map(c => getFeatureDisplayName(c.permission));

const enabledFeatures = changes
.filter(c => c.type === "enabled")
.map(c => getFeatureDisplayName(c.permission));

const ageChanged = changes.find(c => c.type === "age_status_changed");

let message = "";

if (ageChanged) {
// Player aged up
message = "Happy birthday! 🎉 Your permissions have been updated based on your new age.";
} else if (disabledFeatures.length > 0 && enabledFeatures.length === 0) {
// Parent restricted features
message = "Your parent has updated your permissions. " +
"The following features are no longer available:\n\n" +
disabledFeatures.map(f => `${f}`).join("\n");
} else if (enabledFeatures.length > 0 && disabledFeatures.length === 0) {
// Parent enabled features
message = "Great news! Your parent has enabled new features:\n\n" +
enabledFeatures.map(f => `${f}`).join("\n");
} else {
// Mixed changes
message = "Your permissions have been updated.";
if (enabledFeatures.length > 0) {
message += "\n\nNow available:\n" + enabledFeatures.map(f => `${f}`).join("\n");
}
if (disabledFeatures.length > 0) {
message += "\n\nNo longer available:\n" + disabledFeatures.map(f => `${f}`).join("\n");
}
}

showDialog({
title: "Permissions Updated",
message: message,
buttons: [{ text: "OK", action: "dismiss" }]
});
}

function getFeatureDisplayName(permissionName) {
const displayNames = {
"voice-chat": "Voice Chat",
"text-chat-private": "Private Messages",
"text-chat-public": "Public Chat",
"in-game-purchases": "In-Game Purchases",
"multiplayer": "Online Multiplayer",
// Add all your permissions here
};
return displayNames[permissionName] || permissionName;
}

無効な機能を適切に処理

クライアント側で、機能が無効になっている場合、UIがこれを明確に反映するようにします(サーバーから返されたセッションデータを使用):

function updateFeatureAccess(session) {
for (const permission of session.permissions) {
const featureElement = document.querySelector(`[data-feature="${permission.name}"]`);

if (!featureElement) continue;

if (!permission.enabled) {
featureElement.classList.add("feature-disabled");

if (permission.managedBy === "GUARDIAN") {
// Show "Ask parent" option
featureElement.setAttribute("data-disabled-reason", "parent");
featureElement.querySelector(".disabled-message").textContent =
"Ask a parent to enable this feature";
} else if (permission.managedBy === "PROHIBITED") {
// Feature is not available in this jurisdiction
featureElement.setAttribute("data-disabled-reason", "prohibited");
featureElement.querySelector(".disabled-message").textContent =
"This feature is not available";
} else if (permission.managedBy === "PLAYER") {
featureElement.setAttribute("data-disabled-reason", "player-choice");
featureElement.querySelector(".disabled-message").textContent = "Tap to enable";
featureElement.addEventListener("click", () => togglePlayerPermission(permission.name));
}
} else {
featureElement.classList.remove("feature-disabled");
if (permission.managedBy === "PLAYER") {
featureElement.setAttribute("data-managed-by", "player");
}
}
}
}
プレイヤー管理の権限を処理

managedByPLAYERの場合、プレイヤーは親の同意なしで権限を直接制御できます。これは通常、プレイヤーが年齢アップした後に発生します。プレイヤーがこれらの権限を有効または無効にできるトグルやボタンなどのUIコントロールを提供する必要があります。プレイヤーが/session/upgrade APIを介してPLAYER管理の権限を有効にすることをリクエストすると、チャレンジを作成せずに自動的に有効になります(親の同意は不要です)。

ステップ5:セッション削除を処理

親がFamily Connectを通じてプレイヤーのプロダクトへのすべてのアクセスを取り消すと、セッションは最終ステップとして削除されます。セッションは単に消えます:クエリはHTTP 400を返します。これが発生した場合(Webhookまたはセッション比較中に検出)、プレイヤーは再度アクセスを取得するために年齢ゲートと同意フローを完了する必要があります。

セッション削除の理解

親がゲームまたはプロダクトへのアクセスを取り消すと:

  1. セッションが削除される - 最終ステップとして、セッションが削除されます。クエリはHTTP 400を返し、セッションが存在しなかったかのように見えます
  2. すべての未完了のチャレンジが失敗する - そのプレイヤーの保留中のチャレンジはすべて自動的にFAILに設定されます
  3. Webhookイベントが送信される - 削除に関するWebhook通知を受信します
削除されたセッションは400を返す

k-ID APIは、有効なセッションステータスとしてACTIVEまたはHOLDのみを返します。親がアクセスを取り消すと、セッションが削除され、APIは400(見つからない)を返します。これは意図的です:セッションが削除されると、存在しなくなったものとして扱う必要があります。削除されたセッションは、プレイヤーがゲームにアクセスできなくなったため、実質的に「見つからない」ものです。

削除されたセッションを検出

Webhook経由(サーバー側):

case "Session.Delete":
await deleteStoredSession(data.id);
await markSessionAsDeleted(data.id);
break;

セッションを取得する際のAPI経由(サーバー側):

const response = await fetch(
`https://game-api.k-id.com/api/v1/session/get?sessionId=${sessionId}`,
{ headers: { "Authorization": `Bearer ${process.env.KID_API_KEY}` } }
);

if (response.status === 400) {
const error = await response.json();
if (error.error === "NOT_FOUND") {
await deleteStoredSession(sessionId);
return res.json({ sessionDeleted: true });
}
}
400を処理するためのベストプラクティス

セッションをクエリする際のNOT_FOUNDエラーを含む400レスポンスは、以下を意味する可能性があります:

  • セッションが作成されなかった
  • セッションが削除された
  • セッションIDが無効

アプリケーションは、これらのすべてのケースを同じ方法で処理する必要があります:根本的な理由に関係なく、プレイヤーがアクセス権を持っていないことを示すものとして扱います。ポーリングのみに依存するのではなく、Webhookを使用してセッション削除に関するリアルタイム通知を受信します。

年齢ゲートにリダイレクト

ゲームクライアントがサーバーからsessionDeleted: trueレスポンスを受信したら、年齢ゲートフローを再開します:

// Client-side handling
async function checkSession(sessionId) {
const response = await fetch(`/api/session/${sessionId}`);
const data = await response.json();

if (data.sessionDeleted || data.needsConsent) {
// Show explanation to the player
showDialog({
title: "Session Ended",
message: "Your session has ended. Please complete age verification to continue playing.",
buttons: [{
text: "Continue",
action: () => navigateToAgeGate()
}]
});
}
}

ステップ6:新しい権限のアップグレードパスを提供

プレイヤーは、親の同意を必要とする機能へのアクセスをリクエストしたい場合があります。/session/upgrade APIは、このフローを可能にします。

アップグレードが可能かどうかを確認

「親に依頼」ボタンを表示する前に、権限をアップグレードできるかどうかを確認します。このロジックは、サーバーから返されたセッションデータを使用してクライアント側で実行できます:

// Client-side check using session data from your server
function canRequestPermission(session, permissionName) {
const permission = session.permissions.find(p => p.name === permissionName);

if (!permission) return false;

// Can only request if currently disabled and managed by GUARDIAN
return !permission.enabled && permission.managedBy === "GUARDIAN";
}

権限のアップグレードをリクエスト

プレイヤーが「親に依頼」をタップすると、ゲームクライアントがサーバーを呼び出し、サーバーがk-IDセッションアップグレードAPIを呼び出します:

// Server-side endpoint
app.post("/api/request-permission", async (req, res) => {
const { sessionId, permissionName } = req.body;

const response = await fetch(
"https://game-api.k-id.com/api/v1/session/upgrade",
{
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KID_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
sessionId: sessionId,
requestedPermissions: [{ name: permissionName }]
})
}
);

const result = await response.json();

if (result.status === "PASS") {
// Permission was enabled immediately (player can manage it)
// Update stored session
await storeSession(sessionId, result.session);
return res.json({ success: true, session: result.session });
} else if (result.status === "CHALLENGE") {
// Parent consent required
return res.json({
success: false,
requiresConsent: true,
challenge: result.challenge
});
}
});

セッションアップグレードウィジェットは、親が権限リクエストを確認して承認するための完全な、事前構築されたインターフェースを提供します。Webベースのゲームを構築している場合、またはiframeを表示できる場合、これは最も簡単なアプローチです。

保護者管理のセッションのみ

セッションアップグレードウィジェットは、保護者管理のセッションでのみ機能します。プレイヤーのセッションがプレイヤー管理(managedBy"PLAYER")の場合、/session/upgrade APIを介してチャレンジを作成せずに権限を直接有効にできるため、ウィジェットは必要ありません。

チャレンジが返されたら、ウィジェットURLを生成して親に表示します:

// Server-side endpoint to generate the widget URL
app.post("/api/permission-widget", async (req, res) => {
const { challengeId, parentEmail } = req.body;

const response = await fetch(
"https://game-api.k-id.com/api/v1/widget/generate-session-upgrade-url",
{
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KID_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
challengeId: challengeId,
email: parentEmail
})
}
);

const { url } = await response.json();
res.json({ widgetUrl: url });
});

クライアント側でiframeにウィジェットを表示します:

<iframe 
id="permission-widget"
src="WIDGET_URL"
width="100%"
height="600"
allow="camera;payment;publickey-credentials-get;publickey-credentials-create"
frameborder="0">
</iframe>

ウィジェットは、親の検証と権限承認を含む完全な同意フローを処理します。フローが完了したらWidget.ExitReview DOMイベントをリッスンし、サーバーからセッションを更新します。

UIを完全にコントロールする必要がある場合、またはiframeを使用できない場合は、カスタム同意フローを構築します。親が同意を提供するためのオプションを提示します:

// Client-side UI
function showConsentRequest(challenge) {
showDialog({
title: "Ask a Parent",
message: "A parent needs to approve this feature. How would you like to reach them?",
options: [
{
label: "Send an email",
action: () => showEmailInput(challenge.challengeId)
},
{
label: "Show QR code",
action: () => showQRCode(challenge.url)
},
{
label: "Show code",
sublabel: `Go to asktoplay.com and enter: ${challenge.oneTimePassword}`,
action: () => showCodeDisplay(challenge.oneTimePassword)
}
]
});
}

カスタム同意フローを処理する詳細については、カスタム年齢ゲートクイックスタートを参照してください。

ベストプラクティス

パフォーマンスの推奨事項

  • 可能な限りWebhookを優先:ポーリングよりも効率的で、リアルタイムの更新を提供するため
  • etagパラメータを使用/session/getを呼び出す際に、不要なデータ転送を避けるため
  • セッションをローカルにキャッシュ:必要な場合にのみ更新を取得
  • 頻繁にポーリングしない:ポーリングする必要がある場合は、ゲームプレイ中は少なくとも30秒間隔でリクエストを待つか、ゲーム開始時のみチェック

エッジケースの処理

  • オフラインプレイヤー:セッションをキャッシュし、キャッシュされた権限を適用します。接続が復帰したら更新を確認します。
  • 複数のデバイス:プレイヤーが複数のデバイスを使用できる場合、一貫性を確保するために、アカウントに関連付けられたクラウドストレージにセッションを保存します。
  • ゲームプレイ中の年齢アップ:長時間のゲームプレイセッションでは、誕生日によってトリガーされた変更をキャッチするために、定期的なセッション更新を検討します。

次のステップ

セッションと権限の管理を実装したので、これらのリソースを探索してください:

  • Sessions:セッションのライフサイクルと構造の詳細
  • Challenges:同意チャレンジ、ステータス処理、ベストプラクティスの完全なガイド
  • Permissions:権限タイプと管理に関する詳細情報
  • Permissions:アップグレードフローの詳細
  • Webhooks:Webhook実装と検証の完全なガイド
  • Session.ChangePermissions:Webhookイベントリファレンス
  • Best practices:追加の実装ガイダンス