From 6555cfcac3dc784c0c46c0a42e9cf6dbcbee2517 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Sun, 17 Nov 2024 02:21:00 +0800 Subject: [PATCH] Fix basic auth with webauthn (#32531) (#32536) Backport #32531 by @lunny WebAuthn should behave the same way as TOTP. When enabled, basic auth with username/password should need to WebAuthn auth, otherwise returned 401. Co-authored-by: Lunny Xiao --- services/auth/basic.go | 10 ++++++ tests/integration/api_twofa_test.go | 53 +++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/services/auth/basic.go b/services/auth/basic.go index 90bd642370..1f6c3a442d 100644 --- a/services/auth/basic.go +++ b/services/auth/basic.go @@ -5,6 +5,7 @@ package auth import ( + "errors" "net/http" "strings" @@ -141,6 +142,15 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore } if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() { + // Check if the user has webAuthn registration + hasWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(req.Context(), u.ID) + if err != nil { + return nil, err + } + if hasWebAuthn { + return nil, errors.New("Basic authorization is not allowed while webAuthn enrolled") + } + if err := validateTOTP(req, u); err != nil { return nil, err } diff --git a/tests/integration/api_twofa_test.go b/tests/integration/api_twofa_test.go index aad806b6dc..18e6fa91b7 100644 --- a/tests/integration/api_twofa_test.go +++ b/tests/integration/api_twofa_test.go @@ -53,3 +53,56 @@ func TestAPITwoFactor(t *testing.T) { req.Header.Set("X-Gitea-OTP", passcode) MakeRequest(t, req, http.StatusOK) } + +func TestBasicAuthWithWebAuthn(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + // user1 has no webauthn enrolled, he can request API with basic auth + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + unittest.AssertNotExistsBean(t, &auth_model.WebAuthnCredential{UserID: user1.ID}) + req := NewRequest(t, "GET", "/api/v1/user") + req.SetBasicAuth(user1.Name, "password") + MakeRequest(t, req, http.StatusOK) + + // user1 has no webauthn enrolled, he can request git protocol with basic auth + req = NewRequest(t, "GET", "/user2/repo1/info/refs") + req.SetBasicAuth(user1.Name, "password") + MakeRequest(t, req, http.StatusOK) + + // user1 has no webauthn enrolled, he can request container package with basic auth + req = NewRequest(t, "GET", "/v2/token") + req.SetBasicAuth(user1.Name, "password") + resp := MakeRequest(t, req, http.StatusOK) + + type tokenResponse struct { + Token string `json:"token"` + } + var tokenParsed tokenResponse + DecodeJSON(t, resp, &tokenParsed) + assert.NotEmpty(t, tokenParsed.Token) + + // user32 has webauthn enrolled, he can't request API with basic auth + user32 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 32}) + unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{UserID: user32.ID}) + + req = NewRequest(t, "GET", "/api/v1/user") + req.SetBasicAuth(user32.Name, "notpassword") + resp = MakeRequest(t, req, http.StatusUnauthorized) + + type userResponse struct { + Message string `json:"message"` + } + var userParsed userResponse + DecodeJSON(t, resp, &userParsed) + assert.EqualValues(t, "Basic authorization is not allowed while webAuthn enrolled", userParsed.Message) + + // user32 has webauthn enrolled, he can't request git protocol with basic auth + req = NewRequest(t, "GET", "/user2/repo1/info/refs") + req.SetBasicAuth(user32.Name, "notpassword") + MakeRequest(t, req, http.StatusUnauthorized) + + // user32 has webauthn enrolled, he can't request container package with basic auth + req = NewRequest(t, "GET", "/v2/token") + req.SetBasicAuth(user1.Name, "notpassword") + MakeRequest(t, req, http.StatusUnauthorized) +}