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.
      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.
The solutions are categorized according to the client flow for obtaining the token:
      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.
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:
/login,
        where the response includes the header
        Set-Cookie: friendships-Authorization=<Token>; Domain=social.decentraland.org.
      Cookie: friendships-Authorization=<Token> is sent, and the server
        validates the token and obtains the user_id for the entire connection. Example of proto
        file:
      service FriendshipsService {
  rpc GetFriends(google.protobuf.Empty) returns (stream Users) {}
}
Advantages
Disadvantages
/login endpoint would have to use the AuthChain anyways. Diverging from the
        canonical mechanism wouldn't be sensible on this scenario.
      
      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
dcl-rpc currently does
        not expose the connection because it is abstracted.
      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.
      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
.proto file is modified to receive the token as a
        parameter, so migrating to another .proto definition that excludes that field should be
        handled correctly.
      
      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
synapse_token and user_id.
      Disadvantages
FriendshipToken must be regenerated. This
        can generate a bit more overhead on the server.
      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
dcl-rpc currently does
        not expose the connection because it is abstracted.
      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:
2.a: Use a login message2.b: the login message is sent by the Server3: The Client logins against the service
      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".