Skip to content

Design doc: Outgoing HTTP proxy in sandstorm http bridge

Ian Denhardt edited this page Jan 23, 2020 · 2 revisions

Originally published here: https://oasis.sandstorm.io/shared/MI6IK9GPzpWZXa2t5P1ZvTgo5MwZS_uZnd_RQ6PI6-e


Overview

sandstorm-http-bridge should expose an HTTP proxy back to the app which it can use to:

  • Invoke arbitrary Cap'n Proto APIs using JSON-over-HTTP. We will define a set of Cap'n Proto annotations which can be applied to a Cap'n Proto schema file to describe how the interface should look as a JSON-HTTP interface.
  • Make external unauthenticated HTTP requests, given the proper capabilities.
  • Perform simulated OAuth handshakes to external services like Google, Dropbox, etc. in order to then make external authenticated HTTP requests.

Simulated OAuth

We will simulate OAuth handshakes for well-known services like Google, Github, Dropbox, etc., so that an app which interacts with these services need not change its server-side code. The client-side code will need to change to replace the OAuth request redirect with a powerbox request, which should be an easy substitution. The "request token" returned by the powerbox request (after https://github.com/sandstorm-io/sandstorm/pull/1892 is applied) is equivalent to an "authorization code" as returned by an OAuth request.

On the server, the app will attempt to exchange the authorization code for an access token by contacting the service's OAuth endpoint. The HTTP proxy will intercept this request and instead call SessionContext.claimRequest(). The "access token" returned is actually a SturdyRef for a WebSession. In cases where a "refresh token" is requested, we'll use the same token -- Sandstorm does not have any equivalent of a refresh token but it doesn't really matter.

For example, we will special-case accounts.google.com with the following endpoints:

  • /oauth2/v4/token: endpoint used to exchanged authorization codes and refresh tokens for access tokens
  • /o/oauth2/revoke?token={token}: drop()s the SturdyRef Unfortunately OAuth2 does not standardize the locations of these endpoints, so we will have to maintain a table of them.

Outbound HTTP

Regular outbound HTTP requests may be proxied through the bridge with the following semantics:

  • If the request has an "Authorization: bearer" header: The token is assumed to be a SturdyRef. It is restored and treated as a WebSession capability. This capability is cached for a short time to be reused in future requests with the same token. This path is intended to be used in conjunction with simulated OAuth to obtain the token.
  • If no authorization header: http-bridge maintains a table mapping hostnames to SturdyRefs. The app must pre-initialize this table from powerbox requests; we can't perform a request automatically because we don't know what session to associate it with.
    • TODO: Unclear if maintaining this table is actually a benefit, or if it's easy enough to just tell the apps to send the Authorization header. This table would be ambient authority which is not great.

capnp-json bridge

If the authorization header specifies a SturdyRef for a non-WebSession capability, the capability's interface is automatically converted to a REST-like interface. (Note: For this to be automatic, we need to add dynamic introspection to the Cap'n Proto protocol, which should not be too hard.)

The following annotations could be applied to Cap'n Proto interfaces to define how they are laid out as HTTP endpoints:

  • $http(route = "foo", method = get): Annotate a method to indicate that in the HTTP version of the interface, the method endpoint is "/foo" and expects GET requests.

  • $param(type = path|query|body): Annotate a parameter to specify that the parameter is passed as a path component, query component, or in the JSON body. Path components are expected in the order in which the parameters are defined.

  • $pathPipeline: Annotate a method or specific result field to indicate that a request for a child of this route should descend into this result value. Only works if all the method's parameters are path parameters. This allows e.g. a path like "foo/14/bar/12" to map to "foo(14).bar(12)" -- two calls in one.### All interfaces can also be accessed via a default mapping -- needed in particular when the interface hasn't yet been annotated:

  • route = ".capnp/" + method name

  • always POST

  • all params are body params Requests can contain some special headers to perform capnp-specific operations:

  • X-Capnp-Save: baz=foo.bar; interface=0xabcdef1234567890

    • The field "foo.bar" in the response should be saved persistently and assigned the hostname "baz".
    • Optional parameter "interface" sets the interface ID, in case the default is wrong.
  • X-Capnp-Pipeline: baz=foo.bar

    • The field "foo.bar" in the response should be mapped temporarily to the hostname "baz". Note that certain capabilities are automatically mapped at special-cased hostnames:
  • sandstorm: SandstormApi capability

  • http-bridge: SandstormHttpBridge capability

MVP

To support basic app-to-app powerbox with HTTP APIs, we need:

  • http://http-bridge/session/<id> -- SessionContext corresponding to session ID. Pipeline-friendly.
    • /claim -- claimRequest(), produces a SturdyRef / access token.
  • Treat all bearer tokens as ApiSession SturdyRefs.
Clone this wiki locally