fix twilio authentication

This commit is contained in:
ゆめ 2022-11-10 20:13:09 -05:00
parent 5d6c8b8aa6
commit 808d25e901
11 changed files with 85 additions and 50 deletions

View file

@ -36,6 +36,7 @@ type C struct {
AccountSid string
AuthToken string
SkipVerify bool
BaseURL string
}
Auth struct {
ValidMinutes int

View file

@ -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}
}
}

View file

@ -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:

View file

@ -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)
}
}
}
}

View file

@ -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))
}

View file

@ -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" {

View file

@ -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"},
}
)

View file

@ -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)))

View file

@ -0,0 +1,5 @@
{{ template "/twilio/head.tpl.xml" . }}
{{ $session := (get .Global "twilio_session") }}
<Response>
<Redirect method="POST">/twilio/sms/menu.xml</Redirect>
</Response>

View file

@ -0,0 +1,6 @@
{{ template "/twilio/head.tpl.xml" . }}
{{ $session := (get .Global "twilio_session") }}
<Response>
<Message>Hello! This is yumechi.</Message>
</Response>

View file

@ -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>