vangoh

package module
v0.0.0-...-560b874 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 6, 2018 License: MIT Imports: 14 Imported by: 0

README

Vangoh GoDoc

The Vanilla Go HMAC handler

Vangoh implements the HMAC authentication scheme popularized by Amazon's AWS.

In order to compute and check the HMAC signature, it checks the details included in a request's Authorization header. It expects them to be in the form

Authorization: [ORG] [KEY]:[HMAC_SIGNATURE]

Where:

  • [ORG] is the organization tag, e.g., AWS.
  • [KEY] is the unique, public identifier of the user or service, such as a username or API key, e.g., EKsZs7eRe9z3q79KsZmoK1plHAB0UC.
  • [HMACSignature] is the base64-encoded HMAC signature of various details of the request, e.g., 44IVL7XT3zqe2+/6tUbe39Da4Hq2MK==.

Much like the AWS HMAC implementation, Vangoh allows for specifying additional headers to be used when calculating the hash, and incorporates the Date header to assist in identifying duplicate requests. Read the AWS documentation and the code to see the details of constructing the signature.

There is also support of separate authentication sources for multiple organizational tags, permitting varying authentication for standard users vs. privileged API access. An internal service making several requests may authenticate through an in-memory map, for instance, while a standard user request may authenticate through a less performant but more scalable database lookup of their private key.

Vangoh was designed to conform to the net/http package, and includes two "middleware"-esque functions (vangoh.Handler, vangoh.NegroniHandler) for easy integration with your existing code.

Documentation

Full documentation is hosted at Godoc.

Example usage
package main

import (
  "crypto"
  _ "crypto/sha1"
  "fmt"
  "net/http"

  "github.com/auroratechnologies/vangoh"
)

type inMemoryKeyProvider struct {
  keyMap map[string][]byte
}

// A SecretProvider must implement the GetSecret method. In this case it's a
// simple in-memory map, although it could easily be a database connection or
// any other implementation.
func (imkp *inMemoryKeyProvider) GetSecret(identifier []byte) ([]byte, error) {
  key, found := imkp.keyMap[string(identifier)]
  if !found {
    // Returning nil, nil indicates to Vangoh that key was not found
    return nil, nil
  }
  return key, nil
}

// Any net/http handler or handlerFunc that you want
var apiEndpoint = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  w.WriteHeader(http.StatusOK)
  fmt.Fprintln(w, "Hello, authenticated world!")
})

func main() {
  // Declaring the in memory map of user IDs to Secret Keys
  var userMap = make(map[string][]byte)
  userMap["exampleUserID"] = []byte("SUPERSECRETEXAMPLEKEY")

  // Making our example SecretKeyProvider with the map
  userProvider := &inMemoryKeyProvider{
    keyMap: userMap,
  }

  // Creating our new Vangoh instance
  vg := vangoh.New()
  // Setting the hashing algorithm to SHA-1
  vg.SetAlgorithm(crypto.SHA1.New)
  // Linking the "EXMP" organization tag to the example SecretProvider
  _ = vg.AddProvider("EXMP", userProvider)
  // Adding the custom headers (as a regex) that we want to include in our
  // signature computation.
  vg.IncludeHeader("^X-Exmp-.*")

  // Route the handler through Vangoh, and ListenAndServer as usual
  app := vg.Handler(apiEndpoint)
  http.ListenAndServe("0.0.0.0:3000", app)
}

And with that we have replicated the core authentication procedure used by AWS!

Documentation

Overview

Vangoh is a library designed to easily enable go web servers to be secured using HMAC authentication.

Vangoh stands for Vanilla Go HMAC, and it is just that. It makes use of nothing apart from the Go core libraries to provide a robust and flexible solution for adding HMAC request authentication to a new or pre-existing web stack.

It was designed to implement the HMAC scheme that was popularized by Amazon's AWS as described in detail here - http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html

The primary characteristic of this implementation is that the signature is placed In the "Authorization" header, along with the access ID, and a tag for the organization.

Apart from implementing the signature-computing scheme defined by AWS, Vangoh also includes support for multiple different secret key providers within one instance, allowing flexibility in how different users and requests are authenticated. In addition, Vangoh allows for choice in the hashing algorithm that your HMAC implementation uses. The constructors default the algorithm used to SHA256, but it can be easily configured to support any class that implements hash.Hash

Vangoh is designed to fit in with the conventions of the net/http package, meaning that integration with middleware stacks is easy, regardless of other software in use.

Index

Constants

View Source
const AuthRegex = "^[A-Za-z0-9_]+ [A-Za-z0-9_/+]+:" +
	"(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"

Expected regex format of the Authorization signature.

An authorization signature consists of three parts:

Authorization: [ORG] [KEY]:[HMAC_SIGNATURE]

