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
This commit is contained in:
mintyleaf 2024-11-19 21:43:02 +04:00 committed by GitHub
parent 8a4df3af99
commit de148cb2ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 119 additions and 96 deletions

View file

@ -345,7 +345,7 @@ var _ = Describe("API test", func() {
It("Should fail if the api key is missing", func() { It("Should fail if the api key is missing", func() {
err, sc := postInvalidRequest("http://127.0.0.1:9090/models/available") err, sc := postInvalidRequest("http://127.0.0.1:9090/models/available")
Expect(err).ToNot(BeNil()) Expect(err).ToNot(BeNil())
Expect(sc).To(Equal(403)) Expect(sc).To(Equal(401))
}) })
}) })

View file

@ -1,95 +1,95 @@
package middleware package middleware
import ( import (
"crypto/subtle" "crypto/subtle"
"errors" "errors"
"github.com/dave-gray101/v2keyauth" "github.com/dave-gray101/v2keyauth"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/keyauth" "github.com/gofiber/fiber/v2/middleware/keyauth"
"github.com/microcosm-cc/bluemonday" "github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/config" )
)
// This file contains the configuration generators and handler functions that are used along with the fiber/keyauth middleware
// 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
// 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.
// 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) {
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)
customLookup, err := v2keyauth.MultipleKeySourceLookup([]string{"header:Authorization", "header:x-api-key", "header:xi-api-key"}, keyauth.ConfigDefault.AuthScheme) if err != nil {
if err != nil { return nil, err
return nil, err }
}
return &v2keyauth.Config{
return &v2keyauth.Config{ CustomKeyLookup: customLookup,
CustomKeyLookup: customLookup, Next: getApiKeyRequiredFilterFunction(applicationConfig),
Next: getApiKeyRequiredFilterFunction(applicationConfig), Validator: getApiKeyValidationFunction(applicationConfig),
Validator: getApiKeyValidationFunction(applicationConfig), ErrorHandler: getApiKeyErrorHandler(applicationConfig),
ErrorHandler: getApiKeyErrorHandler(applicationConfig), AuthScheme: "Bearer",
AuthScheme: "Bearer", }, nil
}, nil }
}
func getApiKeyErrorHandler(applicationConfig *config.ApplicationConfig) fiber.ErrorHandler {
func getApiKeyErrorHandler(applicationConfig *config.ApplicationConfig) fiber.ErrorHandler { return func(ctx *fiber.Ctx, err error) error {
return func(ctx *fiber.Ctx, err error) error { if errors.Is(err, v2keyauth.ErrMissingOrMalformedAPIKey) {
if errors.Is(err, v2keyauth.ErrMissingOrMalformedAPIKey) { if len(applicationConfig.ApiKeys) == 0 {
if len(applicationConfig.ApiKeys) == 0 { return ctx.Next() // if no keys are set up, any error we get here is not an error.
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 { if applicationConfig.OpaqueErrors {
return ctx.SendStatus(403) return ctx.SendStatus(401)
} }
return ctx.Status(403).SendString(bluemonday.StrictPolicy().Sanitize(err.Error())) return ctx.Status(401).Render("views/login", nil)
} }
if applicationConfig.OpaqueErrors { if applicationConfig.OpaqueErrors {
return ctx.SendStatus(500) return ctx.SendStatus(500)
} }
return err return err
} }
} }
func getApiKeyValidationFunction(applicationConfig *config.ApplicationConfig) func(*fiber.Ctx, string) (bool, error) { func getApiKeyValidationFunction(applicationConfig *config.ApplicationConfig) func(*fiber.Ctx, string) (bool, error) {
if applicationConfig.UseSubtleKeyComparison { if applicationConfig.UseSubtleKeyComparison {
return func(ctx *fiber.Ctx, apiKey string) (bool, error) { return func(ctx *fiber.Ctx, apiKey string) (bool, error) {
if len(applicationConfig.ApiKeys) == 0 { if len(applicationConfig.ApiKeys) == 0 {
return true, nil // If no keys are setup, accept everything return true, nil // If no keys are setup, accept everything
} }
for _, validKey := range applicationConfig.ApiKeys { for _, validKey := range applicationConfig.ApiKeys {
if subtle.ConstantTimeCompare([]byte(apiKey), []byte(validKey)) == 1 { if subtle.ConstantTimeCompare([]byte(apiKey), []byte(validKey)) == 1 {
return true, nil return true, nil
} }
} }
return false, v2keyauth.ErrMissingOrMalformedAPIKey return false, v2keyauth.ErrMissingOrMalformedAPIKey
} }
} }
return func(ctx *fiber.Ctx, apiKey string) (bool, error) { return func(ctx *fiber.Ctx, apiKey string) (bool, error) {
if len(applicationConfig.ApiKeys) == 0 { if len(applicationConfig.ApiKeys) == 0 {
return true, nil // If no keys are setup, accept everything return true, nil // If no keys are setup, accept everything
} }
for _, validKey := range applicationConfig.ApiKeys { for _, validKey := range applicationConfig.ApiKeys {
if apiKey == validKey { if apiKey == validKey {
return true, nil return true, nil
} }
} }
return false, v2keyauth.ErrMissingOrMalformedAPIKey return false, v2keyauth.ErrMissingOrMalformedAPIKey
} }
} }
func getApiKeyRequiredFilterFunction(applicationConfig *config.ApplicationConfig) func(*fiber.Ctx) bool { func getApiKeyRequiredFilterFunction(applicationConfig *config.ApplicationConfig) func(*fiber.Ctx) bool {
if applicationConfig.DisableApiKeyRequirementForHttpGet { if applicationConfig.DisableApiKeyRequirementForHttpGet {
return func(c *fiber.Ctx) bool { return func(c *fiber.Ctx) bool {
if c.Method() != "GET" { if c.Method() != "GET" {
return false return false
} }
for _, rx := range applicationConfig.HttpGetExemptedEndpoints { for _, rx := range applicationConfig.HttpGetExemptedEndpoints {
if rx.MatchString(c.Path()) { if rx.MatchString(c.Path()) {
return true return true
} }
} }
return false return false
} }
} }
return func(c *fiber.Ctx) bool { return false } return func(c *fiber.Ctx) bool { return false }
} }

View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Open Authenticated Website</title>
</head>
<body>
<h1>Authorization is required</h1>
<input type="text" id="token" placeholder="Token" />
<button onclick="login()">Login</button>
<script>
function login() {
const token = document.getElementById('token').value;
var date = new Date();
date.setTime(date.getTime() + (24*60*60*1000));
document.cookie = `token=${token}; expires=${date.toGMTString()}`;
window.location.reload();
}
</script>
</body>
</html>