Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MAP-24] Add Golang webhook server example #63

Merged
merged 9 commits into from
Apr 14, 2022
1 change: 1 addition & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ jobs:
with:
go-version: '^1.14'
- run: cd go/examples/sign-request && go build
- run: cd go/examples/webhook-server && go build
9 changes: 9 additions & 0 deletions go/examples/webhook-server/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module webhook-server

go 1.18

require (
github.com/Truelayer/truelayer-signing/go v0.1.4 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If anyone knows about a better http cache...
This is not actively maintained but it does the job.

github.com/wk8/go-ordered-map v0.2.0 // indirect
)
13 changes: 13 additions & 0 deletions go/examples/webhook-server/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
github.com/Truelayer/truelayer-signing/go v0.1.4 h1:tdYr7J8orPEUlrVJ4g922V0OVr+Px4QDOxaHbOcbI7w=
github.com/Truelayer/truelayer-signing/go v0.1.4/go.mod h1:SWksk9wzvQRhn0rb8Q0drHt9N0w67pvTg0PWBdQ+cWk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/wk8/go-ordered-map v0.2.0 h1:KlvGyHstD1kkGZkPtHCyCfRYS0cz84uk6rrW/Dnhdtk=
github.com/wk8/go-ordered-map v0.2.0/go.mod h1:9ZIbRunKbuvfPKyBP1SIKLcXNlv74YCOZ3t3VTS6gRk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
89 changes: 89 additions & 0 deletions go/examples/webhook-server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package main

import (
"fmt"
"io"
"log"
"net/http"

tlsigning "github.com/Truelayer/truelayer-signing/go"
"github.com/gregjones/httpcache"
)

func main() {
tp := httpcache.NewMemoryCacheTransport()
client := http.Client{Transport: tp}

http.HandleFunc("/hook/d7a2c49d-110a-4ed2-a07d-8fdb3ea6424b", receiveHook(&client))

log.Println("Starting server on: 7000")

log.Fatal(http.ListenAndServe(":7000", nil))
}

func receiveHook(client *http.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method is not supported.", http.StatusNotFound)
return
}

err := verifyHook(client, r)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusUnauthorized)
}

// handle verified hook

w.WriteHeader(http.StatusAccepted)
}
}

func verifyHook(client *http.Client, r *http.Request) error {
tlSignature := r.Header.Get("Tl-Signature")
if len(tlSignature) == 0 {
return fmt.Errorf("missing Tl-Signature header")
}

jwsHeader, err := tlsigning.ExtractJwsHeader(tlSignature)
if err != nil {
return fmt.Errorf("jku missing")
tl-marco-tormento marked this conversation as resolved.
Show resolved Hide resolved
}

defer r.Body.Close()
webhookBody, err := io.ReadAll(r.Body)
if err != nil {
return fmt.Errorf("webhook body missing")
}

// ensure jku is an expected TrueLayer url
if jwsHeader.Jku != "https://webhooks.truelayer.com/.well-known/jwks" && jwsHeader.Jku != "https://webhooks.truelayer-sandbox.com/.well-known/jwks" {
tl-marco-tormento marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("unpermitted jku %s", jwsHeader.Jku)
}

// fetch jwks (cached according to cache-control headers)
resp, err := client.Get(jwsHeader.Jku)
if err != nil {
return fmt.Errorf("jku missing")
tl-marco-tormento marked this conversation as resolved.
Show resolved Hide resolved
}
defer resp.Body.Close()
jwks, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("jwks missing")
}
tl-flavio-barinas marked this conversation as resolved.
Show resolved Hide resolved

// verify signature using the jwks
return tlsigning.VerifyWithJwks(jwks).Method(http.MethodPost).Path(r.RequestURI).Headers(getHeadersMap(r.Header)).Body(webhookBody).Verify(tlSignature)
}

func getHeadersMap(requestHeaders map[string][]string) map[string][]byte {
headers := make(map[string][]byte)
for key, values := range requestHeaders {
for _, value := range values {
// keep last one in case of multiple values
tl-marco-tormento marked this conversation as resolved.
Show resolved Hide resolved
headers[key] = []byte(value)
}
}
return headers
}