The first component is an organizational tag, which must consist of at least one character, and has a valid character set of alphanumeric characters and underscores.

This should be followed by a single space, and then the key, which also must consist of one or more alphanumeric characters and/or underscores.

The key must be followed by a single colon ':' character, and then the signature, encoded in Base64 (valid characters being all alphanumeric, plus "+", forward slash "/", and equals sign "=" as padding on the end if needed.)

Any leading or trailing whitespace around the header will be trimmed before validation.

Variables

View Source
var ErrorAuthHeaderMalformed = &AuthenticationError{
	c: http.StatusBadRequest,
	s: "Authorization header does not match expected format",
}
View Source
var ErrorAuthHeaderMissing = &AuthenticationError{
	c: http.StatusBadRequest,
	s: "Missing 'Authorization' header",
}
View Source
var ErrorAuthOrgUnknown = &AuthenticationError{
	c: http.StatusBadRequest,
	s: "Authentication organization is not recognized",
}
View Source
var ErrorDateHeaderMalformed = &AuthenticationError{
	c: http.StatusBadRequest,
	s: "Date header is not a valid format",
}
View Source
var ErrorDateHeaderMissing = &AuthenticationError{
	c: http.StatusBadRequest,
	s: "Missing 'Date' header",
}
View Source
var ErrorDateHeaderTooSkewed = &AuthenticationError{
	c: http.StatusForbidden,
	s: "Date header's value is too old",
}
View Source
var ErrorHMACSignatureMismatch = &AuthenticationError{
	c: http.StatusForbidden,
	s: "HMAC signature does not match",
}
View Source
var ErrorInProviderKeyLookup = &AuthenticationError{
	c: http.StatusInternalServerError,
	s: "Unable to look up secret key",
}
View Source
var ErrorSecretNotFound = &AuthenticationError{
	c: http.StatusForbidden,
	s: "Authentication key is not recognized",
}
View Source
var SupportedDateFormatNames = []string{
	time.RFC822,
	time.RFC822Z,
	time.RFC850,
	time.ANSIC,
	time.RFC1123,
	time.RFC1123Z,
}

The names of the supported formats for the timestamp in the Date HTTP header. If the timestamp does not match one of these formats, the request will fail the authorization check.

Functions

func AddAuthorizationHeader

func AddAuthorizationHeader(vg *Vangoh, r *http.Request, org string, key []byte, secret []byte)

func AddCustomDateHeader

func AddCustomDateHeader(r *http.Request, headerName string)

func AddDateHeader

func AddDateHeader(r *http.Request)

Types

type AuthenticationError

type AuthenticationError struct {
	// contains filtered or unexported fields
}

func (*AuthenticationError) Error

func (a *AuthenticationError) Error() string

func (*AuthenticationError) StatusCode

func (a *AuthenticationError) StatusCode() int

func (*AuthenticationError) WriteResponse

func (a *AuthenticationError) WriteResponse(w http.ResponseWriter, debug bool)

type CallbackPayload

type CallbackPayload struct {
	// contains filtered or unexported fields
}

func (*CallbackPayload) GetPayload

func (c *CallbackPayload) GetPayload() interface{}

func (*CallbackPayload) SetPayload

func (c *CallbackPayload) SetPayload(p interface{})

type SecretProvider

type SecretProvider interface {
	GetSecret(key []byte) ([]byte, error)
	// contains filtered or unexported methods
}

A SecretProvider only needs to implement the secret lookup method as described above.

type SecretProviderWithCallback

type SecretProviderWithCallback interface {
	GetSecret(key []byte, cbPayload *CallbackPayload) ([]byte, error)
	SuccessCallback(r *http.Request, cbPayload *CallbackPayload)
	// contains filtered or unexported methods
}

type Vangoh

type Vangoh struct {
	// contains filtered or unexported fields
}

Vangoh is an object that forms the primary point of configuration of the middleware HMAC handler. It allows for the configuration of the hashing function to use, the headers (specified as regexes) to be included in the computed signature, and the mapping between organization tags and the secret key providers associated with them.

func New

func New() *Vangoh

Creates a new Vangoh instance with no secret providers.

func NewSingleProvider

func NewSingleProvider(provider secretProvider) *Vangoh

Creates a new Vangoh instance that supports a single secretProvider. Attempting to add providers with AddProvider will fail with an error.

func (*Vangoh) AddProvider

func (vg *Vangoh) AddProvider(org string, skp secretProvider) error

AddProvider sets the secret provider of a specific organization. If the Vangoh instance was created to use a single provider for all requests, regardless of organization tag, calling AddProvider will fail and return an error. If the organization already has a provider, calling AddProvider will fail and return an error.

By supporting different providers based on org tags, there is the ability to configure authentication sources based on user type or purpose. For instance, if an endpoint is going to be used by both a small set of internal services as well as external users, you could create a different provider for each, as demonstrated below.

