From 808d25e901b6a746aa7dcc37c0adc7eacfc05145 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD <yume@yumechi.jp> Date: Thu, 10 Nov 2022 20:13:09 -0500 Subject: [PATCH] fix twilio authentication --- config/config.go | 1 + internal/auth/auth.go | 26 +++++++++++++--- internal/servetpl/funcmap/funcmap.go | 6 ++++ internal/session/manager.go | 8 +++-- internal/twilio/twilio_test.go | 36 ---------------------- internal/twilio/verify.go | 40 +++++++++++++++++++++---- server/webroot/log.go | 4 +-- server/webroot/server.go | 2 +- webroot/twilio/sms/entrypoint.tpl.xml | 5 ++++ webroot/twilio/sms/menu.tpl.xml | 6 ++++ webroot/twilio/voice/entrypoint.tpl.xml | 1 + 11 files changed, 85 insertions(+), 50 deletions(-) delete mode 100644 internal/twilio/twilio_test.go create mode 100644 webroot/twilio/sms/entrypoint.tpl.xml create mode 100644 webroot/twilio/sms/menu.tpl.xml diff --git a/config/config.go b/config/config.go index 98bafa7..940e63a 100644 --- a/config/config.go +++ b/config/config.go @@ -36,6 +36,7 @@ type C struct { AccountSid string AuthToken string SkipVerify bool + BaseURL string } Auth struct { ValidMinutes int diff --git a/internal/auth/auth.go b/internal/auth/auth.go index b270489..6c938c3 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -13,9 +13,22 @@ import ( const AuthSessionName = "auth_session" type RequestAuth struct { - Valid bool - Roles []string - Expire time.Time + Present bool + Valid bool + Roles []string + Expire time.Time +} + +func (a RequestAuth) HasRole(role Role) bool { + if !a.Valid { + return false + } + for _, r := range a.Roles { + if r == string(role) { + return true + } + } + return false } func Middleware(store sessions.Store) echo.MiddlewareFunc { @@ -31,6 +44,7 @@ func Middleware(store sessions.Store) echo.MiddlewareFunc { var auth RequestAuth if expireTs, ok := sess.Values["expire"].(string); ok { + auth.Present = true if expireTime, err := time.Parse(time.RFC3339, expireTs); err != nil { log.Printf("invalid expireTime: %v", expireTs) } else { @@ -116,5 +130,9 @@ func Login(c echo.Context) (err error) { } func GetRequestAuth(c echo.Context) RequestAuth { - return c.Get("auth_" + AuthSessionName).(RequestAuth) + if a, ok := c.Get("auth_" + AuthSessionName).(RequestAuth); ok { + return a + } else { + return RequestAuth{Present: false, Valid: false} + } } diff --git a/internal/servetpl/funcmap/funcmap.go b/internal/servetpl/funcmap/funcmap.go index 9a0c689..2a6be46 100644 --- a/internal/servetpl/funcmap/funcmap.go +++ b/internal/servetpl/funcmap/funcmap.go @@ -60,6 +60,9 @@ func Invoke(name string, target reflect.Value, args ...reflect.Value) (any, erro } target = reflect.ValueOf(t) } + for i, arg := range args { + log.Printf("invoke %s arg[%d]=%v", name, i, arg) + } ret := target.Call(args) if len(ret) == 0 { return nil, nil @@ -68,6 +71,9 @@ func Invoke(name string, target reflect.Value, args ...reflect.Value) (any, erro if err, ok := ret[len(ret)-1].Interface().(error); ok && err != nil { return nil, err } + for i, r := range ret { + log.Printf("invoke %s ret[%d]=%v", name, i, r) + } switch len(ret) { case 0: diff --git a/internal/session/manager.go b/internal/session/manager.go index dca84ce..d042d11 100644 --- a/internal/session/manager.go +++ b/internal/session/manager.go @@ -1,6 +1,8 @@ package session import ( + "log" + "github.com/gorilla/sessions" "github.com/labstack/echo/v4" ) @@ -28,8 +30,10 @@ func ManagedSession(c echo.Context) (p Provider, close func()) { checkedOutSessions[name] = s return s.Values }, func() { - for _, s := range checkedOutSessions { - s.Save(c.Request(), c.Response()) + for name, s := range checkedOutSessions { + if err := s.Save(c.Request(), c.Response()); err != nil { + log.Printf("error saving session %s: %v", name, err) + } } } } diff --git a/internal/twilio/twilio_test.go b/internal/twilio/twilio_test.go deleted file mode 100644 index 73f9cc6..0000000 --- a/internal/twilio/twilio_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package twilio - -import ( - "bytes" - "net/http/httptest" - "net/url" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestTwilioValidate(t *testing.T) { - params := map[string]string{ - "CallSid": "CA1234567890ABCDE", - "Caller": "+12349013030", - "Digits": "1234", - "From": "+12349013030", - "To": "+18005551212", - } - bodyForm := make(url.Values) - for k, v := range params { - bodyForm.Set(k, v) - } - - req := httptest.NewRequest("POST", "https://mycompany.com/myapp.php?foo=1&bar=2", - bytes.NewBufferString(bodyForm.Encode())) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - require.NoError(t, req.ParseForm()) - - assert.ErrorContains(t, TwilioValidate(req), "no twilio signature") - req.Header.Set("X-Twilio-Signature", "garbage") - assert.ErrorContains(t, TwilioValidate(req), "twilio signature verification failed") - req.Header.Set("X-Twilio-Signature", "0/KCTR6DLpKmkAf8muzZqo1nDgQ=") - assert.NoError(t, TwilioValidate(req)) -} diff --git a/internal/twilio/verify.go b/internal/twilio/verify.go index 674af13..aeb22d2 100644 --- a/internal/twilio/verify.go +++ b/internal/twilio/verify.go @@ -1,6 +1,7 @@ package twilio import ( + "crypto/subtle" "fmt" "log" "net/http" @@ -9,8 +10,10 @@ import ( "strings" "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" "github.com/eternal-flame-AD/yoake/config" + "github.com/eternal-flame-AD/yoake/internal/auth" "github.com/twilio/twilio-go/client" ) @@ -22,14 +25,33 @@ func firstUrlValues(val url.Values) map[string]string { return res } -func VerifyMiddleware(prefix string) echo.MiddlewareFunc { +func VerifyMiddleware(prefix string, baseurlS string) echo.MiddlewareFunc { + baseURL, err := url.Parse(baseurlS) + if err != nil { + log.Fatalf("invalid twilio baseurl: %v", baseurlS) + } + log.Printf("twilio baseurl is %v", baseURL) + var basicAuth echo.MiddlewareFunc + if userpass := baseURL.User.String(); userpass != "" { + basicAuth = middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) { + ui := url.UserPassword(username, password) + return subtle.ConstantTimeCompare([]byte(ui.String()), []byte(userpass)) == 1, nil + }) + } return func(next echo.HandlerFunc) echo.HandlerFunc { + verifySignature := func(c echo.Context) error { + if reqAuth := auth.GetRequestAuth(c); reqAuth.Valid && reqAuth.HasRole(auth.RoleAdmin) { + return next(c) + } - return func(c echo.Context) error { cleanPath := path.Clean(c.Request().URL.Path) //log.Printf("cleanPath: %s", cleanPath) if cleanPath == prefix || strings.HasPrefix(cleanPath, prefix+"/") { - if err := TwilioValidate(c.Request()); err != nil { + fullReq := c.Request().Clone(c.Request().Context()) + log.Printf("original request URL: %v, scheme=%s, host=%s, user=%s", c.Request().URL, c.Request().URL.Scheme, c.Request().URL.Host, c.Request().URL.User) + fullReq.URL = baseURL.ResolveReference(c.Request().URL) + fullReq.URL.User = nil + if err := TwilioValidate(c, fullReq); err != nil { c.String(http.StatusOK, "We are sorry. Request Validation Failed. This is not your fault.") log.Printf("twilio verify failed: %v", err) return err @@ -37,10 +59,14 @@ func VerifyMiddleware(prefix string) echo.MiddlewareFunc { } return next(c) } + if basicAuth != nil { + return basicAuth(verifySignature) + } + return verifySignature } } -func TwilioValidate(req *http.Request) error { +func TwilioValidate(c echo.Context, req *http.Request) error { conf := config.Config().Twilio signature := req.Header.Get("X-Twilio-Signature") if signature == "" { @@ -51,7 +77,11 @@ func TwilioValidate(req *http.Request) error { } requestValidator := client.NewRequestValidator(conf.AuthToken) if req.Method == "POST" { - if !requestValidator.Validate(req.URL.String(), firstUrlValues(req.PostForm), signature) { + form, err := c.FormParams() + if err != nil { + return err + } + if !requestValidator.Validate(req.URL.String(), firstUrlValues(form), signature) { return fmt.Errorf("twilio signature verification failed") } } else if req.Method == "GET" { diff --git a/server/webroot/log.go b/server/webroot/log.go index 801b2bf..c5bbd14 100644 --- a/server/webroot/log.go +++ b/server/webroot/log.go @@ -127,8 +127,8 @@ var ( LogError: true, LogContentLength: true, LogResponseSize: true, - LogHeaders: []string{}, + LogHeaders: []string{"Content-Type"}, LogQueryParams: []string{}, - LogFormValues: []string{}, + LogFormValues: []string{"From", "To"}, } ) diff --git a/server/webroot/server.go b/server/webroot/server.go index cd0e56c..b3d74fb 100644 --- a/server/webroot/server.go +++ b/server/webroot/server.go @@ -66,8 +66,8 @@ func Init(hostname string) { } }, middleware.Gzip(), - logMiddleware("twilio", twilio.VerifyMiddleware("/twilio")), auth.Middleware(sessionCookie), + logMiddleware("twilio", twilio.VerifyMiddleware("/twilio", config.Config().Twilio.BaseURL)), middleware.Rewrite(map[string]string{"*/": "$1/index.html"}), logMiddleware("template", servetpl.ServeTemplateDir(webroot.Root)), logMiddleware("static", middleware.Static(webroot.Root))) diff --git a/webroot/twilio/sms/entrypoint.tpl.xml b/webroot/twilio/sms/entrypoint.tpl.xml new file mode 100644 index 0000000..72cca33 --- /dev/null +++ b/webroot/twilio/sms/entrypoint.tpl.xml @@ -0,0 +1,5 @@ +{{ template "/twilio/head.tpl.xml" . }} +{{ $session := (get .Global "twilio_session") }} +<Response> + <Redirect method="POST">/twilio/sms/menu.xml</Redirect> +</Response> \ No newline at end of file diff --git a/webroot/twilio/sms/menu.tpl.xml b/webroot/twilio/sms/menu.tpl.xml new file mode 100644 index 0000000..29f6fba --- /dev/null +++ b/webroot/twilio/sms/menu.tpl.xml @@ -0,0 +1,6 @@ +{{ template "/twilio/head.tpl.xml" . }} +{{ $session := (get .Global "twilio_session") }} + +<Response> +<Message>Hello! This is yumechi.</Message> +</Response> \ No newline at end of file diff --git a/webroot/twilio/voice/entrypoint.tpl.xml b/webroot/twilio/voice/entrypoint.tpl.xml index 1705c15..4ca4197 100644 --- a/webroot/twilio/voice/entrypoint.tpl.xml +++ b/webroot/twilio/voice/entrypoint.tpl.xml @@ -2,6 +2,7 @@ {{ $session := (get .Global "twilio_session") }} {{ $num_visited := (get $session "num_visited") }} +{{ if not $num_visited }}{{ $num_visited = 0 }} {{ end }} {{ set $session "num_visited" (math "argv(1) + 1" $num_visited) }} <Response> <Say voice="alice" language="en-US">This is Anne!</Say>