ADR-203: Authentication Mechanism for Friendship WebSocket Service

More details about this document
Latest published version:
https://adr.decentraland.org/adr/ADR-203
Authors:
Julieta11
agusaldasoro
jmoguilevsky
lauti7
guidota
2fd
kuruk-mm
moliva
menduz
Feedback:
GitHub decentraland/adr (pull requests, new issue, open issues)
Edit this documentation:
GitHub View commits View commits on githistory.xyz

Abstract

The objective of this document is to define the Mechanism that the Friendship WebSocket Server will use to authenticate their users.

This document contains the analysis of different solutions for authenticating a client and managing the token required for Authentication in a WebSocket Server. Each solution will be evaluated based on its pros and cons and their cost, including aspects such as scalability, security, and complexity.

All the proposed solutions here are thought as part of an exhaustive analysis of the Authentication section of Social Service M2. Anyway, the investigation done in this document can be the starting point for anyone at Decentraland who is creating a WebSocket Server.

Context, Reach & Prioritization

Authentication will be done using the same mechanism as Matrix, meaning that the user needs to send an AuthChain to login, leveraging the work in Social Service Authentication.

In the current flow for the Chat in Matrix, the user is responsible for obtaining the Token, and then sending it on every HTTP Rest request made to the server when using the chat, for example sending a message:

sequenceDiagram
    User->>+Social service: POST /login with AuthChain
    Social service->>+Matrix: Login with AuthChain
    Matrix->>-Social service: SynapseToken
    Social service->>+Cache: Store token + userID
    Social service-->>-User: SynapseToken
    User->>+Social service: POST /messages with SynapseToken

Once the token is received, the service queries Synapse to retrieve the corresponding user_id and handles all queries using that user's credentials. To prevent overloading Synapse, that information is cached in Redis.

This document focuses on possible solutions for authenticating the user with the Matrix Auth Mechanism.

Note: The usage of Synapse is meant to be deprecated, but in the maintime the Social Service needs the Synapse credentials in order to create rooms and send messages.

Solution Space Exploration

The solutions are categorized according to the client flow for obtaining the token:

  1. The Client obtains the token before creating the WebSocket connection.
  2. The Client obtains the token before sending a message to the Service.
  3. The Client logins against the service, and the SynapseToken is not sent to the final user.

Solution 1: The Client obtains the token before creating the WebSocket connection

Using the already mentioned flow, the user obtains the Token from the request to the social server POST /login Rest HTTP endpoint.

sequenceDiagram
    User->>+Social service: POST /login with AuthChain
    Social service->>+Matrix: Login with AuthChain
    Matrix->>-Social service: SynapseToken
    Social service->>+Cache: Store token + userID
    Social service-->>-User: SynapseToken (+ setCookie)

In this solution group, the connection will only be stablished when it's iniciated with a valid token.

Solution 1: Authenticate the WebSocket Connection

To prioritize minimizing requests to Redis and Synapse while taking advantage of an open connection to send the token only once and not having to send it with each message, a possible solution is to authenticate the WebSocket connection when it is opened. This way, only one WebSocket connection with the server is allowed when authenticated, and if no token is sent or it is incorrect, the connection is closed.

To achieve this, the client that opens the connection must send the Authentication Token via a header:

The native browser client does not allow headers to be added when creating a WebSocket connection. Therefore, the proposed solution is to use cookies when using browser:

service FriendshipsService {
  rpc GetFriends(google.protobuf.Empty) returns (stream Users) {}
}

Advantages

Disadvantages

Solution 2: The Client obtains the token before sending a message to the service

2.a: Use a login message

To send the token, this solution proposes to expand the current .proto definition with a mandatory login message that must be sent as the first message when establishing a WebSocket connection. The client sends its token in this message, and the service validates it against Synapse to obtain the corresponding user_id. This user_id remains the same throughout the connection for all subsequent messages, taking advantage of the established WebSocket connection.

Example of proto file:

service FriendshipsService {
  rpc Login(Token) returns (google.protobuf.Empty) {}
  rpc GetFriends(google.protobuf.Empty) returns (stream Users) {}
}

Advantages

Disadvantages

Solution 2.b: the login message is sent by the Server

This solution is analogue to 1.a but the message for the login is sent by the server, so the client must respond with the token. This way the logic for the client-side will be simpler. The rest of the analysis remains the same.

Solution 2.c: include the token on each message

