Vendor APIs and keys

For some vendor APIs like OrgSync, we get one API key to access the entire OrgSync API. There are cases where we’d like someone to have access to only parts of the API, say, an orgnization manager who wants to see all the events going on in his orgnization.

The problem is, we only have one master API key, and it gives unrestricted access to the entire vendor API. And at NYU, We don’t like the idea of some IT manager accidentally deleting student accounts or just messing anything up in general.

We want to set up a server to get the orgnization event data for our imaginary IT admin from orgnization whose ID is 42. We want to give them a key to access our server, which is a different key than the vendor API key.

The vendor API call looks like this

GET https://vendor.com/api/v2/orgs/42/events?key=[vendor_api_key]

We want our users be able to call

GET https://ourdomain.com/api/v2/orgs/42/events?key=[our_proxy_api_key]

to get the same results as if they are calling the vendor API directly, but only be able to access the org_ids that we allow them to.

A reverse proxy server

We can write a server to act as a reverse proxy, with some additional access controls.

Basically here is what we want to achieve with a reverse proxy server:

  • Verify client identity
  • Verify client permissions
  • If client has the permission to access the API endpoint
    • Our server composes an HTTP request, with the unrestricted API key, to the vendor API
    • Our server responds to the client with whatever response we got from vendor API
  • If client is not authenticated or does not have the permission to access the API endpoint
    • We return a 401 Unauthroized

Our Gorilla mux router and authentication

We can set up our router like this

func main() {
	r := mux.NewRouter()
	r.HandleFunc("/api/v2/orgs/{org_id}/events", handleGetEvents).Methods("GET")
	http.Handle("/", r)
	log.Fatal(http.ListenAndServe(":3000", nil))
}

We can use the {org_id} in the route to verify if our user has permission to access the API.

func handleGetEvents(w http.ResponseWriter, r *http.Request) {
    key := r.URL.Query().Get("key")
    orgID := mux.Vars(r)["org_id"]
    // write some code here to check if user with `key` has permission to 
    // access the events of `orgID`
}

Now that we have written access control, we need to serve the client with a response from our vendor API.

With just an HTTP client

This can be easily achieved with an HTTP client.

func handleGetEvents(w http.ResponseWriter, r *http.Request) {
    // ...
    // code to verify the proxy key and other parameters
    // ...

    // modify api key in query params
    q := r.URL.Query()
    q.Set("key", orgsyncAPIKey)
    r.URL.RawQuery = q.Encode()

    resp, err := http.Get(r.URL.String())
    if err != nil {
        // handle error here
        return
    }
    defer resp.Body.Close()
    // copy the response to client
    io.Copy(w, resp.Body)
}

Some issues with this method:

  • It does not copy the headers. You can copy the headers by doing something like this
func copyHeader(source http.Header, dest *http.Header){
  for n, v := range source {
      for _, vv := range v {
          dest.Add(n, vv)
      }
  }
}
  • It only works with GET requests
  • It doesn’t handle redirects well
  • Performance sucks

The better way to do this is to use a ReverseProxy, from the built-in httputil package.

With Reverse Proxy

func handleGetEvents(w http.ResponseWriter, r *http.Request) {
    // ...
    // code to verify the proxy key and other parameters
    // ...

    proxy := httputil.NewSingleHostReverseProxy(&url.URL{
        Scheme: "https",
        Host:   "api.orgsync.com",
    })
    proxy.Director = func(r *http.Request) {
        r.Host = "api.orgsync.com"
        r.URL.Host = r.Host
        r.URL.Scheme = "https"
        
        // replace our proxy api key with the real api key
        q := r.URL.Query()
        q.Set("key", orgsyncAPIKey)
        r.URL.RawQuery = q.Encode()
    }

    proxy.ServeHTTP(w, r)
}

It does exactly what a reverse proxy does. We take the client request, replace the API key with the vendor API key, the host with the vendor host, and send the request on client’s behalf.

What have we achieved here?

We generate an API key and give it to the IT manager of orgnization 42, which he can use to access our proxy API. He never sees the real vendor API key, because our reverse proxy fetched the data from vendor on his behalf. We don’t need to cache the responses, and we don’t even need to make a separate API. This puts very little responsibility on our end, yet ensuring security.