Remote Authentication (Desktop)

In the context of remote authentication, the desktop client is the device that wants to be authenticated by the already logged-in mobile client. See the introduction for more information.

Remote Authentication Gateway

The desktop implementation of remote authentication uses WebSocket connections, operating similarly to the Gateway connection. However, it has a separate set of payloads and events, as well as a simplified packet structure.

Gateway Versions
VersionStatusDefault
2Available✓ Client
1Discontinued

Gateway Payloads

In practice, opcodes are lower-cased with under_scores joining each word in the name. For instance, Nonce Proof would be nonce_proof.

For readability, opcodes in the following documentation are typically left in Title Case.

Remote authentication Gateway event payloads are flat packets, with the op field indicating the type of payload, and the rest of the fields being the payload data.

Example Gateway Payload
{
"op": "hello",
"timeout_ms": 142637,
"heartbeat_interval": 41250
}
Gateway Commands
NameDescription
InitStart a new remote auth session
HeartbeatMaintain an active WebSocket connection
Nonce ProofSubmit a cryprographic proof of the handshake
Gateway Events
NameDescription
HelloDefines the heartbeat and timeout intervals
Heartbeat ACKAcknowledges a received client heartbeat
Nonce ProofRequests a cryptographic proof of the handshake
Pending Remote InitAcknowledges a successful handshake
Pending TicketAcknowledges a successful mobile session creation
Pending LoginIndicates that the mobile session was finished
CancelIndicates that the mobile session was canceled
Gateway Close Event Codes
CodeDescriptionExplanation
1000Normal closureThe remote auth session was finished or canceled successfully
4000Unknown errorWe're not sure what went wrong; try reconnecting?
4001Handshake failureThe initial handshake failed; maybe reconnect and try again?
4002Decode errorYou sent an invalid payload; don't do that!
4003TimeoutYour session timed out; reconnect and start a new one

Connecting

For the remote authentication Gateway, the URL you can use to open a WebSocket connection is static.

When connecting to the URL, you must explicitly pass the API version as a query parameter. For example, wss://remote-auth-gateway.discord.gg/?v=2 is a URL a client may use to connect to the Gateway.

Endpoint
wss://remote-auth-gateway.discord.gg/
Query String Params
FieldTypeDescription
vintegerAPI Version to use

Handshaking

Once you open a connection to the Gateway, you will receive an Opcode Hello payload, which contains the heartbeat interval and timeout duration. See the heartbeating section for more information.

At this point, you should generate a 2048-bit RSA-OAEP keypair that will be used in further communications. This will be verified by the Gateway using a nonce proof. An example in Python pseudocode would be:

import base64
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
public_key = private_key.public_key()
Hello Structure
FieldTypeDescription
heartbeat_intervalintegerThe minimum interval (in milliseconds) the client should heartbeat at
timeout_ms 1integerThe lifespan of the remote auth session (in milliseconds) before the connection is closed, typically a few minutes

1 When the timeout duration passes, the Gateway will close with a 4003 close code.

Example Hello
{
"op": "hello",
"timeout_ms": 142637,
"heartbeat_interval": 41250
}

Upon receiving the Opcode Hello payload, you should immediately send an Opcode Init containing the public key that all future communication will be encrypted with.

An example in Python pseudocode would be:

spki = public_key.public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
encoded_public_key = base64.b64encode(spki).decode("utf-8")
Init Structure
FieldTypeDescription
encoded_public_keystringThe base64-encoded SPKI of the client's 2048-bit RSA-OAEP public key
Example Init
{
"op": "init",
"encoded_public_key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo2PGAKj4v6r6sPJtgJe2eIDCM8uEHKpYCSDmp+pun9vqiqPt4pDToS1vGtwTwc5hKKqtIo+I/5veBpGWSD/veuB0xVb/JbkPn847Q+mXAb6c9vRMJVkA7l9GaZdN49U5bnGJi009aNBoy9cAcP/19H6TLpHmZ9RojnqGqlCUdyAiqceTDTzPqov4ST3GJSyKPydL3ZVpPf5P/PGyNfISuESKA2CxGCoBvB4H6/FH7cwSFelyqhwwHPZcyxBjF/3iXx+k1PdS01y0NoTRun4p76bE9rWnecIWONPFvCkby8Xs/OqQ8QcAoLkfVj5L29Ut1+Kmwwfg3nzc4glZa6RuTwIDAQAB"
}

If the Gateway accepts the public key, it will respond with an Opcode Nonce Proof payload, which contains a nonce that you must decrypt with your private key.

Nonce Proof Structure (Receive)
FieldTypeDescription
encrypted_noncestringThe base64-encoded nonce encrypted with the client's public key
Example Nonce Proof (Receive)
{
"op": "nonce_proof",
"encrypted_nonce": "GrOYi2wz9athue0mTNrdlnWGqJYkqu8tPe2uGr9V7CRwqVWDjgUI06gsPKszkORgB92P1P84V04fo7hG0tkQ/kDVNbVguACKfE4AIUUQXQFSkTVaGbZ2+FsItsoqOd+955EvkBK2oMz+kWlYILTQcISip9g5ZrY9SoKQvk7HDW9DliSteZivHzXQOc5RyecyeexOcV8oyC1zTk+uyVUTv3g7fcZ1Y81AK6u4+hvnEKGjOyEn+lbOotkNwcMC02xyVBX3IysSuNXf/f8/6gPMBNUHXEtlvhYx9AMCsPrPKkiilV7HpLN3oIvAfsZnyxbYcNiC6YS7z7VIPRaXEWW76w=="
}