Each message sent to the service includes the token as part of the payload. The client must send the token with each message, and the service validates it against Synapse to obtain the corresponding user_id.

Example of proto file:

service FriendshipsService {
  rpc GetFriends(Token) returns (stream Users) {}
}

Advantages

Disadvantages

Solution 2.d: Hybrid Model

Use a login message to obtain the FriendshipToken, a JWT generated by the service that combines the matrix_token and user_id. Each message requires the FriendshipToken, which eliminates the need to query Redis.

If the client sends a message other than the login message, such as GetFriends, the service will respond with Unauthorized. If a connection is established and no login message is received within a certain time frame, the connection will be closed.

Example of proto file:

service FriendshipsService {
  rpc Login(SynapseToken) returns (FriendshipToken) {}
  rpc GetFriends(FriendshipToken) returns (stream Users) {}
}

Advantages

Disadvantages

Solution 3: The Client logins against the service, and the Synapse Token is not sent to the final user

Although the client must continue to obtain the Synapse token as long as they need to send messages through the chat, it is somewhat confusing and clutters the protocol for them to have to manually handle a Synapse token, as it is an external dependency.

For this reason, this proposal seeks to obfuscate the token so that the user logs in with Synapse through the Friendships WebSocket Server, and the server internally handles the token and user_id mapping, storing it in the connection. Then, all messages are sent without any parameters.

This solution is analogous to 2.b but obscuring the Synapse Token.

sequenceDiagram
  client-->>server: open ws
  server-->>client: pleaseSignChallenge(challengeText=randomString())
  client-->>server: signedChallenge(result=sign(challengeText))
  server-->>server: authenticate, authorize
  server-->>server: load modules based on the permissions
  server-->>client: welcomeMessages(avalableModulesList)
syntax = "proto3";
package decentraland.bff;

message GetChallengeRequest {
  string address = 1;
}

message GetChallengeResponse {
  string challenge_to_sign = 1;
  bool already_connected = 2;
}

message SignedChallenge {
  string auth_chain_json = 1;
}

message WelcomePeerInformation {
  string peer_id = 1;

  // list of available modules in this BFF
  repeated string available_modules = 2;
}

service BffAuthenticationService {
  rpc GetChallenge(GetChallengeRequest) returns (GetChallengeResponse) {}
  rpc Authenticate(SignedChallenge) returns (WelcomePeerInformation) {}
}

Advantages

Disadvantages

Conclusion

Explored solutions are:

The primary disadvantages associated with the solution using cookies (1: Authenticate the WebSocket Connection) are due to native libraries that do not ensure header management when initiating a WebSocket connection. While there are different solutions for current clients that work, there is not guarantee the flexibility of this solution for future clients. Furthermore, the benefits of this solution are not significant enough to justify the effort required for implementation. The community typically opts for a solution more similar to the second group when authentication is required in a WebSocket.

One of the most straightforward solutions to replace in the future is the 2.c: include the token on each message solution. Since that solution is minimally intrusive, the implementation cost is low, and there is no cost associated with its removal.

Ideally, the solution should follow an upgrade format initiated by the client, whereby the server asks the client the credentials like AuthChain or the Authentication Token. If the message requesting the token is not received, the service terminates the communication, similar to the HTTP or TLS handshake. Solutions within this category include:

Among those solutions, the benefit of implementing the third one is that the Synapse Token is opaque to the client, making it something internal that can be removed in the future if Synapse dependency is removed. Anyway, the cost of implementing that solution today implies many changes in new dcl libraries like rpc-rust and decentraland-crypto-rust which may imply a risk for a project as big as Social Service M2.

For the Scope of the Social Service M2 Project, the decision is to implement the 2.c solution, which will handle the authentication individually on each message, as it is cheaper the implement and the focus and effort is in the robustness and availability of a new WebSocket Server implemented in Rust that is thought to replace the dependency to Synapse when managing friendships and request friendships. The 2.c solution does not represent any security risk or concern, as the Synapse Token is already stored on the local storage of the client, and as it will be validated on every message the behavior will be analog to what exists today.

In this line, the idea is to revise this solution after Social Service M2 is released and then decide if better Authentication management can be done (like implementing the 3 solution). So, the .proto definition used to manage the friendships in the Social Service for M2 is for internal use only, and the release will be marked as beta. The token in each message is strictly "optional" in the .proto from day-0, that would make it "always removable".

License

Copyright and related rights waived via CC0-1.0. DRAFT Review