Colin's Journal

Colin's Journal: A place for thoughts about politics, software, and daily life.

October 4th, 2015

HTTP Push Notfications

 

Arran Beach

Arran Beach

Owlauth allows users to register a device (e.g. a phone) and then use this device to confirm their identify when logging into a website or application.  For this to work the Owlauth app running on the phone needs to be able to receive push notifications of authentication requests.  There are many centralised ways to push notifications to a phone, but as each domain owner can run their own Owlauth server, a centralised solution isn’t a good fit.

By taking advantage of Go’s cheap handling of concurrent processing with Goroutines, a simple HTTP based push approach can be used.  Clients connect to the server:

  1. Passing a Bearer token that authorizes them as a registered device.
  2. Passing an If-None-Match ETag if it has one.
  3. The HTTP GET then blocks until either:
    1. The HTTP Keep Alive timeout is approaching – in which case the server returns nothing.
    2. A new authentication request is available or an existing request goes away.
  4. When the GET returns, an ETag header reflecting the current state.
  5. The client loops, issuing a new HTTP GET request with the latest ETag.

The server returns before the Keep Alive timeout, which means that the client will reuse the same TCP (and SSL) session as for the first request.  This makes this timeout and GET operation an effective ping that proves the connection is still established.  If the TCP connection becomes invalid, the client’s HTTP library will open a new connection to the server, giving the desired reconnect behaviour.

The server implementation in Go is straightforward:

        var requestedEtag string
        requestedEtag = r.Header.Get("If-None-Match")

        authChannel := reqStore.RegisterListener(dev.DeviceOwner)
        defer reqStore.UnregisterListener(dev.DeviceOwner, authChannel)
        clientGone := w.(http.CloseNotifier).CloseNotify()
        var request *reqdb.PendingRequests
        timeout := time.NewTimer(DeviceRequestGetTimeout)

        for {
            select {
            case req := <-authChannel:
                if req.GetEtag() != requestedEtag {
                    request = &req
                    // Return the ETag
                    w.Header().Add("ETag", req.GetEtag())
                    return request, nil
                }
                // Just loop and try again.
            case <-clientGone:
                // Nothing to be done - just return
                log.Printf("Device gone - returning\n")
                w.Header().Add("ETag", requestedEtag)
                return nil, nil
            case <-timeout.C:
                log.Printf("Device request get timeout reached.\n")
                w.Header().Add("ETag", requestedEtag)
                return nil, nil
            }
        }

The reqStore object keeps track of outstanding authorisation requests and provides the details on a channel to all registered clients that are sat waiting in this GET.

The only other element of the equation on the client side is adopting a suitable retry strategy when http connections are not working.  For desktops that could be just a simple back-off to a few seconds sleep.  For the Android client it needs to taken into account the current network state to avoid excessive battery drain.

I’ve now got a basic version of this working on my phone.  I’ll run it for a while and see how much battery impact it has.

Copyright 2015 Colin Stewart

Email: colin at owlfish.com