SSB HTTP Invites

Revision: 2021-04-26

Author: Andre Medeiros contact@staltz.com

License: This work is licensed under a Creative Commons Attribution 4.0 International License.

Abstract

As part of the process of onboarding to SSB, new users often need to connect to a "pub server" or "room server" where content can be retrieved from. These servers often employ an access control system based on invite tokens, to prevent access to undesired actors from the public internet. The invite system deployed by these servers has been a convoluted algorithm repurposing secret-handshake to create an ephemeral muxrpc connection only for the initial remote procedure call to claim the invite token. In this document, we describe a simpler HTTP-based invite-token system for pubs and rooms that applies before any secret-handshake connection is built.

Terminology

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.

Table of contents

Conditions

This specification makes clear assumptions about the setup involved peers authenticating.

Server: an SSB peer, known as the "server", MUST have an internet-public host address, MUST be accessible for secret-handshake connections under a multiserver address, and MUST support HTTPS requests as well as it MUST NOT support plain HTTP.

Client: another SSB peer, known as the "client", SHOULD be able to open a secret-handshake and muxrpc connection with the server. The user controlling this SSB peer also MUST control a web browser used to make requests to the server. The client's browser and operating system SHOULD support hyperlinks to SSB URIs, redirecting them to SSB applications that recognize and parse SSB URIs. The client's SSB application employed during SSB HTTP Authentication MUST be able to recognize and parse SSB URIs.

Specification

  1. Suppose an SSB user (known as "the client") has the SSB ID userId and has an SSB app supporting parsing SSB URIs
  2. Suppose the server is hosted at domain serverHost and has generated an invite inviteCode
  3. The invite link corresponding to inviteCode SHOULD be a URL in the format https://${serverHost}/join?invite=${inviteCode}
  4. When the client visits that URL in a browser, the server MUST respond with HTML such that:
    1. If the inviteCode is already claimed or otherwise no longer valid, an error page SHOULD be rendered as response, and no further steps in this specification apply
    2. Otherwise, the inviteCode is unclaimed, and the following SSB URI MUST be rendered on the response page: ssb:experimental?action=claim-http-invite&invite=${inviteCode}&postTo=${submissionUrl} where ${submissionUrl} is another URL on the server
  5. The client's SSB app SHOULD parse the SSB URI and subsequently SHOULD send an HTTPS POST request to the endpoint submissionUrl with the header Content-Type equal application/json and the following body: {"id":"${userId}","invite":"${inviteCode}"}
  6. The server receives the POST request and:
    1. If the inviteCode is already claimed, the response SHOULD be an error, and no further steps in this specification apply
    2. Otherwise, the inviteCode is now considered claimed for userId, which means:
      1. The server SHOULD store the client's userId and allow the client to access resources on the server, effectively making the client a recognized member
      2. The server MUST respond with header Content-Type equal application/json and body {"multiserverAddress":"${serverMsAddr}"} where ${serverMsAddr} consititutes the server's multiserver address
  7. The client receives the submissionUrl response, parses ${serverMsAddr} from the response body, and MAY use that multiserver address to create a muxrpc connection with the server
  8. If the server receives a muxrpc connection from the client, it MUST authorize it and grant them a tunnel address
  9. The client is now an Internal User

The JSON schemas for which the response from the submissionUrl MUST conform to is shown below.

Successful responses

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://github.com/ssb-ngi-pointer/ssb-http-invite#claimed-json-endpoint-success",
  "type": "object",
  "properties": {
    "status": {
      "title": "Response status tag",
      "description": "Indicates the completion status of this response",
      "type": "string",
      "pattern": "^(successful)$"
    },
    "multiserverAddress": {
      "title": "Multiserver address of the server",
      "description": "Should conform to https://github.com/ssbc/multiserver-address",
      "type": "string"
    }
  },
  "required": [
    "status",
    "multiserverAddress"
  ]
}

Failed responses

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://github.com/ssb-ngi-pointer/ssb-http-invite#claimed-json-endpoint-error",
  "type": "object",
  "properties": {
    "status": {
      "title": "Response status tag",
      "description": "Indicates the completion status of this response",
      "type": "string"
    },
    "error": {
      "title": "Response error",
      "description": "Describes the specific error that occurred",
      "type": "string"
    }
  },
  "required": [
    "status",
    "error"
  ]
}

Programmatic invite façade

