6
\$\begingroup\$

As a beginner in Go, I'm seeking feedback on a web framework middleware I wrote up. I reckon that knowledge of the web framework is not needed for the code review.

The goal of this middleware is provide an easy to use library to sign requests received by the web framework. Effectively, there should be a prefix in the Authorization header followed by a space, then an access key followed by a colon and the signature of the message, e.g. the Authorization header could be TEST my_access_key:1234567890abcdef.

My understanding is that OO in Go is done by interfaces, which is why my initial working version (as detailed below) uses this. However, I feel like it's an overkill to start handling a new type of signed header because it requires defining seven functions some of which could be simple parameters. The few functions that really seem like an overkill to me are AuthHeaderPrefix, AuthHeaderRequired, ContextKey and HashFunction. In the best case scenario, I'd like to be able to define those in a struct which already implements the Manager interface. However, the three other functions of the interface are directly depend on the usage of this middleware. For example, the SecretKey function (which validates the header based on the access key) could either check for a value in the database, in a basic data structure like an array, or some other custom variation (in the tests it just checks that the access key is exactly a given string). Because of this, my understanding is that I cannot simply define a struct which only partially implements the interface, and leave the non-trivial functions for the developer to handle, is that right?

Github link

// Package signedauth provides a Gin middleware for checking signed requests.
// Signed requests is a good way to secure endpoint which may alter databases.
package signedauth

import (
    "crypto/hmac"
    "encoding/hex"
    "errors"
    "github.com/gin-gonic/gin"
    "hash"
    "net/http"
    "strings"
)

// Manager defines the functions needed to fulfill an auth key managing role.
type Manager interface {
    AuthHeaderPrefix() string                         // The beginning of the string from the HTTP_AUTHORIZATION header. The exact header must be followed by a space.
    SecretKey(string, *http.Request) (string, *Error) // The secret key for the provided access key and request. Header verification should happen here, and an error returned to fail.
    DataToSign(*http.Request) (string, *Error)        // The data which must be signed and verified, or an error to return.
    AuthHeaderRequired() bool                         // Whether or not a request without any header should be accepted (c.Next) or forbidden (c.AbortWithError with status 403).
    HashFunction() func() hash.Hash                   // Returns the hash function to use, e.g. sha1.New (imported from "crypto/sha1"), or sha512.New384 for SHA-384.
    ContextKey() string                               // The key in the context where will be set the appropriate value if the request was correctly signed.
    ContextValue(string) interface{}                  // The value which will be stored in the context if authentication is successful, from the access key.
}

// Error defines the authentication failure with a status. The error string will *not* be returned by Gin.
type Error struct {
    Status int   // The status for this failure.
    Err    error // The error associated to this failure.
}

// SignatureAuth is the middleware function. It must be called with a struct which implements the Manager interface.
func SignatureAuth(mgr Manager) gin.HandlerFunc {

    return func(c *gin.Context) {
        accesskey, signature, err := extractAuthInfo(mgr, c.Request.Header.Get("Authorization"))
        if err != nil {
            // Credentials doesn't match, we return 401 Unauthorized and abort request.
            c.AbortWithError(err.Status, err.Err)
        } else if accesskey == "" && signature == "" && !mgr.AuthHeaderRequired() {
            c.Next()
        } else {
            // Authorization header has the correct format.
            secret, keyerr := mgr.SecretKey(accesskey, c.Request)
            if keyerr != nil {
                c.AbortWithError(keyerr.Status, keyerr.Err)
            } else {
                data, dataerr := mgr.DataToSign(c.Request)
                if dataerr != nil {
                    c.AbortWithError(dataerr.Status, dataerr.Err)
                } else if !isSignatureValid(mgr.HashFunction(), secret, data, signature) {
                    // Accesskey is valid but signature is not.
                    c.AbortWithError(http.StatusUnauthorized, errors.New("wrong access key or signature"))
                } else {
                    // Accesskey and signature are valid.
                    c.Set(mgr.ContextKey(), mgr.ContextValue(accesskey))
                    c.Next()
                }
            }
        }
    }
}

