From 596ec0bc161704b2be16747246bc128ffd08d2fe Mon Sep 17 00:00:00 2001 From: Andrew Giel Date: Thu, 4 Sep 2025 16:45:57 -0400 Subject: [PATCH] [activities] add Validating Proxy Request Headers documentation --- .../multiplayer-experience.mdx | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/docs/activities/development-guides/multiplayer-experience.mdx b/docs/activities/development-guides/multiplayer-experience.mdx index f60f4a8de5..70610f48d2 100644 --- a/docs/activities/development-guides/multiplayer-experience.mdx +++ b/docs/activities/development-guides/multiplayer-experience.mdx @@ -138,6 +138,8 @@ Activities are surfaced through iframes in the Discord app. The activity website It is theoretically possible for a malicious client to mock Discord's RPC protocol or load one activity website when launching another. Because the activity is loaded inside Discord, the RPC protocol is active, and the activity is none the wiser. +### Using the Activity Instance API + To enable an activity to "lock down" activity access, we encourage utilizing the `get_activity_instance` API, found at `discord.com/api/applications//activity-instances/'`. The route requires a Bot token of the application. It returns a serialized active activity instance for the given application, if found, otherwise it returns a 404. Here are two example responses: ```javascript @@ -150,5 +152,99 @@ curl https://discord.com/api/applications/1215413995645968394/activity-instances With this API, the activity's backend can verify that a client is in fact in an instance of that activity before allowing the client to participate in any meaningful gameplay. How an activity implements "session verification" is left to the developer's discretion. The solution can be as granular as gating specific features or as binary as not returning the activity HTML except for valid sessions. +###### Validating Proxy Request Headers + +For apps that want additional security validation, Discord provides an optional proxy authentication system. When your embedded app makes requests through Discord's proxy, each request can include cryptographic headers that prove the request's authenticity. + +Each proxy-authenticated request is sent with the following headers: + +- `X-Signature-Ed25519` as a cryptographic signature +- `X-Signature-Timestamp` as a Unix timestamp +- `X-Discord-Proxy-Payload` as a base64-encoded payload containing user context + +If you choose to use proxy authentication, you can validate these headers to ensure requests are legitimate. If the signature fails validation, your app should respond with a `401` error code. + + +Below are some code examples that show how to validate the headers sent in proxy-authenticated requests. + +**JavaScript** + +```js +const nacl = require("tweetnacl"); + +// Your public key can be found on your application in the Developer Portal +const PUBLIC_KEY = "APPLICATION_PUBLIC_KEY"; + +const signature = req.get("X-Signature-Ed25519"); +const timestamp = req.get("X-Signature-Timestamp"); +const payload = req.get("X-Discord-Proxy-Payload"); + +// Decode the base64 payload +const payloadBytes = Buffer.from(payload, "base64"); +const payloadString = payloadBytes.toString("utf-8"); +const payloadData = JSON.parse(payloadString); + +// Verify timestamp matches payload +if (payloadData.created_at.toString() !== timestamp) { + return res.status(401).end("invalid request timestamp"); +} + +// Check if token has expired +if (payloadData.expires_at < Math.floor(Date.now() / 1000)) { + return res.status(401).end("expired proxy token"); +} + +// Verify the signature using tweetnacl +const isVerified = nacl.sign.detached.verify( + payloadBytes, + Buffer.from(signature, "base64"), + Buffer.from(PUBLIC_KEY, "hex") +); + +if (!isVerified) { + return res.status(401).end("invalid request signature"); +} +``` + +**Python** + +```py +import json +import base64 +import time +from nacl.signing import VerifyKey +from nacl.exceptions import BadSignatureError + +# Your public key can be found on your application in the Developer Portal +PUBLIC_KEY = 'APPLICATION_PUBLIC_KEY' + +verify_key = VerifyKey(bytes.fromhex(PUBLIC_KEY)) + +signature = request.headers["X-Signature-Ed25519"] +timestamp = request.headers["X-Signature-Timestamp"] +payload = request.headers["X-Discord-Proxy-Payload"] + +# Decode the base64 payload +payload_bytes = base64.b64decode(payload) +payload_string = payload_bytes.decode('utf-8') +payload_data = json.loads(payload_string) + +# Verify timestamp matches payload +if str(payload_data['created_at']) != timestamp: + abort(401, 'invalid request timestamp') + +# Check if token has expired +if payload_data['expires_at'] < int(time.time()): + abort(401, 'expired proxy token') + +try: + verify_key.verify(payload_bytes, bytes.fromhex(signature)) +except BadSignatureError: + abort(401, 'invalid request signature') +``` + + +Proxy authentication is entirely optional and provided as an additional security layer for apps that choose to implement it. + In the below flow diagram, we show how the server can deliver the activity website, only for valid users in a valid activity instance: ![application-test-mode-prod](images/activities/activity-instance-validation.webp)