mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-09-26 22:16:14 -04:00
Do not set WWW-Authenticate headers when an active signed URL authentication attempt fails. Signed URL clients cannot respond to Basic or Bearer challenges, and advertising them may trigger unintended authentication prompts. Keep returning 401 Unauthorized while preserving the existing challenge behavior for unsigned requests and disabled signed URL mechanisms.
360 lines
13 KiB
Go
360 lines
13 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
|
"github.com/opencloud-eu/opencloud/pkg/log"
|
|
"github.com/opencloud-eu/opencloud/services/proxy/pkg/config"
|
|
"github.com/opencloud-eu/opencloud/services/proxy/pkg/router"
|
|
"github.com/opencloud-eu/opencloud/services/proxy/pkg/user/backend"
|
|
backendmocks "github.com/opencloud-eu/opencloud/services/proxy/pkg/user/backend/mocks"
|
|
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
|
"github.com/opencloud-eu/reva/v2/pkg/signedurl"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"go-micro.dev/v4/store"
|
|
)
|
|
|
|
func TestSignedURLAuthLegacy_shouldServe(t *testing.T) {
|
|
pua := SignedURLAuthenticator{}
|
|
tests := []struct {
|
|
url string
|
|
enabled bool
|
|
expected bool
|
|
}{
|
|
{"https://example.com/example.jpg", true, false},
|
|
{"https://example.com/example.jpg?OC-Signature=something", true, true},
|
|
{"https://example.com/example.jpg", false, false},
|
|
{"https://example.com/example.jpg?OC-Signature=something", false, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
pua.PreSignedURLConfig.Enabled = tt.enabled
|
|
r := httptest.NewRequest("", tt.url, nil)
|
|
result := pua.shouldServeLegacy(r)
|
|
|
|
if result != tt.expected {
|
|
t.Errorf("with %s expected %t got %t", tt.url, tt.expected, result)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSignedURLAuth_shouldServe(t *testing.T) {
|
|
tests := []struct {
|
|
url string
|
|
secret string
|
|
enabled bool
|
|
expected bool
|
|
}{
|
|
{"https://example.com/example.jpg", "", true, false},
|
|
{"https://example.com/example.jpg", "", false, false},
|
|
{"https://example.com/example.jpg?oc-jwt-sig=something1", "secret", true, true},
|
|
{"https://example.com/example.jpg?oc-jwt-sig=something2", "", true, false},
|
|
{"https://example.com/example.jpg?oc-jwt-sig=something3", "secret", false, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
pua := SignedURLAuthenticator{}
|
|
pua.PreSignedURLConfig.Enabled = tt.enabled
|
|
if tt.secret != "" {
|
|
signURLVerifier, err := signedurl.NewJWTSignedURL(signedurl.WithSecret(tt.secret))
|
|
if err != nil {
|
|
t.Fatalf("failed to create signed URL verifier: %v", err)
|
|
}
|
|
pua.URLVerifier = signURLVerifier
|
|
}
|
|
r := httptest.NewRequest("", tt.url, nil)
|
|
result := pua.shouldServe(r)
|
|
|
|
if result != tt.expected {
|
|
t.Errorf("with %s expected %t got %t", tt.url, tt.expected, result)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSignedURLAuth_authenticateRejectsDisallowedMethods(t *testing.T) {
|
|
signer, err := signedurl.NewJWTSignedURL(signedurl.WithSecret("secret"))
|
|
if err != nil {
|
|
t.Fatalf("failed to create signer: %v", err)
|
|
}
|
|
signed, err := signer.Sign("https://example.com/dav/spaces/file.txt", "userid", time.Minute)
|
|
if err != nil {
|
|
t.Fatalf("failed to sign url: %v", err)
|
|
}
|
|
|
|
pua := SignedURLAuthenticator{Logger: log.NewLogger(), URLVerifier: signer}
|
|
pua.PreSignedURLConfig.AllowedHTTPMethods = []string{"GET"}
|
|
|
|
for _, method := range []string{http.MethodPost, http.MethodPut, http.MethodDelete, "PROPFIND", "MOVE"} {
|
|
r := httptest.NewRequest(method, signed, nil)
|
|
if _, ok := pua.authenticate(r); ok {
|
|
t.Errorf("expected %s with a signed url to be rejected", method)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSignedURLAuthFailureSuppressesAuthenticationChallenge(t *testing.T) {
|
|
oldStrategies := SupportedAuthStrategies
|
|
SupportedAuthStrategies = nil
|
|
t.Cleanup(func() { SupportedAuthStrategies = oldStrategies })
|
|
|
|
verifier, err := signedurl.NewJWTSignedURL(signedurl.WithSecret("secret"))
|
|
if err != nil {
|
|
t.Fatalf("failed to create signed URL verifier: %v", err)
|
|
}
|
|
userProvider := &backendmocks.UserBackend{}
|
|
userProvider.On("GetUserByClaims", mock.Anything, "username", "").Return(nil, "", backend.ErrAccountNotFound)
|
|
|
|
tests := []struct {
|
|
name string
|
|
url string
|
|
authenticator SignedURLAuthenticator
|
|
expectedChallenge bool
|
|
}{
|
|
{
|
|
name: "invalid signed URL",
|
|
url: "https://example.com/file?oc-jwt-sig=invalid",
|
|
authenticator: SignedURLAuthenticator{
|
|
Logger: log.NewLogger(),
|
|
PreSignedURLConfig: config.PreSignedURL{AllowedHTTPMethods: []string{http.MethodGet}},
|
|
URLVerifier: verifier,
|
|
},
|
|
expectedChallenge: false,
|
|
},
|
|
{
|
|
name: "invalid legacy signed URL",
|
|
url: "https://example.com/file?OC-Signature=invalid",
|
|
authenticator: SignedURLAuthenticator{
|
|
Logger: log.NewLogger(),
|
|
PreSignedURLConfig: config.PreSignedURL{Enabled: true},
|
|
UserProvider: userProvider,
|
|
},
|
|
expectedChallenge: false,
|
|
},
|
|
{
|
|
name: "legacy signed URLs disabled",
|
|
url: "https://example.com/file?OC-Signature=invalid",
|
|
authenticator: SignedURLAuthenticator{},
|
|
expectedChallenge: true,
|
|
},
|
|
{
|
|
name: "signed URL verifier disabled",
|
|
url: "https://example.com/file?oc-jwt-sig=invalid",
|
|
authenticator: SignedURLAuthenticator{},
|
|
expectedChallenge: true,
|
|
},
|
|
{
|
|
name: "unsigned URL",
|
|
url: "https://example.com/file",
|
|
authenticator: SignedURLAuthenticator{URLVerifier: verifier},
|
|
expectedChallenge: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
SupportedAuthStrategies = nil
|
|
req := httptest.NewRequest(http.MethodGet, tt.url, nil)
|
|
req = req.WithContext(router.SetRoutingInfo(req.Context(), router.RoutingInfo{}))
|
|
rr := httptest.NewRecorder()
|
|
nextCalled := false
|
|
handler := Authentication([]Authenticator{tt.authenticator}, EnableBasicAuth(true))(
|
|
http.HandlerFunc(func(http.ResponseWriter, *http.Request) { nextCalled = true }),
|
|
)
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, rr.Code)
|
|
assert.False(t, nextCalled)
|
|
if tt.expectedChallenge {
|
|
assert.NotEmpty(t, rr.Header().Values(WwwAuthenticate))
|
|
} else {
|
|
assert.Empty(t, rr.Header().Values(WwwAuthenticate))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSignedURLAuth_allRequiredParametersPresent(t *testing.T) {
|
|
pua := SignedURLAuthenticator{}
|
|
baseURL := "https://example.com/example.jpg?"
|
|
tests := []struct {
|
|
params string
|
|
errorMessage string
|
|
}{
|
|
{"OC-Signature=something&OC-Credential=something&OC-Date=something&OC-Expires=something&OC-Verb=something", ""},
|
|
{"OC-Credential=something&OC-Date=something&OC-Expires=something&OC-Verb=something", "required OC-Signature parameter not found"},
|
|
{"OC-Signature=something&OC-Date=something&OC-Expires=something&OC-Verb=something", "required OC-Credential parameter not found"},
|
|
{"OC-Signature=something&OC-Credential=something&OC-Expires=something&OC-Verb=something", "required OC-Date parameter not found"},
|
|
{"OC-Signature=something&OC-Credential=something&OC-Date=something&OC-Verb=something", "required OC-Expires parameter not found"},
|
|
{"OC-Signature=something&OC-Credential=something&OC-Date=something&OC-Expires=something", "required OC-Verb parameter not found"},
|
|
}
|
|
for _, tt := range tests {
|
|
r := httptest.NewRequest("", baseURL+tt.params, nil)
|
|
err := pua.allRequiredParametersArePresent(r.URL.Query())
|
|
if tt.errorMessage != "" {
|
|
assert.EqualError(t, err, tt.errorMessage, tt.params)
|
|
} else {
|
|
assert.Nil(t, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSignedURLAuth_requestMethodMatches(t *testing.T) {
|
|
pua := SignedURLAuthenticator{}
|
|
tests := []struct {
|
|
method string
|
|
url string
|
|
errorMessage string
|
|
}{
|
|
{"GET", "https://example.com/example.jpg?OC-Verb=GET", ""},
|
|
{"GET", "https://example.com/example.jpg?OC-Verb=get", ""},
|
|
{"POST", "https://example.com/example.jpg?OC-Verb=GET", "required OC-Verb parameter did not match request method"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
r := httptest.NewRequest(tt.method, tt.url, nil)
|
|
err := pua.requestMethodMatches(r.Method, r.URL.Query())
|
|
if tt.errorMessage != "" {
|
|
assert.EqualError(t, err, tt.errorMessage, tt.url)
|
|
} else {
|
|
assert.Nil(t, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSignedURLAuth_requestMethodIsAllowed(t *testing.T) {
|
|
pua := SignedURLAuthenticator{}
|
|
tests := []struct {
|
|
method string
|
|
allowed []string
|
|
errorMessage string
|
|
}{
|
|
{"GET", []string{}, "request method is not listed in PreSignedURLConfig AllowedHTTPMethods"},
|
|
{"GET", []string{"POST"}, "request method is not listed in PreSignedURLConfig AllowedHTTPMethods"},
|
|
{"GET", []string{"GET"}, ""},
|
|
{"GET", []string{"get"}, ""},
|
|
{"GET", []string{"POST", "GET"}, ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
pua.PreSignedURLConfig.AllowedHTTPMethods = tt.allowed
|
|
err := pua.requestMethodIsAllowed(tt.method)
|
|
if tt.errorMessage != "" {
|
|
assert.EqualError(t, err, tt.errorMessage)
|
|
} else {
|
|
assert.Nil(t, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSignedURLAuth_urlIsExpired(t *testing.T) {
|
|
nowFunc := func() time.Time {
|
|
t, _ := time.Parse(time.RFC3339, "2020-02-02T12:30:00.000Z")
|
|
return t
|
|
}
|
|
pua := SignedURLAuthenticator{
|
|
Now: nowFunc,
|
|
}
|
|
|
|
tests := []struct {
|
|
url string
|
|
errorMessage string
|
|
}{
|
|
// a valid signed url
|
|
{"http://example.com/example.jpg?OC-Date=2020-02-02T12:29:00.000Z&OC-Expires=61", ""},
|
|
// invalid expiry
|
|
{"http://example.com/example.jpg?OC-Date=2020-02-02T12:29:00.000Z&OC-Expires=invalid", "time: invalid duration \"invalids\""},
|
|
// wrong date format on OC-Date
|
|
{"http://example.com/example.jpg?OC-Date=2020-02-02TTT12:29:00.000Z&OC-Expires=5", "parsing time \"2020-02-02TTT12:29:00.000Z\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"TT12:29:00.000Z\" as \"15\""},
|
|
// expired - 12:29:00 + 59s < 12:30
|
|
{"http://example.com/example.jpg?OC-Date=2020-02-02T12:29:00.000Z&OC-Expires=59", "URL is expired"},
|
|
// expired - basically url was created yesterday
|
|
{"http://example.com/example.jpg?OC-Date=2020-02-01T12:29:00.000Z&OC-Expires=59", "URL is expired"},
|
|
// future OC-Date - also valid now
|
|
{"http://example.com/example.jpg?OC-Date=2020-02-03T12:29:00.000Z&OC-Expires=59", ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
r := httptest.NewRequest("", tt.url, nil)
|
|
err := pua.urlIsExpired(r.URL.Query())
|
|
if tt.errorMessage != "" {
|
|
assert.EqualError(t, err, tt.errorMessage, tt.url)
|
|
} else {
|
|
assert.Nil(t, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSignedURLAuth_createSignature(t *testing.T) {
|
|
pua := SignedURLAuthenticator{}
|
|
expected := "27d2ebea381384af3179235114801dcd00f91e46f99fca72575301cf3948101d"
|
|
s := pua.createSignature("something", []byte("somerandomkey"))
|
|
|
|
if s != expected {
|
|
t.Fail()
|
|
}
|
|
}
|
|
|
|
func TestSignedURLAuth_validate(t *testing.T) {
|
|
nowFunc := func() time.Time {
|
|
t, _ := time.Parse(time.RFC3339, "2020-02-02T12:30:00.000Z")
|
|
return t
|
|
}
|
|
cfg := config.PreSignedURL{
|
|
AllowedHTTPMethods: []string{"get"},
|
|
Enabled: true,
|
|
}
|
|
pua := SignedURLAuthenticator{
|
|
PreSignedURLConfig: cfg,
|
|
Store: store.NewMemoryStore(),
|
|
Now: nowFunc,
|
|
}
|
|
|
|
pua.Store.Write(&store.Record{
|
|
Key: "useri",
|
|
Value: []byte("1234567890"),
|
|
Metadata: nil,
|
|
})
|
|
|
|
tests := []struct {
|
|
now string
|
|
url string
|
|
errorMessage string
|
|
}{
|
|
{"2020-02-02T12:30:00.000Z", "http://example.com/example.jpg?OC-Date=2020-02-02T12:29:00.000Z&OC-Expires=invalid", "required OC-Signature parameter not found"},
|
|
{"2020-02-02T12:30:00.000Z", "http://cloud.example.net/?OC-Credential=alice&OC-Date=2019-05-14T11%3A01%3A58.135Z&OC-Expires=1200&OC-Verb=GET&OC-Signature=f9e53a1ee23caef10f72ec392c1b537317491b687bfdd224c782be197d9ca2b6", "URL is expired"},
|
|
{"2019-05-14T11:02:00.000Z", "http://cloud.example.net/?OC-Credential=alice&OC-Date=2019-05-14T11%3A01%3A58.135Z&OC-Expires=1200&OC-Verb=GET&OC-Signature=f9e53a1ee23caef10f72ec392c1b537317491b687bfdd224c782be197d9ca2b", "signature mismatch: expected f9e53a1ee23caef10f72ec392c1b537317491b687bfdd224c782be197d9ca2b6 != actual f9e53a1ee23caef10f72ec392c1b537317491b687bfdd224c782be197d9ca2b"},
|
|
{"2019-05-14T11:02:00.000Z", "http://cloud.example.net/?OC-Credential=alice&OC-Date=2019-05-14T11%3A01%3A58.135Z&OC-Expires=1200&OC-Verb=GET&OC-Signature=f9e53a1ee23caef10f72ec392c1b537317491b687bfdd224c782be197d9ca2b6", ""},
|
|
{"2019-05-14T11:02:00.000Z", "http://cloud.example.net/?OC-Date=2019-05-14T11%3A01%3A58.135Z&OC-Expires=1200&OC-Verb=GET&OC-Credential=alice&OC-Signature=f9e53a1ee23caef10f72ec392c1b537317491b687bfdd224c782be197d9ca2b6", ""},
|
|
{"2019-05-14T11:02:00.000Z", "http://cloud.example.net/?OC-Algo=PBKDF2%2F10000-SHA512&OC-Date=2019-05-14T11%3A01%3A58.135Z&OC-Expires=1200&OC-Verb=GET&OC-Credential=alice&OC-Signature=f9e53a1ee23caef10f72ec392c1b537317491b687bfdd224c782be197d9ca2b6", ""},
|
|
{"2024-02-07T12:03:11.966Z", "http://localhost:33001/try?id=1&id=2&OC-Credential=user&OC-Date=2024-02-07T12%3A03%3A11.966Z&OC-Expires=2&OC-Verb=GET&OC-Algo=PBKDF2%2F10000-SHA512&OC-Signature=86e21a1efbf0be989a206109cfedf70a22f338dc8995e849ce002032bc6741c5", ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
u := userpb.User{
|
|
Id: &userpb.UserId{OpaqueId: "useri"},
|
|
DisplayName: "Test User",
|
|
}
|
|
ctx := revactx.ContextSetUser(context.Background(), &u)
|
|
|
|
pua.Now = func() time.Time {
|
|
t, _ := time.Parse(time.RFC3339, tt.now)
|
|
return t
|
|
}
|
|
|
|
r := httptest.NewRequest("", tt.url, nil).WithContext(ctx)
|
|
err := pua.validate(r)
|
|
if tt.errorMessage == "" {
|
|
assert.Nil(t, err)
|
|
} else {
|
|
assert.EqualError(t, err, tt.errorMessage)
|
|
}
|
|
}
|
|
}
|