From 1f22f5f34fa18446e57c8e650a477652e5150e49 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 26 May 2020 03:05:39 -0600 Subject: [PATCH] add token auth --- .gitignore | 1 + mplexer/cmd/mgmt/mgmt.go | 139 +++++++++++++++++++++------------ mplexer/cmd/signjwt/signjwt.go | 44 +++++++++++ 3 files changed, 135 insertions(+), 49 deletions(-) create mode 100644 mplexer/cmd/signjwt/signjwt.go diff --git a/.gitignore b/.gitignore index 8e27fef..69dee76 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ acme.d xversion.go /mplexer/cmd/mgmt/mgmt +/mplexer/cmd/signjwt/signjwt /mplexer/cmd/telebit/telebit /telebit /cmd/telebit/telebit diff --git a/mplexer/cmd/mgmt/mgmt.go b/mplexer/cmd/mgmt/mgmt.go index d8841d3..ab3d322 100644 --- a/mplexer/cmd/mgmt/mgmt.go +++ b/mplexer/cmd/mgmt/mgmt.go @@ -3,13 +3,16 @@ package main import ( + "context" "encoding/json" "flag" "fmt" "net/http" "os" + "strings" "time" + jwt "github.com/dgrijalva/jwt-go" "github.com/go-acme/lego/v3/challenge" "github.com/go-acme/lego/v3/providers/dns/duckdns" "github.com/go-acme/lego/v3/providers/dns/godaddy" @@ -27,6 +30,8 @@ var ( GitTimestamp = "0000-00-00T00:00:00+0000" ) +type MWKey string + func main() { var err error var provider challenge.Provider = nil // TODO is this concurrency-safe? @@ -35,12 +40,13 @@ func main() { addr := flag.String("address", "", "IPv4 or IPv6 bind address") port := flag.String("port", "3000", "port to listen to") + secret := flag.String("secret", "", "a >= 16-character random string for JWT key signing") // SECRET flag.Parse() if "" != os.Getenv("GODADDY_API_KEY") { id := os.Getenv("GODADDY_API_KEY") - secret := os.Getenv("GODADDY_API_SECRET") - if provider, err = newGoDaddyDNSProvider(id, secret); nil != err { + apiSecret := os.Getenv("GODADDY_API_SECRET") + if provider, err = newGoDaddyDNSProvider(id, apiSecret); nil != err { panic(err) } } else if "" != os.Getenv("DUCKDNS_TOKEN") { @@ -51,67 +57,102 @@ func main() { panic("Must provide either DUCKDNS or GODADDY credentials") } + if "" == *secret { + *secret = os.Getenv("SECRET") + } + if "" == *secret { + fmt.Fprintf(os.Stderr, "Usage: signjwt ") + os.Exit(1) + return + } + r := chi.NewRouter() r.Use(middleware.Logger) r.Use(middleware.Timeout(15 * time.Second)) r.Use(middleware.Recoverer) - // TODO add authorization header and validation - //r.Post("/api/dns/{domain:[a-z0-9\\.-]+}", func(w http.ResponseWriter, r *http.Request) { - //r.Post("/api/dns/*", func(w http.ResponseWriter, r *http.Request) { - r.Post("/api/dns/{domain}", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") + r.Route("/api/dns", func(r chi.Router) { + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var tokenString string + if auth := strings.Split(r.Header.Get("Authorization"), " "); len(auth) > 1 { + // TODO handle Basic auth tokens as well + tokenString = auth[1] + } + if "" == tokenString { + tokenString = r.URL.Query().Get("access_token") + } - ch := Challenge{} + // TODO check expiration and such + tok, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + return []byte(*secret), nil + }) + if nil != err { + fmt.Println("validation error:", tokenString, err) + http.Error(w, "{\"error\":\"could not verify token\"}", http.StatusBadRequest) + return + } - // TODO prevent slow loris - decoder := json.NewDecoder(r.Body) - err := decoder.Decode(&ch) - if nil != err || "" == ch.Token || "" == ch.KeyAuth { - msg := `{"error":"expected json in the format {\"token\":\"xxx\",\"key_authorization\":\"yyy\"}"}` - http.Error(w, msg, http.StatusUnprocessableEntity) - return - } + ctx := context.WithValue(r.Context(), MWKey("token"), tok) - domain := chi.URLParam(r, "domain") - //domain := chi.URLParam(r, "*") - ch.Domain = domain + next.ServeHTTP(w, r.WithContext(ctx)) + }) + }) - // TODO some additional error checking before the handoff - //ch.error = make(chan error, 1) - ch.error = make(chan error) - presenters <- &ch - err = <-ch.error - if nil != err || "" == ch.Token || "" == ch.KeyAuth { - msg := `{"error":"expected json in the format {\"token\":\"xxx\",\"key_authorization\":\"yyy\"}"}` - http.Error(w, msg, http.StatusUnprocessableEntity) - return - } + r.Post("/{domain}", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") - w.Write([]byte("{\"success\":true}\n")) - }) + ch := Challenge{} - // TODO ugly Delete, but wahtever - r.Delete("/api/dns/{domain}/{token}/{keyAuth}", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") + // TODO prevent slow loris + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&ch) + if nil != err || "" == ch.Token || "" == ch.KeyAuth { + msg := `{"error":"expected json in the format {\"token\":\"xxx\",\"key_authorization\":\"yyy\"}"}` + http.Error(w, msg, http.StatusUnprocessableEntity) + return + } - ch := Challenge{ - Domain: chi.URLParam(r, "domain"), - Token: chi.URLParam(r, "token"), - KeyAuth: chi.URLParam(r, "keyAuth"), - error: make(chan error), - //error: make(chan error, 1), - } + domain := chi.URLParam(r, "domain") + //domain := chi.URLParam(r, "*") + ch.Domain = domain - cleanups <- &ch - err = <-ch.error - if nil != err || "" == ch.Token || "" == ch.KeyAuth { - msg := `{"error":"expected json in the format {\"token\":\"xxx\",\"key_authorization\":\"yyy\"}"}` - http.Error(w, msg, http.StatusUnprocessableEntity) - return - } + // TODO some additional error checking before the handoff + //ch.error = make(chan error, 1) + ch.error = make(chan error) + presenters <- &ch + err = <-ch.error + if nil != err || "" == ch.Token || "" == ch.KeyAuth { + msg := `{"error":"expected json in the format {\"token\":\"xxx\",\"key_authorization\":\"yyy\"}"}` + http.Error(w, msg, http.StatusUnprocessableEntity) + return + } - w.Write([]byte("{\"success\":true}\n")) + w.Write([]byte("{\"success\":true}\n")) + }) + + // TODO ugly Delete, but whatever + r.Delete("/{domain}/{token}/{keyAuth}", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + ch := Challenge{ + Domain: chi.URLParam(r, "domain"), + Token: chi.URLParam(r, "token"), + KeyAuth: chi.URLParam(r, "keyAuth"), + error: make(chan error), + //error: make(chan error, 1), + } + + cleanups <- &ch + err = <-ch.error + if nil != err || "" == ch.Token || "" == ch.KeyAuth { + msg := `{"error":"expected json in the format {\"token\":\"xxx\",\"key_authorization\":\"yyy\"}"}` + http.Error(w, msg, http.StatusUnprocessableEntity) + return + } + + w.Write([]byte("{\"success\":true}\n")) + }) }) r.Get("/", func(w http.ResponseWriter, r *http.Request) { diff --git a/mplexer/cmd/signjwt/signjwt.go b/mplexer/cmd/signjwt/signjwt.go new file mode 100644 index 0000000..b3e40c6 --- /dev/null +++ b/mplexer/cmd/signjwt/signjwt.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "os" + + jwt "github.com/dgrijalva/jwt-go" + _ "github.com/joho/godotenv/autoload" +) + +func main() { + var secret string + + if len(os.Args) == 2 { + secret = os.Args[1] + } + if "" == secret { + secret = os.Getenv("SECRET") + } + if "" == secret { + fmt.Fprintf(os.Stderr, "Usage: signjwt ") + os.Exit(1) + return + } + + tok, err := getToken(secret, []string{}) + if nil != err { + fmt.Fprintf(os.Stderr, "signing error: %s", err) + os.Exit(1) + return + } + + fmt.Println(tok) +} + +func getToken(secret string, domains []string) (token string, err error) { + tokenData := jwt.MapClaims{"domains": domains} + + jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, tokenData) + if token, err = jwtToken.SignedString([]byte(secret)); err != nil { + return "", err + } + return token, nil +}