fix twilio authentication
This commit is contained in:
parent
5d6c8b8aa6
commit
808d25e901
11 changed files with 85 additions and 50 deletions
config
internal
server/webroot
webroot/twilio
|
@ -36,6 +36,7 @@ type C struct {
|
||||||
AccountSid string
|
AccountSid string
|
||||||
AuthToken string
|
AuthToken string
|
||||||
SkipVerify bool
|
SkipVerify bool
|
||||||
|
BaseURL string
|
||||||
}
|
}
|
||||||
Auth struct {
|
Auth struct {
|
||||||
ValidMinutes int
|
ValidMinutes int
|
||||||
|
|
|
@ -13,9 +13,22 @@ import (
|
||||||
const AuthSessionName = "auth_session"
|
const AuthSessionName = "auth_session"
|
||||||
|
|
||||||
type RequestAuth struct {
|
type RequestAuth struct {
|
||||||
Valid bool
|
Present bool
|
||||||
Roles []string
|
Valid bool
|
||||||
Expire time.Time
|
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 {
|
func Middleware(store sessions.Store) echo.MiddlewareFunc {
|
||||||
|
@ -31,6 +44,7 @@ func Middleware(store sessions.Store) echo.MiddlewareFunc {
|
||||||
|
|
||||||
var auth RequestAuth
|
var auth RequestAuth
|
||||||
if expireTs, ok := sess.Values["expire"].(string); ok {
|
if expireTs, ok := sess.Values["expire"].(string); ok {
|
||||||
|
auth.Present = true
|
||||||
if expireTime, err := time.Parse(time.RFC3339, expireTs); err != nil {
|
if expireTime, err := time.Parse(time.RFC3339, expireTs); err != nil {
|
||||||
log.Printf("invalid expireTime: %v", expireTs)
|
log.Printf("invalid expireTime: %v", expireTs)
|
||||||
} else {
|
} else {
|
||||||
|
@ -116,5 +130,9 @@ func Login(c echo.Context) (err error) {
|
||||||
|
|
||||||
}
|
}
|
||||||
func GetRequestAuth(c echo.Context) RequestAuth {
|
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}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,9 @@ func Invoke(name string, target reflect.Value, args ...reflect.Value) (any, erro
|
||||||
}
|
}
|
||||||
target = reflect.ValueOf(t)
|
target = reflect.ValueOf(t)
|
||||||
}
|
}
|
||||||
|
for i, arg := range args {
|
||||||
|
log.Printf("invoke %s arg[%d]=%v", name, i, arg)
|
||||||
|
}
|
||||||
ret := target.Call(args)
|
ret := target.Call(args)
|
||||||
if len(ret) == 0 {
|
if len(ret) == 0 {
|
||||||
return nil, nil
|
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 {
|
if err, ok := ret[len(ret)-1].Interface().(error); ok && err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
for i, r := range ret {
|
||||||
|
log.Printf("invoke %s ret[%d]=%v", name, i, r)
|
||||||
|
}
|
||||||
|
|
||||||
switch len(ret) {
|
switch len(ret) {
|
||||||
case 0:
|
case 0:
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package session
|
package session
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
@ -28,8 +30,10 @@ func ManagedSession(c echo.Context) (p Provider, close func()) {
|
||||||
checkedOutSessions[name] = s
|
checkedOutSessions[name] = s
|
||||||
return s.Values
|
return s.Values
|
||||||
}, func() {
|
}, func() {
|
||||||
for _, s := range checkedOutSessions {
|
for name, s := range checkedOutSessions {
|
||||||
s.Save(c.Request(), c.Response())
|
if err := s.Save(c.Request(), c.Response()); err != nil {
|
||||||
|
log.Printf("error saving session %s: %v", name, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
|
||||||
}
|
|
|
@ -1,6 +1,7 @@
|
||||||
package twilio
|
package twilio
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -9,8 +10,10 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/labstack/echo/v4/middleware"
|
||||||
|
|
||||||
"github.com/eternal-flame-AD/yoake/config"
|
"github.com/eternal-flame-AD/yoake/config"
|
||||||
|
"github.com/eternal-flame-AD/yoake/internal/auth"
|
||||||
"github.com/twilio/twilio-go/client"
|
"github.com/twilio/twilio-go/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,14 +25,33 @@ func firstUrlValues(val url.Values) map[string]string {
|
||||||
return res
|
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 {
|
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)
|
cleanPath := path.Clean(c.Request().URL.Path)
|
||||||
//log.Printf("cleanPath: %s", cleanPath)
|
//log.Printf("cleanPath: %s", cleanPath)
|
||||||
if cleanPath == prefix || strings.HasPrefix(cleanPath, prefix+"/") {
|
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.")
|
c.String(http.StatusOK, "We are sorry. Request Validation Failed. This is not your fault.")
|
||||||
log.Printf("twilio verify failed: %v", err)
|
log.Printf("twilio verify failed: %v", err)
|
||||||
return err
|
return err
|
||||||
|
@ -37,10 +59,14 @@ func VerifyMiddleware(prefix string) echo.MiddlewareFunc {
|
||||||
}
|
}
|
||||||
return next(c)
|
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
|
conf := config.Config().Twilio
|
||||||
signature := req.Header.Get("X-Twilio-Signature")
|
signature := req.Header.Get("X-Twilio-Signature")
|
||||||
if signature == "" {
|
if signature == "" {
|
||||||
|
@ -51,7 +77,11 @@ func TwilioValidate(req *http.Request) error {
|
||||||
}
|
}
|
||||||
requestValidator := client.NewRequestValidator(conf.AuthToken)
|
requestValidator := client.NewRequestValidator(conf.AuthToken)
|
||||||
if req.Method == "POST" {
|
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")
|
return fmt.Errorf("twilio signature verification failed")
|
||||||
}
|
}
|
||||||
} else if req.Method == "GET" {
|
} else if req.Method == "GET" {
|
||||||
|
|
|
@ -127,8 +127,8 @@ var (
|
||||||
LogError: true,
|
LogError: true,
|
||||||
LogContentLength: true,
|
LogContentLength: true,
|
||||||
LogResponseSize: true,
|
LogResponseSize: true,
|
||||||
LogHeaders: []string{},
|
LogHeaders: []string{"Content-Type"},
|
||||||
LogQueryParams: []string{},
|
LogQueryParams: []string{},
|
||||||
LogFormValues: []string{},
|
LogFormValues: []string{"From", "To"},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -66,8 +66,8 @@ func Init(hostname string) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
middleware.Gzip(),
|
middleware.Gzip(),
|
||||||
logMiddleware("twilio", twilio.VerifyMiddleware("/twilio")),
|
|
||||||
auth.Middleware(sessionCookie),
|
auth.Middleware(sessionCookie),
|
||||||
|
logMiddleware("twilio", twilio.VerifyMiddleware("/twilio", config.Config().Twilio.BaseURL)),
|
||||||
middleware.Rewrite(map[string]string{"*/": "$1/index.html"}),
|
middleware.Rewrite(map[string]string{"*/": "$1/index.html"}),
|
||||||
logMiddleware("template", servetpl.ServeTemplateDir(webroot.Root)),
|
logMiddleware("template", servetpl.ServeTemplateDir(webroot.Root)),
|
||||||
logMiddleware("static", middleware.Static(webroot.Root)))
|
logMiddleware("static", middleware.Static(webroot.Root)))
|
||||||
|
|
5
webroot/twilio/sms/entrypoint.tpl.xml
Normal file
5
webroot/twilio/sms/entrypoint.tpl.xml
Normal 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>
|
6
webroot/twilio/sms/menu.tpl.xml
Normal file
6
webroot/twilio/sms/menu.tpl.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{{ template "/twilio/head.tpl.xml" . }}
|
||||||
|
{{ $session := (get .Global "twilio_session") }}
|
||||||
|
|
||||||
|
<Response>
|
||||||
|
<Message>Hello! This is yumechi.</Message>
|
||||||
|
</Response>
|
|
@ -2,6 +2,7 @@
|
||||||
{{ $session := (get .Global "twilio_session") }}
|
{{ $session := (get .Global "twilio_session") }}
|
||||||
|
|
||||||
{{ $num_visited := (get $session "num_visited") }}
|
{{ $num_visited := (get $session "num_visited") }}
|
||||||
|
{{ if not $num_visited }}{{ $num_visited = 0 }} {{ end }}
|
||||||
{{ set $session "num_visited" (math "argv(1) + 1" $num_visited) }}
|
{{ set $session "num_visited" (math "argv(1) + 1" $num_visited) }}
|
||||||
<Response>
|
<Response>
|
||||||
<Say voice="alice" language="en-US">This is Anne!</Say>
|
<Say voice="alice" language="en-US">This is Anne!</Say>
|
||||||
|
|
Loading…
Add table
Reference in a new issue