You should then send an Opcode Nonce Proof payload containing a base64URL-encoded representation of the decrypted nonce's SHA-256 digest.

An example in Python pseudocode would be:

nonce = private_key.decrypt(
base64.b64decode(encrypted_nonce),
padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None),
)
nonce_proof = base64.urlsafe_b64encode(
hashes.Hash(hashes.SHA256()).update(nonce).finalize()
).decode("utf-8").rstrip("=")
Nonce Proof Structure (Send)
FieldTypeDescription
noncestringThe base64URL-encoded SHA-256 digest of the decrypted nonce
Example Nonce Proof (Send)
{
"op": "nonce_proof",
"nonce": "1xFdzsYTFMtZuUarGNKpBpMANyqNpRXpDFaxl8wFeQBUnVuY60d9j-or80f36xusAVTmUL90jwHrxMVPGZXJ8ahqDAiXByEOjkreJXZdPRbnvDkHHyqUP0QgHQBvrKq5Cba9-SDO8pFSc9T1YMWGut7n34xx6txX-QR7wDcZAoghq5EBl04WRlnt1DWfX2wMA7NuL1GFIZdw10IedBZ13E72BXnDasX97XMX_ldbxKwee6ABGf18zde2oHbTqw"
}

If the Gateway accepts the nonce proof, it will respond with an Opcode Pending Remote Init payload, which indicates that the key exchange was successful and the remote auth session is ready.

At this point, the fingerprint can be used to create a remote auth session on the mobile client. The fingerprint is typically communicated using a QR code with the following format: https://discord.com/ra/<fingerprint>.

Pending Remote Init Structure
FieldTypeDescription
fingerprintstringThe base64URL-encoded SHA-256 digest of the client's public key
Example Pending Remote Init
{
"op": "pending_remote_init",
"fingerprint": "UZ0-kOVzXDZTFVV5_QlpURSO2BQHrtkKWHNpIGoDI0k"
}

Heartbeating

In order to maintain your WebSocket connection, you need to continuously send heartbeats at the interval determined in Opcode Hello:

This heartbeat interval is the minimum interval you should heartbeat at. You can heartbeat at a faster interval if you wish.

After receiving Opcode Hello, you should send Opcode Heartbeat every elapsed interval:

In return, you will be sent back an Opcode Heartbeat ACK. If a client does not receive a heartbeat ACK between its attempts at sending heartbeats, this may be due to a failed or "zombied" connection. The client should immediately terminate the connection and reconnect.

Example Heartbeat
{ "op": "heartbeat" }
Example Heartbeat ACK
{ "op": "heartbeat_ack" }

Finalizing

Once the mobile client creates a remote auth session (scans the QR code), the Gateway will send an Opcode Pending Ticket payload, which contains the information of the user that is attempting to authenticate.

Pending Ticket Structure
FieldTypeDescription
encrypted_user_payloadstringThe base64-encoded user payload encrypted with the client's public key
User Payload Structure

The user payload is a colon-separated string in the format 852892297661906993:0:05145cc5646fbcba277b6d5ea2030610:dolfies.

It contains the following fields (see the user object for more information):

FieldTypeDescription
idsnowflakeThe ID of the user
discriminatorstringThe user's stringified 4-digit Discord tag
avatar?stringThe user's avatar hash
usernamestringThe user's username, may be unique across the platform (2-32 characters)
Example Pending Ticket
{
"op": "pending_ticket",
"encrypted_user_payload": "MeJm9TeLa9S+/gUYZlm69TQqT3eqz1sG6f5Ym84lGCH7Hde/Dpv/knXX+wWp8WeyLPHYGn1smpTaGxwHmuvee1IZv8ybP9WeVscsBnrXpO7Fg2aT3hZJfPStncJxI0Uq+JhjqQ1V2lLuhoraeBrZbl/e0CBNhv0wmIGzFow6G078MQvikg20Jr+/2wRgY/buXuipqfW9RYIZUyr3Dl+MRW8EodKU3SOBgqaVpHETDLXkmb6rsvYU+O78iUu+cvTK3A0GkajxmhJ2nfg79iQBuLU6qJm7sN9mHy2uKAExa5TUJfeeCqXjTr1uROozlyBmCMjeP+Srrg37r0y2pkjYCg=="
}

At this point, the desktop client will either receive an Opcode Pending Login or Opcode Cancel event. Both of these events will result in the Gateway closing with a 1000 close code. If neither of these events occur (the mobile client does not finish or cancel), the Gateway will eventually timeout and close with a 4003 close code.

If the mobile client finishes remote auth, the Gateway will send an Opcode Pending Login payload, which indicates that the authentication was successful.

Pending Login Structure
FieldTypeDescription
ticketstringThe ticket that can be used to obtain a token
Example Pending Login
{
"op": "pending_login",
"ticket": "ODUyODkyMjk3NjYxOTA2OTkz.HYoNwT.1X5Qs3Sd2Z2sDf3sFFFwd22_MccjcmwY"
}

If the mobile client cancels remote auth, the Gateway will send an Opcode Cancel payload, which indicates that the authentication was canceled.

Example Cancel
{ "op": "cancel" }

Endpoints

Exchange Remote Auth Ticket

POST/users/@me/remote-auth/login

Exchanges a remote auth ticket for an authentication token.

JSON Params
FieldTypeDescription
ticketstringThe ticket obtained from the remote authentication flow
Response Body
FieldTypeDescription
encrypted_tokenstringThe authentication token encrypted with the client's public key