As an additional endpoint for programmatic purposes, if the query parameter encoding=json is added to the invite link (for illustration: https://${serverHost}/join?invite=${inviteCode}&encoding=json), then the server SHOULD return a JSON response. The JSON body MUST conform to the following schemas:

Successful responses

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://github.com/ssb-ngi-pointer/ssb-http-invite#invite-json-endpoint-success",
  "type": "object",
  "properties": {
    "status": {
      "title": "Response status tag",
      "description": "Indicates the completion status of this response",
      "type": "string",
      "pattern": "^(successful)$"
    },
    "invite": {
      "title": "Invite code",
      "description": "Sequence of bytes that acts as a token to accept the invite",
      "type": "string"
    },
    "postTo": {
      "title": "Submission URL",
      "description": "URL where clients should submit POST requests with a JSON body",
      "type": "string"
    }
  },
  "required": [
    "status",
    "invite",
    "postTo"
  ]
}

Failed responses

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://github.com/ssb-ngi-pointer/ssb-http-invite#invite-json-endpoint-error",
  "type": "object",
  "properties": {
    "status": {
      "title": "Response status tag",
      "description": "Indicates the completion status of this response",
      "type": "string"
    },
    "error": {
      "title": "Response error",
      "description": "Describes the specific error that occurred",
      "type": "string"
    }
  },
  "required": [
    "status",
    "error"
  ]
}

Example

Suppose the client has the SSB ID @FlieaFef19uJ6jhHwv2CSkFrDLYKJd/SuIS71A5Y2as=.ed25519 and the server is hosted at scuttlebutt.eu. Then the invite user journey is:

  1. Invite code 39c0ac1850ec9af14f1bb73 was generated by the server
  2. The corresponding invite link is https://scuttlebutt.eu/join?invite=39c0ac1850ec9af14f1bb73
  3. When the client opens that link in a browser, it renders a link to the SSB URI ssb:experimental?action=claim-http-invite&invite=39c0ac1850ec9af14f1bb73&postTo=https%3A%2F%2Fscuttlebutt.eu%2Fclaiminvite
  4. The client's SSB app processes the SSB URI and makes a POST request to https://scuttlebutt.eu/claiminvite with body
    {
      "id": "@FlieaFef19uJ6jhHwv2CSkFrDLYKJd/SuIS71A5Y2as=.ed25519",
      "invite": "39c0ac1850ec9af14f1bb73"
    }
    
  5. The server accepts the POST request, and responds with the JSON body
    {
      "status": "successful",
      "multiserverAddress": "net:scuttlebutt.eu:8008~shs:zz+n7zuFc4wofIgKeEpXgB+/XQZB43Xj2rrWyD0QM2M="
    }
    
  6. The server now recognizes the client as an authorized member for any subsequent secret-handshake and muxrpc connections at the multiserver address net:scuttlebutt.eu:8008~shs:zz+n7zuFc4wofIgKeEpXgB+/XQZB43Xj2rrWyD0QM2M=

The JSON endpoint https://scuttlebutt.eu/join?invite=39c0ac1850ec9af14f1bb73&encoding=json is an alternative to the SSB URI, and would respond with the following JSON:

{
  "status": "successful",
  "invite": "39c0ac1850ec9af14f1bb73",
  "postTo": "https://scuttlebutt.eu/claiminvite"
}

After that, the same steps 4, 5, and 6 apply.

Implementation notes

The rendering of the invite façade HTML is unspecified on purpose. Implementors can choose to present the SSB URI either as a link, or as a code to be copied and pasted, or as an automatic redirect.

Furthermore, the invite page is a good place to render instructions on how to install an SSB app, in case the invitee is uninitiated in SSB and this is their entry point.

Specifically, these instructions can also use mobile operating systems deep linking capabilities. For instance, suppose the page recommends installing Manyverse: the page could link to join.manyver.se (with additional query parameters to pass on the invite code), which in turn uses Android Deep Linking redirect (see this technical possibility) to open Manyverse (if it's installed) or open Google Play Store (to install the app). Same idea should apply for mobile apps, say "Imaginary App" using the fixed URL "join.imaginary.app". Desktop apps are different as they can be installed without an app store. This paragraph was informed by Wouter Moraal's UX Research for Manyverse.

Security considerations

Malicious web visitor

A web visitor, either human or bot, could attempt brute force visiting all possible invite URLs, in order to force themselves to become an internal user. However, this could easily be mitigated by rate limiting requests by the same IP address.

Appendix

List of new SSB URIs