Example:

func main() {
	// Create provider for internal services credentials (not included with Vangoh).
	internalProvider := providers.NewInMemoryProvider(...)
	// Create provider for normal user credentials (not included with Vangoh).
	userProvider := providers.NewDatabaseProvider(...)

	vg := vangoh.New()
	_ = vg.AddProvider("INT", internalProvider)
	_ = vg.AddProvider("API", userProvider)

	// ...
}

In this example, any connections made with the authorization header "INT [userID]:[signature]" will be authenticated against `internalProvider`, and connections with the header "API [userID]:[signature]" will be authenticated against `userProvider`.

func (*Vangoh) AuthenticateRequest

func (vg *Vangoh) AuthenticateRequest(r *http.Request) *AuthenticationError

Checks a request for proper authentication details, returning the relevent error if the request fails this check or nil if the request passes.

func (*Vangoh) ConstructBase64Signature

func (vg *Vangoh) ConstructBase64Signature(r *http.Request, secret []byte) string

func (*Vangoh) ConstructSignature

func (vg *Vangoh) ConstructSignature(r *http.Request, secret []byte) []byte

func (*Vangoh) CreateSigningString

func (vg *Vangoh) CreateSigningString(r *http.Request) string

CreateSigningString creates the string used for signature generation, in accordance with the specifications as laid out in the package documentation. Refer there for more detail, or to the Amazon Signature V2 documentation: http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html.

func (*Vangoh) GetDebug

func (vg *Vangoh) GetDebug() bool

func (*Vangoh) Handler

func (vg *Vangoh) Handler(h http.Handler) http.Handler

Protect a `http.Handler` from unauthenticated requests. The wrapped handler will only be called if the request contains a valid Authorization header.

Example:

func main() {
	// Create a new Vangoh instance.
	vg := vangoh.New()

	// Assuming the endpoint to be protected is called 'baseHandler'.
	protectedHandler := vg.Handler(unprotectedHandler)

	// Works just like any other `http.Handler`.
	http.ListenAndServe("0.0.0.0:3000", protectedHandler)
}

func (*Vangoh) IncludeHeader

func (vg *Vangoh) IncludeHeader(headerRegex string) error

IncludeHeader specifies additional headers to include in the construction of the HMAC signature body for a request.

Given a regex, any non-canonical (e.g. "X-Aur", not "x-aur") headers that match the regex will be included.

For instance, to match all headers beginning with "X-Aur-", we could include the header regex "X-Aur-.*". It is important to note that this funcationality uses traditional, non-POSIX regular expressions, and will add anchoring to the provided regex if it is not included.

This means that the regex "X-Aur" will only match headers with key "X-Aur" exactly. In order to do prefix matching you must add a wildcard match after, i.e. "X-Aur.*"

func (*Vangoh) NegroniHandler

func (vg *Vangoh) NegroniHandler(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)

Implements the `negroni.Handler` interface, for use as a middleware.

Example:

func main() {
	mux := http.NewServeMux()

	// Create a new Vangoh instance.
	vg := vangoh.New()

	// Create a new Negroni instance, with the standard Recovery and Logger
	// middlewares.
	n := negroni.New(
		negroni.NewRecovery(),
		negroni.NewLogger(),
		negroni.HandlerFunc(vg.NegroniHandler))

	// Run the app.
	n.UseHandler(mux)
	n.Run(":3000")
}

func (*Vangoh) SetAlgorithm

func (vg *Vangoh) SetAlgorithm(algorithm func() hash.Hash)

func (*Vangoh) SetCustomDateHeader

func (vg *Vangoh) SetCustomDateHeader(headerName string)

func (*Vangoh) SetDebug

func (vg *Vangoh) SetDebug(debug bool)

func (*Vangoh) SetMaxTimeSkew

func (vg *Vangoh) SetMaxTimeSkew(timeSkew time.Duration)

SetMaxTimeSkew sets the maximum allowable duration between the date and time specified in the Date header and the server time when the response is processed. If the date in the header exceeds the duration Vangoh will respond to the request with a HTTP status 403 Forbidden.

To match the behavior of AWS (15 minute skew window):

vg := vangoh.New()
vg.SetMaxTimeSkew(time.Minute * 15)

When checking the date header, Vangoh follows the precedent of RFC 2616, accepting dates in any of the following formats:

ANSIC    = "Mon Jan _2 15:04:05 2006"
RFC822   = "02 Jan 06 15:04 MST"
RFC822Z  = "02 Jan 06 15:04 -0700"
RFC850   = "Monday, 02-Jan-06 15:04:05 MST"
RFC1123  = "Mon, 02 Jan 2006 15:04:05 MST"
RFC1123Z = "Mon, 02 Jan 2006 15:04:05 -0700"

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL