Skip to content

Commit

Permalink
feat: use standard webhooks library instead
Browse files Browse the repository at this point in the history
  • Loading branch information
joel authored and joel committed Mar 1, 2024
1 parent a0b1b5c commit eca7134
Showing 1 changed file with 42 additions and 47 deletions.
89 changes: 42 additions & 47 deletions internal/api/hooks.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"bytes"
"context"
"encoding/json"
"fmt"
Expand All @@ -10,9 +11,10 @@ import (
"net/url"
"strings"
"time"
"bytes"

"github.com/gofrs/uuid"
standardwebhooks "github.com/standard-webhooks/standard-webhooks/libraries/go"
"github.com/supabase/auth/internal/conf"

"github.com/sirupsen/logrus"
"github.com/supabase/auth/internal/hooks"
Expand Down Expand Up @@ -58,44 +60,63 @@ func (a *API) runHook(ctx context.Context, name string, input, output any) ([]by
return response, nil
}

func (a *API) runHTTPHook(ctx context.Context, hookURI string, input, output any) ([]byte, error) {
func (a *API) runHTTPHook(ctx context.Context, hookConfig conf.ExtensibilityPointConfiguration, input, output any) ([]byte, error) {
timeout := defaultTimeout
client := http.Client{
Timeout: timeout,
}
var hookLog *logrus.Entry
hooklog := logrus.WithFields(logrus.Fields{
"component": "auth_hook",
"url": hookURI,
"url": hookConfig.URI,
// "event": .. ? TBD
})

defaultHookRetries := 3
signatureList := "" // TODO: This needs to be replaced with the endpoint specific secret
// Log something to indicate that a hook is called. Ensure this can be used for debugging
hooklog.Infof("hook invoked")

if isOverSizeLimit(input.([]byte)) {
// TODO: adjust to error codes
return nil, internalServerError("Over size limit")
}

for i := 0; i < defaultHookRetries; i++ {
hooklog.Infof("invocation attempt: %d", i)

// Don't need to sign payload
req, err := http.NewRequest(http.MethodPost, w.URL, bytes.NewBuffer(w.payload))
req, err := http.NewRequest(http.MethodPost, hookConfig.URI, bytes.NewBuffer(input.([]byte)))
if err != nil {
return nil, internalServerError("Failed to make request object").WithInternalError(err)
}
watcher, req := watchForConnection(req)
start := time.Now()
req = setHookHeaders(req, signatureList)

req.Header.Set("Content-Type", "application/json")

msgID := uuid.Must(uuid.NewV4())
currentTime := time.Now().Unix()
var signatureList = []string{}

//TODO: If secret rotation is enabled we use two secrets
for _, secret := range hookConfig.HTTPHookSecrets {
wh, err := standardwebhooks.NewWebhook(secret)
if err != nil {
return nil, err
}
signature, err := wh.Sign(msgID, input, currentTime)
if err != nil {
return nil, err
}
signatureList = append(signatureList, signature)

}

req.Header.Set("webhook-id", fmt.Sprintf("msg_%s", msgID))
req.Header.Set("webhook-timestamp", fmt.Sprintf("%d", currentTime))
req.Header.Set("webhook-signature", strings.Join(signatureList, ", "))

watcher, req = watchForConnection(req)
rsp, err := client.Do(req)
if err != nil {
if terr, ok := err.(net.Error); ok && terr.IsTimeout() {
// if i == w.Retries-1 {
// closeBody(rsp)
// return nil, httpError(http.StatusGatewayTimeout, "Failed to perform webhook in time frame (%v seconds)", timeout.Seconds())
// }
if terr, ok := err.(net.Error); ok && terr.Timeout() {
hooklog.Info("Request timed out")
continue
} else if watcher.gotConn {
Expand All @@ -121,16 +142,19 @@ func (a *API) runHTTPHook(ctx context.Context, hookURI string, input, output any
// }
return body, nil
default:
// Check if response is too large, if so retrun error
// If it's a failed request check IsRetryable() via retry-after header on rsp

// if rsp.Header.Get('retry-after') != "" {
// // If it's a failed request check IsRetryable() via retry-after header on rsp

// }
hookLog.Infof("Bad response for hook %d in %s", rsp.StatusCode, dur)
}
}

}
return nil, nil

}

func watchForConnection(req *http.Request) (*connectionWatcher, *http.Request) {
w := new(connectionWatcher)
t := &httptrace.ClientTrace{
Expand Down Expand Up @@ -159,35 +183,6 @@ func closeBody(rsp *http.Response) {
}
}

// Probably take in config here
func setHookHeaders(req *http.Request, signatureList string) *http.Request {
req.Header.Set("Content-Type", "application/json")
msgID := uuid.Must(uuid.NewV4())
currentTime := time.Now().Unix()
req.Header.Set("webhook-id", fmt.Sprintf("msg_%s", msgID))
req.Header.Set("webhook-timestamp", fmt.Sprintf("%d", currentTime))
req.Header.Set("webhook-signature", signatureList)
return req
}

func GenerateHookSignature(msgId string, payload map[string]interface{}) (string, error) {
// TODO: Find a way to narrow the types
timestamp := time.Now().UTC().Unix()
jsonData, err := json.Marshal(payload)
if err != nil {
return "", err
}
output := fmt.Sprintf("%s.%d.%s", msgId, timestamp, string(jsonData))
// if hook rotation is enabled sign two payloads
// Sign the payload using signature. Find a way to check
return output, nil
}

func fetchHookKeys() []string {
// Decode and retrive one or two hook keys, depending on whether or not secret rotation is enabled
return nil
}

func isOverSizeLimit(payload []byte) bool {
// Check the size of the payload
const maxSizeKB = 20 * 1024 // 20KB in bytes
Expand Down Expand Up @@ -218,7 +213,7 @@ func (a *API) invokeHTTPHook(ctx context.Context, input, output any, hookURI str
panic("output should be *hooks.CustomSMSProviderOutput")
}

if _, err := a.runHTTPHook(ctx, hookURI, input, output); err != nil {
if _, err := a.runHTTPHook(ctx, a.config.Hook.CustomSMSProvider, input, output); err != nil {
return internalServerError("Error invoking custom SMS provider hook.").WithInternalError(err)
}

Expand Down

0 comments on commit eca7134

Please sign in to comment.