Login with: Google Twitter Facebook Local

Avoiding The Needless Multiplication Of Forms

Implementing interactive features

Dr Andrew Moss

2019-08-20

Part 2: Identifying sessions with MACs

The rough plan for this series of posts is to describe the different pieces of code in the front- and back-ends that enable interactivity.

Sessions and cookies

After a user has been authenticated we store their details in the following struct:
type Session struct { Sub string Profile string Email string Name string Token *oauth2.Token Provider string }
The first fields were retrieved from the identity provider when the user logged in. The Subfield is an opaque value that identifies the user to the service (i.e. a subscriber number), while the other three are useful for displaying their comments. The Tokenis our access code for the identity service, and the Provideris just an enumeration of identity providers.
Sessions are stored as values in a map. The keys are the login cookie that we have set in the user browser. Their raw form looks like this:
Google|12345|...binary data....
This gives us the identity provider and their opaque id (subscriber number in this case), followed by a MAC (Message Authentication Code). We can think of the MAC as being a signature that proves that we generated the cookie. Sessions are not meant to persist across server restarts so the key for the MAC is just a random 256-bit value picked when the server starts. Because the MAC is a binary stream and we want to store this key as a string we simply base-64 encode the raw form (byte-slice).
MACs are difficult to get right - subtle changes in the sequence of operations and the order of the substrings can change their security properties. All the details are in Keying hash functions for message authentication and luckily there is already an implementation in the standard Go libraries. Use of a HMAC in Go is very simple. At the end of the callback handler (described in the OAuth2 post) we did this:
loginKey := fmt.Sprintf("%s|%s", provider, userInfo.Sub) encLogin := msgMac(loginKey)
This relies on a HMAC initialised to a random key. If we can't do this (e.g. /dev/random didn't start properly in the service jail) then we bail. This successfully follows the two main rules of crypto: #1 Randomize your IVs, #2 Don't be Sony.
hmacKey = make([]byte,32) _,err := rand.Read(hmacKey) if err!=nil { fmt.Printf("Can't initialise the random hmac key! %s\n", err.Error()) return } stateHmac = hmac.New(sha256.New,hmacKey)
All we need to do to get our session key is apply the MAC and base-64 encode the result:
func msgMac(msg string) string { stateHmac.Reset() stateHmac.Write([]byte(msg)) mac := stateHmac.Sum(nil) state := fmt.Sprintf("%s|%s",msg,mac) return base64.StdEncoding.EncodeToString([]byte(state)) }
When a user logs in, this key is written as their login cookie. It makes finding the session easy:
func Find(req *http.Request) (*Session,[]byte) { var session *Session = nil token,err := req.Cookie("login") if err==nil { loginKey,ok := checkMac(token.Value) if ok { session, _ = sessions[string(loginKey)] if session==nil { fmt.Printf("Error? %s %s\n", session, sessions ) } } else { fmt.Printf("Login %v failed mac check\n",token.Value) } } if session==nil { session = &Session{Name:"guest",provider:"none"} } sessionBar := session.GenerateBar() return session, sessionBar }
The sessionBaris the grey strip across the top of the page that allows logging in / out and shows the current state. If the cookie is valid (the MAC passed the check) and it matches a current session then we show the logged in version. In all other cases we show the logged out version and let the user login (overwriting any stale state). We do not show any intermediate states to the user, and if anything goes wrong (i.e. the cookie is stale) then diagnostics go to the logs, not back to the browser.
Checking the MAC is valid means regenerating the code from the subscriber number and provider inside the raw form of the cookie:
func checkMac(mac string) ([]byte, bool) { raw,err := base64.StdEncoding.DecodeString(mac) fmt.Printf("checkMac: mac %d bytes -> %d %v/%v\n", len(mac), len(raw), mac, raw) if err!=nil { fmt.Println("checkMac failed to base64 decode state") return nil, false } firstSplit := bytes.IndexByte(raw,'|') if firstSplit==-1 { return nil, false } secondSplit := firstSplit + 1 + bytes.IndexByte(raw[firstSplit+1:],'|') msg := raw[:secondSplit] oldSig := raw[secondSplit+1:] fmt.Printf("Checking: %s | %v as mac\n", string(msg), oldSig) stateHmac.Reset() stateHmac.Write([]byte(msg)) newSig := stateHmac.Sum(nil) match := hmac.Equal(oldSig,newSig) if !match { fmt.Printf("checkMac failed to match sig %v vs %v",oldSig,newSig) } return msg, match }
In the handlers for incoming GET/POST requests we can now identify the ongoing Session for a logged in user. In order to hijack a session an adversary would need to perform one of the (hopefully difficult tasks):
The difficuly of these tasks gives us a reasonable level of security, while the lookup code on a request handler is simple, just call the Findprocedure above. Next time I will describe the Websocket and its protocol...

Comments

Markdown syntax...

Sign in at the top of the page to leave a comment