fix(auth): reject invalid bearer tokens instead of falling back
Modify the authentication handler to return an unauthorized error when an invalid or disabled bearer token is provided, rather than silently falling back to an anonymous request. This ensures that clients attempting to authenticate but failing (due to expired, malformed, or disabled tokens) are explicitly notified of the auth failure instead of proceeding anonymously. True anonymous requests without any Authorization header remain supported.
This commit is contained in:
@@ -100,25 +100,51 @@ func TestBearerTokenUploadActsAsUser(t *testing.T) {
|
||||
t.Fatalf("OwnerID = %q, want %q", box.OwnerID, user.ID)
|
||||
}
|
||||
|
||||
// An invalid bearer token must not authenticate as the user.
|
||||
// An invalid bearer token is an authentication failure, not an anonymous upload.
|
||||
badRequest := multipartUploadRequest(t, "/api/v1/upload", "file", "x.txt", "x")
|
||||
badRequest.Header.Set("Accept", "application/json")
|
||||
badRequest.Header.Set("Authorization", "Bearer wbx_bogus.secret")
|
||||
badResponse := httptest.NewRecorder()
|
||||
app.Upload(badResponse, badRequest)
|
||||
if badResponse.Code != http.StatusCreated {
|
||||
t.Fatalf("anonymous fallback upload status = %d, body = %s", badResponse.Code, badResponse.Body.String())
|
||||
if badResponse.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("invalid token upload status = %d, body = %s", badResponse.Code, badResponse.Body.String())
|
||||
}
|
||||
var badPayload services.UploadResult
|
||||
if err := json.Unmarshal(badResponse.Body.Bytes(), &badPayload); err != nil {
|
||||
t.Fatalf("json.Unmarshal returned error: %v", err)
|
||||
}
|
||||
|
||||
func TestAnonymousUploadWithoutBearerStillWorks(t *testing.T) {
|
||||
app, cleanup := newTestApp(t)
|
||||
defer cleanup()
|
||||
|
||||
response := httptest.NewRecorder()
|
||||
app.Upload(response, multipartUploadRequest(t, "/api/v1/upload", "file", "anonymous.txt", "anonymous"))
|
||||
if response.Code != http.StatusCreated {
|
||||
t.Fatalf("anonymous upload status = %d, body = %s", response.Code, response.Body.String())
|
||||
}
|
||||
badBox, err := app.uploadService.GetBox(badPayload.BoxID)
|
||||
}
|
||||
|
||||
func TestDisabledUserBearerTokenCannotUpload(t *testing.T) {
|
||||
app, cleanup := newTestApp(t)
|
||||
defer cleanup()
|
||||
|
||||
user, err := app.authService.CreateBootstrapUser("daniel", "daniel@example.test", "password123")
|
||||
if err != nil {
|
||||
t.Fatalf("GetBox returned error: %v", err)
|
||||
t.Fatalf("CreateBootstrapUser returned error: %v", err)
|
||||
}
|
||||
if badBox.OwnerID != "" {
|
||||
t.Fatalf("invalid token OwnerID = %q, want empty", badBox.OwnerID)
|
||||
tokenResult, err := app.authService.CreateAPIToken(user.ID, "cli")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateAPIToken returned error: %v", err)
|
||||
}
|
||||
if err := app.authService.DisableUser(user.ID, true); err != nil {
|
||||
t.Fatalf("DisableUser returned error: %v", err)
|
||||
}
|
||||
|
||||
request := multipartUploadRequest(t, "/api/v1/upload", "file", "blocked.txt", "blocked")
|
||||
request.Header.Set("Accept", "application/json")
|
||||
request.Header.Set("Authorization", "Bearer "+tokenResult.Plaintext)
|
||||
response := httptest.NewRecorder()
|
||||
app.Upload(response, request)
|
||||
if response.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("disabled bearer upload status = %d, body = %s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -251,23 +251,32 @@ func (a *App) loginAndRedirect(w http.ResponseWriter, r *http.Request, email, pa
|
||||
}
|
||||
|
||||
func (a *App) currentUser(r *http.Request) (services.User, bool) {
|
||||
user, ok, _ := a.currentUserWithAuthError(r)
|
||||
return user, ok
|
||||
}
|
||||
|
||||
func (a *App) currentUserWithAuthError(r *http.Request) (services.User, bool, error) {
|
||||
// Personal access tokens via Authorization: Bearer act as their owning user.
|
||||
// A bearer header is never set by browsers cross-site, so this path is not
|
||||
// subject to CSRF and intentionally bypasses the session cookie.
|
||||
if header := r.Header.Get("Authorization"); header != "" {
|
||||
if raw, ok := strings.CutPrefix(header, "Bearer "); ok {
|
||||
if user, err := a.authService.UserForAPIToken(raw); err == nil {
|
||||
return user, true
|
||||
user, err := a.authService.UserForAPIToken(raw)
|
||||
if err != nil {
|
||||
return services.User{}, false, err
|
||||
}
|
||||
return services.User{}, false
|
||||
return user, true, nil
|
||||
}
|
||||
}
|
||||
cookie, err := r.Cookie(userSessionCookieName)
|
||||
if err != nil {
|
||||
return services.User{}, false
|
||||
return services.User{}, false, nil
|
||||
}
|
||||
user, _, err := a.authService.UserForSession(cookie.Value)
|
||||
return user, err == nil
|
||||
if err != nil {
|
||||
return services.User{}, false, nil
|
||||
}
|
||||
return user, true, nil
|
||||
}
|
||||
|
||||
func (a *App) requireUser(w http.ResponseWriter, r *http.Request) (services.User, bool) {
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
|
||||
func TestSetStaticCacheHeaders(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"/static/css/app.css": "public, max-age=86400",
|
||||
"/static/js/app.js": "public, max-age=86400",
|
||||
"/static/css/00-base.css": "public, max-age=86400",
|
||||
"/static/js/00-utils.js": "public, max-age=86400",
|
||||
"/static/img/preview.webp": "public, max-age=31536000, immutable",
|
||||
"/static/fonts/ui.woff2": "public, max-age=31536000, immutable",
|
||||
"/static/videos/intro.mp4": "public, max-age=31536000, immutable",
|
||||
|
||||
@@ -16,7 +16,11 @@ import (
|
||||
)
|
||||
|
||||
func (a *App) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
user, loggedIn := a.currentUser(r)
|
||||
user, loggedIn, authErr := a.currentUserWithAuthError(r)
|
||||
if authErr != nil {
|
||||
helpers.WriteJSONError(w, http.StatusUnauthorized, "invalid bearer token")
|
||||
return
|
||||
}
|
||||
isAdminUpload := loggedIn && user.Role == services.UserRoleAdmin
|
||||
settings, err := a.settingsService.UploadPolicy()
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user