4. API

The Federation API consists of two layers:
  1. Between federators

  2. Between other components

4.1. Qualified Identifiers and Names

The federated (and consequently distributed) architecture is reflected in the structure of the various identifiers and names used in the API. Before federation, identifiers were only unique in the context of a single backend; for federation, they are made globally unique by combining them with the federation domain of their backend. We call these combined identifiers qualified identifiers. While other parts of some identifiers or names may change, the domain name (i.e. the qualifying part) is static.

In particular, we use the following identifiers throughout the API:

While the canonical representation for purposes of visualization is as displayed above, the API often decomposes the qualified identifiers into an (unqualified) id and a domain name. In the code and API documentation, we sometimes call a username a “handle” and a qualified username a “qualified handle”.

Besides the above names and identifiers, there are also user display names (sometimes also referred to as “profile names”), which are not unique on the user’s backend, can be changed by the user at any time and are not qualified.

4.2. API between Federators

The layer between federators acts as an envelope for communication between other components of wire server. It uses Protocol Buffers (protobuf from here onwards) for serialization over gRPC. The latest protobuf schema can be inspected at the wire-server repository.

All gRPC calls are made via a mutually authenticated TLS connection and subject to a general, as well as a per-request authorization step.

The Inward service defined in the schema is used between federators. It supports one rpc called call which requires a Request and returns an InwardResponse. These objects looks like this:

message Request {
  Component component = 1;
  bytes path = 2;
  bytes body = 3;
  string originDomain = 4
}

enum Component {
  Brig = 0;
  Galley = 1;
}

message InwardResponse {
  oneof response {
    InwardError err = 1;
    bytes body = 2;
  }
}

message InwardError {
  enum ErrorType {
    IOther = 0;
    IInvalidDomain = 1;
    IFederationDeniedByRemote = 2;
    IInvalidEndpoint = 3;
    IForbiddenEndpoint = 4;
  }

  ErrorType type = 1;
  string msg = 2;
}

The component field in Request tells the federator which components this request is meant for and the rest of the arguments are details of the HTTP request which must be made against the component. It intentionally supports a restricted set of parameters to ensure that the API is simple.

4.3. API From Components to Federator

Between two federated backends, the components talk to each other via their respective federators. When making the call to the federator, the components use protobuf over gRPC. They call the Outward service, which also supports one rpc called call. This rpc requires a FederatedRequest object, which contains a Request object as defined above, as well as the domain of the destination federator. The rpc returns an OutwardResponse, which can either contains a body with the returned information or an OutwardError, these objects look like this:

message FederatedRequest {
  string domain = 1;
  Request request = 2;
}

message OutwardResponse {
  oneof response {
    OutwardError err = 1;
    bytes body = 2;
  }
}

message OutwardError {
  enum ErrorType {
    RemoteNotFound = 0;
    DiscoveryFailed = 1;
    ConnectionRefused = 2;
    TLSFailure = 3;
    InvalidCertificate = 4;
    VersionMismatch = 5;
    FederationDeniedByRemote = 6;
    FederationDeniedLocally = 7;
    RemoteFederatorError = 8;
    InvalidRequest = 9;
  }

  ErrorType type = 1;
  ErrorPayload payload = 2;
}

message ErrorPayload {
  string label = 1;
  string msg = 2;
}

4.4. API From Federator to Components

The components expose a REST API over HTTP to be consumed by the federator. All the paths start with /federation. When a federator recieves a request like this (shown as JSON for convenience):

{
  "component": "Brig",
  "path": "federation/get-user-by-handle",
  "body": "\"janedoe\"",
  "originDomain": "somedomain.example.com"
}

The federator connects to Brig and makes an HTTP request which looks like this:

> POST /federation/get-user-by-handle
> Wire-Origin-Domain: somedomain.example.com
> Content-Type: application/json
>
> "janedoe"

The /federation prefix to the path allows the component to distinguish federated requests from requests by clients or other local components.

If this request succeeds with any status, the response is encoded as the InwardResponse object and returned as a response to the Inward.call gRPC call.

Note, that before the path field of the Request is concatenated with /federation and used as a component of the HTTP request, its segments are normalized as described in Section 6.2.2.3 of RFC 3986 to prevent path-traversal attacks such as /federation/../users/by-handle.

4.5. List of Federation APIs exposed by Components

Each component of the backend provides an API towards the federator for access by other backends. For example on how these APIs are used, see the section on end-to-end flows.

Note

This reflects status of API endpoints as of 2021-06-25. For latest APIs please refer to the corresponding source code linked in the individual section.

4.5.1. Brig

In its current state, the primary purpose of the Brig API is to allow users of remote backends to create conversations with the local users of the backend.

  • get-user-by-handle: Given a handle, return the user profile corresponding to that handle.

  • get-users-by-ids: Given a list of user ids, return the list of corresponding user profiles.

  • claim-prekey: Given a user id and a client id, return a Proteus pre-key belonging to that user.

  • claim-prekey-bundle: Given a user id, return a prekey for each of the user’s clients.

  • claim-multi-prekey-bundle: Given a list of user ids, return the lists of their respective clients.

  • search-users: Given a term, search the user database for matches w.r.t. that term.

  • get-user-clients: Given a list of user ids, return the lists of clients of each of the users.

See the brig source code for the current list of federated endpoints of the Brig, as well as their precise inputs and outputs.

4.5.2. Galley

Each backend keeps a record of the conversations that each of its members is a part of. The purpose of the Galley API is to allow backends to synchronize the state of the conversations of their members.

  • register-conversation: Given a name and a list of conversation members, create a conversation locally. This is used to inform another backend of a new conversation that involves their local user.

  • get-conversations: Given a qualified user id and a list of conversation ids, return the details of the conversations. This allows a remote backend to query conversation metadata of their local user from this backend. To avoid metadata leaks, the backend will check that the domain of the given user corresponds to the domain of the backend sending the request.

  • update-conversation-memberships: Given a qualified user id and a qualified conversation id, update the conversation details locally with the other data provided. This is used to alert remote backend of updates in the conversation metadata of conversations that one of their local users is involved in.

  • receive-message: Given (sender, recipients, message payloads), propagate a message to local users. This is used whenever there is a remote user in a conversation (see end-to-end flows).

  • send-message: Given a sender and a raw message request, send a message to a conversation owned by another backend. This is used when the user sending a message is not on the same backend as the conversation the message is sent in.

See the galley source code for the current list of federated endpoints of the Galley, as well as their precise inputs and outputs.

4.6. End-to-End Flows

4.6.1. User Discovery

In this flow, the user A at backend-a.com tries to search for user B at backend-b.com.

  1. User A@backend-a.com enters the qualified user name of the target user B@backend-b.com into the search field of their Wire client.

  2. The client issues a query to /search/contacts searching for B at backend-b.com.

  3. A’s backend queries the search-users endpoint of B’s backend for B.

  4. B’s backend replies with with B’s user name and qualified handle.

  5. A’s backend forwards that information to A’s client.

4.6.2. Conversation Establishment

After having discovered user B at backend-b.com, user A at backend-a.com wants to establish a conversation with B.

  1. From the search results of a user discovery process, A chooses to create a conversation with B.

  2. A’s client issues a /users/backend-b.com/B/prekeys query to A’s backend.

  3. A’s backend queries the claim-prekey-bundle endpoint of B’s backend using B’s user id.

  4. B’s backend replies with a prekey bundle for each of B’s clients.

  5. A’s backend forwards that information to A’s client.

  6. A’s client queries the /conversations endpoint of its backend using B’s user id.

  7. A’s backend creates the conversation locally and queries the register-conversation endpoint of B’s backend to inform it about the new conversation, including the conversation metadata in the request.

  8. B’s backend registers the conversation locally and confirms the query.

  9. B’s backend notifies B’s client of the creation of the conversation.

4.6.3. Message Sending (A)

Having established a conversation with user B at backend-b.com, user A at backend-a.com wants to send a message to user B.

  1. In a conversation conv-1@backend-a.com on A’s backend with users A@backend-a.com and B@backend-b.com, A sends a message by using the /conversations/backend-a.com/conv-1/proteus/messages endpoint on A’s backend.

  2. A’s backend will check if A included all necessary user devices in their request. For that it will make a get-user-clients request to B’s backend. The returned list of clients will be checked to match against the list of clients the message was encrypted for.

  3. A’s backend will send the message to all clients of those users on A’s backend part of the conversation as usual,

  4. A’s backend will query the receive-message endpoint on B’s backend.

  5. B’s backend will propagate the message to all users on B.

4.6.4. Message Sending (B)

Having received a message from user A at backend-a.com, user B at backend-b.com wants send a reply.

  1. In a conversation conv-1@backend-a.com on A’s backend with users A@backend-a.com and B@backend-b.com, B sends a message by using the /conversations/backend-a.com/conv-1/proteus/messages endpoint on B’s backend.

  2. B’s backend will query the send-message endpoint on A’s backend. Steps 3-6 below are essentially the same as steps 2-5 in Message Sending (A)

  3. A’s backend will check if B included all necessary user devices in their request. For that it will make a get-user-clients request to B’s backend. The returned list of clients will be checked to match against the list of clients the message was encrypted for.

  4. A’s backend will send the message to all clients of those users on A’s backend part of the conversation as usual,

  5. A’s backend will query the receive-message endpoint on B’s backend.

  6. B’s backend will propagate the message to all users on B.