Middlewares providing request and response validation against an OpenAPI spec at runtime, optionally integrating with the Firetail SaaS. Packages containing middleware for various different frameworks can be found in the middlewares directory, and examples of their use in examples.
Get the middleware:
go get github.com/FireTail-io/firetail-go-lib/middlewares/http
Import it:
import firetail "github.com/FireTail-io/firetail-go-lib/middlewares/http"
Create a middleware using GetMiddleware
; see the Options
struct for all the available configurations:
firetailMiddleware, err := firetail.GetMiddleware(
&firetail.Options{
OpenapiSpecPath: path,
LogApiKey: apiToken,
},
)
if err != nil {
// Handle the err...
}
You will then have a func(next http.Handler) http.Handler
, firetailMiddleware
, which you can use to wrap a http.Handler
just the same as with the middleware from net/http/middleware
. This should also be suitable for Chi.
See the Go reference for the Options struct for documentation regarding the available options. For example, if you are using us.firetail.app
you will need to set the LogsApiUrl
to https://api.logging.us-east-2.prod.firetail.app/logs/bulk
.
Automated testing is setup with the testing
package, using github.com/stretchr/testify for shorthand assertions. You can run them with go test
.
The Firetail Go library does not come with XML request & response body decoding support out of the box. You will need to implement your own decoder as an openapi3filter.BodyDecoder and pass it to Firetail as part of the CustomBodyDecoders
field of the firetail.Options
struct. See the following example for a minimal XML decoder setup using sbabiv/xml2map:
middleware, err := firetail.GetMiddleware(&firetail.Options{
OpenapiSpecPath: "./app-spec.yaml",
CustomBodyDecoders: map[string]openapi3filter.BodyDecoder{
"application/xml": func(r io.Reader, h http.Header, sr *openapi3.SchemaRef, ef openapi3filter.EncodingFn) (interface{}, error) {
return xml2map.NewDecoder(r).Decode()
},
},
})
If you use securitySchemes
in your OpenAPI specification, you will need to populate the firetail.Options
struct's AuthCallbacks
field with a callback for each security scheme implementing your authentication logic.
For example, for the following securitySchemes
:
components:
securitySchemes:
MyBasicAuth:
type: http
scheme: basic
Your AuthCallback
could look like this:
AuthCallbacks: map[string]func(context.Context, *openapi3filter.AuthenticationInput){
"MyBasicAuth": func(ctx context.Context, ai *openapi3filter.AuthenticationInput) error {
token := ai.RequestValidationInput.Request.Header.Get("Authorization")
return validateBasicAuthToken(token)
},
},
In order to customise the errors returned by your application when a request fails to authenticate, you can pick up the errors returned by your AuthCallbacks
in a custom ErrHandler
. This allows you to, for example, add the WWW-Authenticate
header on responses to requests that fail to validate against a basic auth security requirement:
// We'll use this err when the basic auth fails to validate.
var BasicAuthErr = errors.New("invalid authorization token")
firetailMiddleware, err := firetail.GetMiddleware(&firetail.Options{
OpenapiSpecPath: "app-spec.yaml",
// First, let's write our auth callback which, if the name of the security scheme it's being asked to check
// is 'MyBasicAuth', will check that the Authorization header contains the b64 encoding of 'admin:password'.
AuthCallbacks: map[string]func(context.Context, *openapi3filter.AuthenticationInput){
"MyBasicAuth": func(ctx context.Context, ai *openapi3filter.AuthenticationInput) error {
token := ai.RequestValidationInput.Request.Header.Get("Authorization")
if token != "Basic YWRtaW46cGFzc3dvcmQ=" {
return BasicAuthErr
}
return nil
},
},
// Then, in our ErrCallback, we can check if the error it's received is a security error. If it is, and
// one of its suberrs is a BasicAuthErr, then we can add the WWW-Authenticate header to the response.
ErrCallback: func(err firetail.ErrorAtRequest, w http.ResponseWriter, r *http.Request) {
if securityErr, isSecurityErr := err.(firetail.ErrorAuthNoMatchingSchema); isSecurityErr {
for _, subErr := range securityErr.Err.Errors {
if subErr == BasicAuthErr {
w.Header().Add("WWW-Authenticate", "Basic")
break
}
}
}
w.Header().Add("Content-Type", "text/plain")
w.WriteHeader(err.StatusCode())
w.Write([]byte(err.Error()))
},
})