This ADR proposes a new authentication flow for Decentraland that simplifies the sign-in process by using deep links to deliver the user's AuthIdentity back to the client. Instead of requiring users to manually verify a code, the auth website stores the AuthIdentity on the server and redirects the user back to the client via a deep link containing an identity retrieval token. The client then fetches the complete identity from the server. If the deep link fails (e.g., client not installed), the flow falls back to the existing verification code mechanism.
The existing Decentraland sign-in process works as follows:
sequenceDiagram
participant User as User
participant Client as Decentraland Client
participant Browser as Browser
participant Server as Auth Server
User->>Client: Presses "Sign In" button
Client->>Server: Creates auth request
Server->>Client: Returns requestId + verification code
Client->>Client: Displays verification code (e.g., "67")
Client->>Browser: Opens auth URL with requestId
User->>Browser: Completes social login (Google, etc.)
Browser->>User: Shows "Verify code: is it 67? YES/NO"
User->>User: Manually checks code displayed in Client
User->>Browser: Clicks YES
Browser->>Server: Confirms verification
Server->>Client: Sends AUTH CHAIN (via WebSocket)
Client->>Client: User authenticated
The current verification code flow has significant user experience friction:
The current implementation is vulnerable to URL-based session fixation/hijacking attacks:
This vulnerability exists because:
Use deep links to return the user to the client after authentication, with the identity stored server-side for retrieval. Falls back to verification code if deep link fails.
Benefits:
Security Model:
Embed the entire AuthIdentity in the deep link URL.
Why rejected:
sequenceDiagram
participant User as User
participant Client as Decentraland Client
participant Browser as Browser
participant Server as Auth Server
participant OS as Operating System
User->>Client: Presses "Sign In" button
Client->>Server: Creates auth request
Server->>Client: Returns requestId
Client->>Browser: Opens auth URL with requestId & flow=deeplink
User->>Browser: Completes wallet connection / social login
Browser->>Browser: Generates AuthIdentity (wallet signature)
Browser->>Server: POST /identities (with signed fetch)
Server->>Browser: Returns { identityId }
Browser->>OS: Opens decentraland://open?signin={identityId}
OS->>Client: Routes deep link to client
Client->>Server: GET /identities/{identityId}
Server->>Client: Returns AuthIdentity
Client->>Client: User authenticated
If the deep link fails to open (detected via browser blur event timeout), the auth website
removes the flow=deeplink parameter and redirects to the standard verification
code flow:
sequenceDiagram
participant User as User
participant Browser as Browser
participant Server as Auth Server
participant Client as Decentraland Client
Browser->>Browser: Deep link attempt fails (no blur detected)
Browser->>Browser: Redirect without flow=deeplink
Browser->>User: Shows "Verify code: is it XX? YES/NO"
User->>Browser: Clicks YES
Browser->>Server: Confirms verification
Server->>Client: Sends AUTH CHAIN (via WebSocket)
Client->>Client: User authenticated
Stores an AuthIdentity for later retrieval. Protected by signed fetch authentication.
Request:
{
"identity": {
// AuthIdentity object from @dcl/crypto
}
}
Headers:
Response (200 OK):
{
"identityId": "uuid-v4",
"expiration": "2025-12-11T12:05:00Z"
}
Error Responses:
Retrieves a stored AuthIdentity. Single-use - identity is deleted after successful retrieval.
Response (200 OK):
{
"identity": {
// AuthIdentity object
}
}
Error Responses:
decentraland://open?signin={identityId}
Where identityId is the UUID returned from the POST /identities endpoint.
The client MUST:
decentraland:// protocol with the operating systemflow=deeplink parametersignin query parametersignin deep link:
identityId from the URLGET /identities/{identityId}The client SHOULD:
The auth website MUST:
flow=deeplink query parameterflow=deeplink is present:
/identities endpoint with signed fetchdecentraland://open?signin={identityId}The auth website SHOULD:
The auth website detects whether the deep link was successful using the browser blur technique:
blur event listener to the windowblur event fired, the app was launched successfullyblur event, fall back to verification code flowconst launchDeepLink = (url: string): Promise<boolean> => {
return new Promise((resolve) => {
let appDetected = false
const handleBlur = () => {
appDetected = true
}
window.addEventListener("blur", handleBlur)
const iframe = document.createElement("iframe")
iframe.style.display = "none"
iframe.src = url
document.body.appendChild(iframe)
setTimeout(() => {
window.removeEventListener("blur", handleBlur)
document.body.removeChild(iframe)
resolve(appDetected)
}, 500)
})
}
Signed Fetch Validation: The server MUST validate signed fetch headers to ensure only the identity owner can store their identity.
Short Expiration: Stored identities MUST expire within 15 minutes.
Single Use: Identities MUST be deleted immediately after successful retrieval.
UUID Generation: Identity IDs MUST be cryptographically random UUIDs (v4) to prevent guessing.
Even if an attacker shares their auth URL with a victim:
decentraland://open?signin={id} opens on the
victim's machine
The deep link is routed by the victim's operating system to the victim's local application, not over the network to the attacker.
This flow is activated by including flow=deeplink in the auth URL:
https://auth.decentraland.org/auth/requests/{requestId}?flow=deeplink
When this parameter is present:
The web application SHOULD handle:
The client SHOULD handle:
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.