From de148cb2ad08adda4d62275f2e16028647bc0c1e Mon Sep 17 00:00:00 2001 From: mintyleaf Date: Tue, 19 Nov 2024 21:43:02 +0400 Subject: [PATCH] feat: add WebUI API token authorization (#4197) * return 401 instead of 403, provide www-authenticate header, redirect to the login page, add cookie token support * set cookies completely through js in auth page --- core/http/app_test.go | 2 +- core/http/middleware/auth.go | 190 +++++++++++++++++------------------ core/http/views/login.html | 23 +++++ 3 files changed, 119 insertions(+), 96 deletions(-) create mode 100644 core/http/views/login.html diff --git a/core/http/app_test.go b/core/http/app_test.go index 653a03d3..e5431c50 100644 --- a/core/http/app_test.go +++ b/core/http/app_test.go @@ -345,7 +345,7 @@ var _ = Describe("API test", func() { It("Should fail if the api key is missing", func() { err, sc := postInvalidRequest("http://127.0.0.1:9090/models/available") Expect(err).ToNot(BeNil()) - Expect(sc).To(Equal(403)) + Expect(sc).To(Equal(401)) }) }) diff --git a/core/http/middleware/auth.go b/core/http/middleware/auth.go index 8f5fe2fb..18e7bc3c 100644 --- a/core/http/middleware/auth.go +++ b/core/http/middleware/auth.go @@ -1,95 +1,95 @@ -package middleware - -import ( - "crypto/subtle" - "errors" - - "github.com/dave-gray101/v2keyauth" - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/keyauth" - "github.com/microcosm-cc/bluemonday" - "github.com/mudler/LocalAI/core/config" -) - -// This file contains the configuration generators and handler functions that are used along with the fiber/keyauth middleware -// Currently this requires an upstream patch - and feature patches are no longer accepted to v2 -// Therefore `dave-gray101/v2keyauth` contains the v2 backport of the middleware until v3 stabilizes and we migrate. - -func GetKeyAuthConfig(applicationConfig *config.ApplicationConfig) (*v2keyauth.Config, error) { - customLookup, err := v2keyauth.MultipleKeySourceLookup([]string{"header:Authorization", "header:x-api-key", "header:xi-api-key"}, keyauth.ConfigDefault.AuthScheme) - if err != nil { - return nil, err - } - - return &v2keyauth.Config{ - CustomKeyLookup: customLookup, - Next: getApiKeyRequiredFilterFunction(applicationConfig), - Validator: getApiKeyValidationFunction(applicationConfig), - ErrorHandler: getApiKeyErrorHandler(applicationConfig), - AuthScheme: "Bearer", - }, nil -} - -func getApiKeyErrorHandler(applicationConfig *config.ApplicationConfig) fiber.ErrorHandler { - return func(ctx *fiber.Ctx, err error) error { - if errors.Is(err, v2keyauth.ErrMissingOrMalformedAPIKey) { - if len(applicationConfig.ApiKeys) == 0 { - return ctx.Next() // if no keys are set up, any error we get here is not an error. - } - if applicationConfig.OpaqueErrors { - return ctx.SendStatus(403) - } - return ctx.Status(403).SendString(bluemonday.StrictPolicy().Sanitize(err.Error())) - } - if applicationConfig.OpaqueErrors { - return ctx.SendStatus(500) - } - return err - } -} - -func getApiKeyValidationFunction(applicationConfig *config.ApplicationConfig) func(*fiber.Ctx, string) (bool, error) { - - if applicationConfig.UseSubtleKeyComparison { - return func(ctx *fiber.Ctx, apiKey string) (bool, error) { - if len(applicationConfig.ApiKeys) == 0 { - return true, nil // If no keys are setup, accept everything - } - for _, validKey := range applicationConfig.ApiKeys { - if subtle.ConstantTimeCompare([]byte(apiKey), []byte(validKey)) == 1 { - return true, nil - } - } - return false, v2keyauth.ErrMissingOrMalformedAPIKey - } - } - - return func(ctx *fiber.Ctx, apiKey string) (bool, error) { - if len(applicationConfig.ApiKeys) == 0 { - return true, nil // If no keys are setup, accept everything - } - for _, validKey := range applicationConfig.ApiKeys { - if apiKey == validKey { - return true, nil - } - } - return false, v2keyauth.ErrMissingOrMalformedAPIKey - } -} - -func getApiKeyRequiredFilterFunction(applicationConfig *config.ApplicationConfig) func(*fiber.Ctx) bool { - if applicationConfig.DisableApiKeyRequirementForHttpGet { - return func(c *fiber.Ctx) bool { - if c.Method() != "GET" { - return false - } - for _, rx := range applicationConfig.HttpGetExemptedEndpoints { - if rx.MatchString(c.Path()) { - return true - } - } - return false - } - } - return func(c *fiber.Ctx) bool { return false } -} +package middleware + +import ( + "crypto/subtle" + "errors" + + "github.com/dave-gray101/v2keyauth" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/keyauth" + "github.com/mudler/LocalAI/core/config" +) + +// This file contains the configuration generators and handler functions that are used along with the fiber/keyauth middleware +// Currently this requires an upstream patch - and feature patches are no longer accepted to v2 +// Therefore `dave-gray101/v2keyauth` contains the v2 backport of the middleware until v3 stabilizes and we migrate. + +func GetKeyAuthConfig(applicationConfig *config.ApplicationConfig) (*v2keyauth.Config, error) { + customLookup, err := v2keyauth.MultipleKeySourceLookup([]string{"header:Authorization", "header:x-api-key", "header:xi-api-key", "cookie:token"}, keyauth.ConfigDefault.AuthScheme) + if err != nil { + return nil, err + } + + return &v2keyauth.Config{ + CustomKeyLookup: customLookup, + Next: getApiKeyRequiredFilterFunction(applicationConfig), + Validator: getApiKeyValidationFunction(applicationConfig), + ErrorHandler: getApiKeyErrorHandler(applicationConfig), + AuthScheme: "Bearer", + }, nil +} + +func getApiKeyErrorHandler(applicationConfig *config.ApplicationConfig) fiber.ErrorHandler { + return func(ctx *fiber.Ctx, err error) error { + if errors.Is(err, v2keyauth.ErrMissingOrMalformedAPIKey) { + if len(applicationConfig.ApiKeys) == 0 { + return ctx.Next() // if no keys are set up, any error we get here is not an error. + } + ctx.Set("WWW-Authenticate", "Bearer") + if applicationConfig.OpaqueErrors { + return ctx.SendStatus(401) + } + return ctx.Status(401).Render("views/login", nil) + } + if applicationConfig.OpaqueErrors { + return ctx.SendStatus(500) + } + return err + } +} + +func getApiKeyValidationFunction(applicationConfig *config.ApplicationConfig) func(*fiber.Ctx, string) (bool, error) { + + if applicationConfig.UseSubtleKeyComparison { + return func(ctx *fiber.Ctx, apiKey string) (bool, error) { + if len(applicationConfig.ApiKeys) == 0 { + return true, nil // If no keys are setup, accept everything + } + for _, validKey := range applicationConfig.ApiKeys { + if subtle.ConstantTimeCompare([]byte(apiKey), []byte(validKey)) == 1 { + return true, nil + } + } + return false, v2keyauth.ErrMissingOrMalformedAPIKey + } + } + + return func(ctx *fiber.Ctx, apiKey string) (bool, error) { + if len(applicationConfig.ApiKeys) == 0 { + return true, nil // If no keys are setup, accept everything + } + for _, validKey := range applicationConfig.ApiKeys { + if apiKey == validKey { + return true, nil + } + } + return false, v2keyauth.ErrMissingOrMalformedAPIKey + } +} + +func getApiKeyRequiredFilterFunction(applicationConfig *config.ApplicationConfig) func(*fiber.Ctx) bool { + if applicationConfig.DisableApiKeyRequirementForHttpGet { + return func(c *fiber.Ctx) bool { + if c.Method() != "GET" { + return false + } + for _, rx := range applicationConfig.HttpGetExemptedEndpoints { + if rx.MatchString(c.Path()) { + return true + } + } + return false + } + } + return func(c *fiber.Ctx) bool { return false } +} diff --git a/core/http/views/login.html b/core/http/views/login.html new file mode 100644 index 00000000..b55434bd --- /dev/null +++ b/core/http/views/login.html @@ -0,0 +1,23 @@ + + + + + + Open Authenticated Website + + +

Authorization is required

+ + + + +