// extractAuthInfo extracts the authentication information from the provided auth string.
func extractAuthInfo(mgr Manager, auth string) (string, string, *Error) {
    if strings.HasPrefix(auth, mgr.AuthHeaderPrefix()+" ") {
        splitheader := strings.Split(auth, " ")
        if len(splitheader) != 2 {
            return "", "", &Error{http.StatusUnauthorized, errors.New("invalid authorization header")}
        }

        splitauth := strings.Split(splitheader[1], ":")
        if len(splitauth) != 2 {
            return "", "", &Error{http.StatusUnauthorized, errors.New("invalid format for access key and signature")}
        }
        return splitauth[0], splitauth[1], nil

    } else if mgr.AuthHeaderRequired() {
        return "", "", &Error{http.StatusUnauthorized, errors.New("invalid authorization header")}
    } else {
        return "", "", nil
    }

}

// isSignatureValid signs the request with the provided secret, and returns that signature.
func isSignatureValid(hashFunc func() hash.Hash, secret string, data string, signature string) bool {
    hash := hmac.New(hashFunc, []byte(secret))
    hash.Write([]byte(data))
    return hex.EncodeToString(hash.Sum(nil)) == signature
}

Usage example

Full test on Github

SHA1 HMAC

This shows an example of a SHA1 HMAC header based auth.

// StrictSHA1Manager is an example definition of an AuthKeyManager struct.
type StrictSHA1Manager struct {
    Required bool
    Prefix   string
    Secret   string
    Key      string
    Value    interface{}
}

// AuthHeaderPrefix returns the prefix used in the initialization.
func (mgr StrictSHA1Manager) AuthHeaderPrefix() string {
    return mgr.Prefix
}

// SecretKey returns the secret key from the provided access key.
// Here should reside additional verifications on the header, or other parts of the request, if needed.
func (mgr StrictSHA1Manager) SecretKey(access string, req *http.Request) (string, *Error) {
    if req.ContentLength != 0 && req.Body == nil {
        // Not sure whether net/http or Gin handles these kinds of fun situations.
        return "", &Error{400, errors.New("received a forged packet")}
    }
    // Grabbing the date and making sure it's in the correct format and is within fifteen minutes.
    dateHeader := req.Header.Get("Date")
    if dateHeader == "" {
        return "", &Error{406, errors.New("no Date header provided")}
    }
    date, derr := time.Parse("2006-01-02T15:04:05.000Z", dateHeader)
    if derr != nil {
        return "", &Error{408, errors.New("could not parse date")}
    } else if time.Since(date) > time.Minute*15 {
        return "", &Error{410, errors.New("request is too old")}
    }

    // The headers look good, let's check the access key.
    // If the reading the access key requires any kind of IO (database, or file reading, etc.)
    // it's quite good to only verify if that access key is valid once all the checks are done.
    if access == "my_access_key" {
        return mgr.Secret, nil
    }
    return "", &Error{418, errors.New("you are a teapot")}
}

// ContextKey returns the key which will store the return from ContextValue() in Gin's context.
func (mgr StrictSHA1Manager) ContextKey() string {
    return mgr.Key
}

// ContextValue returns the value to store in Gin's context at ContextKey().
func (mgr StrictSHA1Manager) ContextValue(access string) interface{} {
    if access == "my_access_key" {
        return "All good with my access key!"
    }
    return "All good with any access key!"
}

// AuthHeaderRequired returns true because we want to forbid any non-signed request in this group.
func (mgr StrictSHA1Manager) AuthHeaderRequired() bool {
    return mgr.Required
}

// HashFunction returns sha1.New. It could return sha512.New384 for example (SHA-1 has known theoretical attacks).
func (mgr StrictSHA1Manager) HashFunction() func() hash.Hash {
    return sha1.New
}

// DataToSign returns a string representing the data which will be HMAC'd with the secret and used to check
// authenticity of the request. This function is only called once all the parameters for the request are valid.
func (mgr StrictSHA1Manager) DataToSign(req *http.Request) (string, *Error) {
    // In this example, we'll be implementing a similar signing method to the Amazon AWS REST one.
    // We'll use the HTTP-Verb, the MD5 checksum of the Body, if any, and the Date header in ISO format.
    // http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html
    // Note: We are returning a variety of error codes which don't follow the spec only for the purpose of testing.
    serializedData := req.Method + "\n"
    if req.ContentLength != 0 {
        body, err := ioutil.ReadAll(req.Body)
        if err != nil {
            return "", &Error{402, errors.New("could not read the body")}
        }
        hash := md5.New()
        hash.Write(body)
        serializedData += hex.EncodeToString(hash.Sum(nil)) + "\n"
    } else {
        serializedData += "\n"
    }
    // We know from SecretKey that the Date header is present and fits our time constaints.
    serializedData += req.Header.Get("Date")

    return serializedData, nil
}

