mirror of
https://github.com/mudler/LocalAI.git
synced 2025-05-20 10:35:01 +00:00
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:
parent
8a4df3af99
commit
de148cb2ad
3 changed files with 119 additions and 96 deletions
|
@ -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))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
23
core/http/views/login.html
Normal file
23
core/http/views/login.html
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue