From b610f9dc8bae1904bab947130d46bc2c0a2ce8d1 Mon Sep 17 00:00:00 2001 From: ppenguin Date: Sat, 24 Oct 2026 16:34:44 +0200 Subject: [PATCH] fix(auth): accept `Bearer ` as well as the bare token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - v1, v2 extractors: `jwt.TokenFromAuthorization` (CasaOS-Common v0.4.31) on the header value - v1 header-or-`?token=`: still decided on the raw header (present → header) - `Internal `: still matched on the raw header (skipper) - tests: bare, `Bearer`, `bearer` → accepted; `Basic`, the scheme alone → 401; `?token=` without a header → accepted; scheme alone + `?token=` → 401 Part of ReCasaOS/CasaOS#46. Co-Authored-By: Claude Opus 5.5 --- go.mod | 2 +- go.sum | 4 +-- route/bearer_test.go | 80 ++++++++++++++++++++++++++++++++++++++++++++ route/v1.go | 3 +- route/v2.go | 2 +- 5 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 route/bearer_test.go diff --git a/go.mod b/go.mod index 47eb283..e8c571f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/ReCasaOS/CasaOS-LocalStorage go 1.26.8 require ( - github.com/ReCasaOS/CasaOS-Common v0.4.26 + github.com/ReCasaOS/CasaOS-Common v0.4.31 github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/deepmap/oapi-codegen v1.12.4 github.com/getkin/kin-openapi v0.149.0 diff --git a/go.sum b/go.sum index b7df89e..7bd137e 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/ReCasaOS/CasaOS-Common v0.4.26 h1:aefnl58xQ4z0ga0+vBnQHmkG1/3fnd1C1W/EBFeAgvc= -github.com/ReCasaOS/CasaOS-Common v0.4.26/go.mod h1:HjYCcRAVDa0c/IncJDof1sddCpze2esCTZt941Sk7LI= +github.com/ReCasaOS/CasaOS-Common v0.4.31 h1:j0+PVY2suHW/StJ69MnDwBk+1rdS19ysxBuOsedK8Tc= +github.com/ReCasaOS/CasaOS-Common v0.4.31/go.mod h1:HjYCcRAVDa0c/IncJDof1sddCpze2esCTZt941Sk7LI= github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4= github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk= github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM= diff --git a/route/bearer_test.go b/route/bearer_test.go new file mode 100644 index 0000000..83402b9 --- /dev/null +++ b/route/bearer_test.go @@ -0,0 +1,80 @@ +package route + +import ( + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/ReCasaOS/CasaOS-Common/external" + "github.com/ReCasaOS/CasaOS-Common/utils/jwt" + "github.com/ReCasaOS/CasaOS-LocalStorage/pkg/config" +) + +// The access token bare (as the dashboard sends it) or as `Bearer `; another scheme, or +// the scheme alone, is no token. v1: header or ?token=, the raw header decides. Past the token, a +// path that is no route answers 404: what is checked here is the token alone. +func TestTheAccessTokenIsTakenBareOrAsBearer(t *testing.T) { + runtime := t.TempDir() + previous := config.CommonInfo.RuntimePath + config.CommonInfo.RuntimePath = runtime + t.Cleanup(func() { config.CommonInfo.RuntimePath = previous }) + if err := os.WriteFile(filepath.Join(runtime, external.InternalSecretFilename), []byte("s3cret"), 0o600); err != nil { + t.Fatal(err) + } + privateKey, publicKey, err := jwt.GenerateKeyPair() + if err != nil { + t.Fatal(err) + } + jwks, err := jwt.GenerateJwksJSON(publicKey) + if err != nil { + t.Fatal(err) + } + userService := httptest.NewServer(jwt.JWKSHandler(jwks)) + defer userService.Close() + if err := os.WriteFile(filepath.Join(runtime, external.UserServiceAddressFilename), []byte(userService.URL), 0o600); err != nil { + t.Fatal(err) + } + token, err := jwt.GetAccessToken("admin", privateKey, 1) + if err != nil { + t.Fatal(err) + } + + v1, v2 := InitV1Router(), InitV2Router() + for _, c := range []struct { + name, authorization, query string + v2 bool + passes bool + }{ + {"v1 bare", token, "", false, true}, + {"v1 Bearer", "Bearer " + token, "", false, true}, + {"v1 bearer", "bearer " + token, "", false, true}, + {"v1 Basic", "Basic " + token, "", false, false}, + {"v1 the scheme alone", "Bearer", "", false, false}, + {"v1 ?token= without a header", "", token, false, true}, + {"v1 the scheme alone and ?token=: the header decides", "Bearer", token, false, false}, + {"v2 bare", token, "", true, true}, + {"v2 Bearer", "Bearer " + token, "", true, true}, + {"v2 Basic", "Basic " + token, "", true, false}, + {"v2 the scheme alone", "Bearer", "", true, false}, + } { + target, router := "/v1/disks/nothing-here", v1 + if c.v2 { + target, router = "/v2/local_storage/nothing-here", v2 + } + if c.query != "" { + target += "?token=" + c.query + } + request := httptest.NewRequest(http.MethodGet, target, nil) + request.RemoteAddr = "203.0.113.5:40000" + if c.authorization != "" { + request.Header.Set("Authorization", c.authorization) + } + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, request) + if passed := recorder.Code != http.StatusUnauthorized; passed != c.passes { + t.Errorf("%s: %d, passes %v, want %v", c.name, recorder.Code, passed, c.passes) + } + } +} diff --git a/route/v1.go b/route/v1.go index a7f564a..2de9d26 100644 --- a/route/v1.go +++ b/route/v1.go @@ -48,8 +48,9 @@ func InitV1Router() http.Handler { }, TokenLookupFuncs: []echo_middleware.ValuesExtractor{ func(c echo.Context) ([]string, error) { + // header or query: the raw header decides; the token in it bare or `Bearer ` if len(c.Request().Header.Get(echo.HeaderAuthorization)) > 0 { - return []string{c.Request().Header.Get(echo.HeaderAuthorization)}, nil + return []string{jwt.TokenFromAuthorization(c.Request().Header.Get(echo.HeaderAuthorization))}, nil } return []string{c.QueryParam("token")}, nil }, diff --git a/route/v2.go b/route/v2.go index 7632da3..9923928 100644 --- a/route/v2.go +++ b/route/v2.go @@ -82,7 +82,7 @@ func InitV2Router() http.Handler { }, TokenLookupFuncs: []echo_middleware.ValuesExtractor{ func(c echo.Context) ([]string, error) { - return []string{c.Request().Header.Get(echo.HeaderAuthorization)}, nil + return []string{jwt.TokenFromAuthorization(c.Request().Header.Get(echo.HeaderAuthorization))}, nil }, }, }))