Access key passed auth

In this example, we don't want to check for a secret key but only check whether the provided access key is valid. This can be used to validate nonces in requests. As you can see, even this definition has many ridiculously simple functions.

// EmptyManager is an example definition of an AuthKeyManager struct.
type EmptyManager struct {
}

// EmptyManager returns the prefix used in the initialization.
func (mgr EmptyManager) AuthHeaderPrefix() string {
    return "EMPTY"
}

// SecretKey returns the secret key from the provided access key.
func (mgr EmptyManager) SecretKey(access string, req *http.Request) (secret string, err *Error) {
    secret = "" // There is no secret key, just an access key.
    if access == "valid" {
        err = nil
    } else {
        err = &Error{403, errors.New("invalid access key")}
    }
    return
}

// ContextKey returns the key which will store the return from ContextValue() in Gin's context.
func (mgr EmptyManager) ContextKey() string {
    return "allGood"
}

// ContextValue returns the value to store in Gin's context at ContextKey().
func (mgr EmptyManager) ContextValue(access string) interface{} {
    return true
}

// AuthHeaderRequired returns true because we want to forbid any non-signed request in this group.
func (mgr EmptyManager) AuthHeaderRequired() bool {
    return true
}

// HashFunction returns sha1.New.
func (mgr EmptyManager) HashFunction() func() hash.Hash {
    return sha1.New
}

// DataToSign returns an empty string. This allows us to test that we can only check for a valid access key.
func (mgr EmptyManager) DataToSign(req *http.Request) (string, *Error) {
    return "", nil
}

Best case scenario

The best solution I currently see, but I'm not sure if it's even valid in Go, would be to define structs which partially implement the interface, in order to leave the more complex function definitions to the user.

Definition in library

type StrictSHA1Manager struct {
    Required bool
    Prefix   string
    Secret   string
    Key      string
    Value    interface{}
}


// AuthHeaderPrefix returns the prefix used in the initialization.
func (mgr StrictSHA1Manager) AuthHeaderPrefix() string {
    return mgr.Prefix
}

// ContextKey returns the key which will store the return from ContextValue() in Gin's context.
func (mgr StrictSHA1Manager) ContextKey() string {
    return mgr.Key
}

// AuthHeaderRequired returns true because we want to forbid any non-signed request in this group.
func (mgr StrictSHA1Manager) AuthHeaderRequired() bool {
    return mgr.Required
}

// HashFunction returns sha1.New. It could return sha512.New384 for example (SHA-1 has known theoretical attacks).
func (mgr StrictSHA1Manager) HashFunction() func() hash.Hash {
    return sha1.New
}

// NewSHA1HMACManager lacks documentation.
func NewSHA1HMACManager(prefix string, contentKey string, required bool) *StrictSHA1Manager{
    return &StrictSHA1Manager{Prefix: prefix, Key: contextKey, Required: required}
}

Usage example?

Hence, I would expect developers to use it as follows.

Note: the following is written on the fly without testing so it may be horrendous.

type MyDerivedManager struct {
    someField interface{}
    *signedauth.StrictSHA1Manager
}

// Here goes the additional function definitions.
func (mgr MyDerivedManager) DataToSign(req *http.Request) (string, *Error) {
    // TODO: everything.
}
func (mgr StrictSHA1Manager) ContextValue(access string) interface{} {
    // TODO: everything.
}
func (mgr StrictSHA1Manager) SecretKey(access string, req *http.Request) (string, *Error) {
    // TODO: everything.
}

func main(){
    mgr := &MyDerivedManager{someField: {}, *signedauth.NewSHA1HMACManager("PREFIX", "contentKey", true)}
}

Does the "best case scenario" paragraph have idiomatic Go, and is it actually the best case scenario?

\$\endgroup\$
2
  • \$\begingroup\$ I confirm that Go allows to have partially defined structs, which are then completed by other structs which embed it. Pretty cool! =D \$\endgroup\$
    – ChrisR
    Aug 3, 2015 at 14:19
  • \$\begingroup\$ I've decided to go with embedded interfaces which greatly simplify definitions of new managers: github.com/ChristopherRabotin/gin-contrib-signedauth/pull/10 . \$\endgroup\$
    – ChrisR
    Aug 3, 2015 at 19:49

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.