Skip to content

Commit 748e41b

Browse files
authored
CBG-4942 obey db scope for CORS.LoginOrigin (#7831)
1 parent 22c64ce commit 748e41b

File tree

6 files changed

+119
-54
lines changed

6 files changed

+119
-54
lines changed

rest/cors_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
package rest
1010

1111
import (
12+
"fmt"
1213
"net/http"
1314
"strconv"
1415
"strings"
@@ -404,6 +405,74 @@ func TestCORSOriginPerDatabase(t *testing.T) {
404405
}
405406
}
406407

408+
func TestCORSLoginOriginPerDatabase(t *testing.T) {
409+
// Override the default (example.com) CORS configuration in the DbConfig for /db:
410+
rt := NewRestTesterPersistentConfigNoDB(t)
411+
defer rt.Close()
412+
dbConfig := rt.NewDbConfig()
413+
dbConfig.CORS = &auth.CORSConfig{
414+
Origin: []string{"http://couchbase.com", "http://staging.couchbase.com"},
415+
LoginOrigin: []string{"http://couchbase.com"},
416+
Headers: []string{},
417+
}
418+
RequireStatus(t, rt.CreateDatabase("dbloginorigin", dbConfig), http.StatusCreated)
419+
420+
const username = "alice"
421+
rt.CreateUser(username, nil)
422+
423+
testCases := []struct {
424+
name string
425+
origin string
426+
responseCode int
427+
responseErrorBody string
428+
}{
429+
{
430+
name: "CORS login origin allowed couchbase",
431+
origin: "http://couchbase.com",
432+
responseCode: http.StatusOK,
433+
},
434+
{
435+
name: "CORS login origin not allowed staging",
436+
origin: "http://staging.couchbase.com",
437+
responseCode: http.StatusBadRequest,
438+
responseErrorBody: "No CORS",
439+
},
440+
}
441+
for _, test := range testCases {
442+
rt.Run(test.name, func(t *testing.T) {
443+
reqHeaders := map[string]string{
444+
"Origin": test.origin,
445+
"Authorization": GetBasicAuthHeader(t, username, RestTesterDefaultUserPassword),
446+
}
447+
resp := rt.SendRequestWithHeaders(http.MethodPost, "/{{.db}}/_session", "", reqHeaders)
448+
RequireStatus(t, resp, test.responseCode)
449+
if test.responseErrorBody != "" {
450+
require.Contains(t, resp.Body.String(), test.responseErrorBody)
451+
// the access control headers are returned based on Origin and not LoginOrigin which could be considered a bug
452+
require.Equal(t, test.origin, resp.Header().Get(accessControlAllowOrigin))
453+
} else {
454+
require.Equal(t, test.origin, resp.Header().Get(accessControlAllowOrigin))
455+
}
456+
if test.responseCode == http.StatusOK {
457+
cookie, err := http.ParseSetCookie(resp.Header().Get("Set-Cookie"))
458+
require.NoError(t, err)
459+
require.NotEmpty(t, cookie.Path)
460+
reqHeaders["Cookie"] = fmt.Sprintf("%s=%s", cookie.Name, cookie.Value)
461+
}
462+
resp = rt.SendRequestWithHeaders(http.MethodDelete, "/{{.db}}/_session", "", reqHeaders)
463+
RequireStatus(t, resp, test.responseCode)
464+
if test.responseErrorBody != "" {
465+
require.Contains(t, resp.Body.String(), test.responseErrorBody)
466+
// the access control headers are returned based on Origin and not LoginOrigin which could be considered a bug
467+
require.Equal(t, test.origin, resp.Header().Get(accessControlAllowOrigin))
468+
} else {
469+
require.Equal(t, test.origin, resp.Header().Get(accessControlAllowOrigin))
470+
}
471+
472+
})
473+
}
474+
}
475+
407476
func TestCORSValidation(t *testing.T) {
408477
rt := NewRestTester(t, &RestTesterConfig{
409478
PersistentConfig: true,

rest/facebook.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"net/http"
1313
"net/url"
1414

15-
"github.com/couchbase/sync_gateway/auth"
1615
"github.com/couchbase/sync_gateway/base"
1716
)
1817

@@ -26,18 +25,14 @@ type FacebookResponse struct {
2625

2726
// POST /_facebook creates a facebook-based login session and sets its cookie.
2827
func (h *handler) handleFacebookPOST() error {
29-
// CORS not allowed for login #115 #762
30-
originHeader := h.rq.Header["Origin"]
31-
if len(originHeader) > 0 {
32-
matched := auth.MatchedOrigin(h.server.Config.API.CORS.LoginOrigin, originHeader)
33-
if matched == "" {
34-
return base.HTTPErrorf(http.StatusBadRequest, "No CORS")
35-
}
28+
err := h.checkLoginCORS()
29+
if err != nil {
30+
return err
3631
}
3732
var params struct {
3833
AccessToken string `json:"access_token"`
3934
}
40-
err := h.readJSONInto(&params)
35+
err = h.readJSONInto(&params)
4136
if err != nil {
4237
return err
4338
}

rest/google.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"net/http"
1515
"slices"
1616

17-
"github.com/couchbase/sync_gateway/auth"
1817
"github.com/couchbase/sync_gateway/base"
1918
)
2019

@@ -29,20 +28,16 @@ type GoogleResponse struct {
2928

3029
// POST /_google creates a google-based login session and sets its cookie.
3130
func (h *handler) handleGooglePOST() error {
32-
// CORS not allowed for login #115 #762
33-
originHeader := h.rq.Header["Origin"]
34-
if len(originHeader) > 0 {
35-
matched := auth.MatchedOrigin(h.server.Config.API.CORS.LoginOrigin, originHeader)
36-
if matched == "" {
37-
return base.HTTPErrorf(http.StatusBadRequest, "No CORS")
38-
}
31+
err := h.checkLoginCORS()
32+
if err != nil {
33+
return err
3934
}
4035

4136
var params struct {
4237
IDToken string `json:"id_token"`
4338
}
4439

45-
err := h.readJSONInto(&params)
40+
err = h.readJSONInto(&params)
4641
if err != nil {
4742
return err
4843
}

rest/handler.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -336,10 +336,7 @@ func (h *handler) validateAndWriteHeaders(method handlerMethod, accessPermission
336336
defer func() {
337337
// Now that we know the DB, add CORS headers to the response:
338338
if h.privs != adminPrivs && h.privs != metricsPrivs {
339-
cors := h.server.Config.API.CORS
340-
if h.db != nil {
341-
cors = h.db.CORS
342-
}
339+
cors := h.getCORSConfig()
343340
if !cors.IsEmpty() {
344341
cors.AddResponseHeaders(h.rq, h.response)
345342
}
@@ -1784,3 +1781,11 @@ func (h *handler) pathTemplateContains(pattern string) bool {
17841781
}
17851782
return strings.Contains(pathTemplate, pattern)
17861783
}
1784+
1785+
// getCORSConfig will return the CORS config for the handler's database if set, otherwise it will return the server CORS config
1786+
func (h *handler) getCORSConfig() *auth.CORSConfig {
1787+
if h.db != nil {
1788+
return h.db.CORS
1789+
}
1790+
return h.server.Config.API.CORS
1791+
}

rest/session_api.go

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,9 @@ func (h *handler) handleSessionGET() error {
3636

3737
// POST /_session creates a login session and sets its cookie
3838
func (h *handler) handleSessionPOST() error {
39-
// CORS not allowed for login #115 #762
40-
originHeader := h.rq.Header["Origin"]
41-
if len(originHeader) > 0 {
42-
matched := ""
43-
if h.server.Config.API.CORS != nil {
44-
matched = auth.MatchedOrigin(h.server.Config.API.CORS.LoginOrigin, originHeader)
45-
}
46-
if matched == "" {
47-
return base.HTTPErrorf(http.StatusBadRequest, "No CORS")
48-
}
39+
err := h.checkLoginCORS()
40+
if err != nil {
41+
return err
4942
}
5043

5144
// NOTE: handleSessionPOST doesn't handle creating users from OIDC - checkPublicAuth calls out into AuthenticateUntrustedJWT.
@@ -100,18 +93,10 @@ func (h *handler) getUserFromSessionRequestBody() (auth.User, error) {
10093

10194
// DELETE /_session logs out the current session
10295
func (h *handler) handleSessionDELETE() error {
103-
// CORS not allowed for login #115 #762
104-
originHeader := h.rq.Header["Origin"]
105-
if len(originHeader) > 0 {
106-
matched := ""
107-
if h.server.Config.API.CORS != nil {
108-
matched = auth.MatchedOrigin(h.server.Config.API.CORS.LoginOrigin, originHeader)
109-
}
110-
if matched == "" {
111-
return base.HTTPErrorf(http.StatusBadRequest, "No CORS")
112-
}
96+
err := h.checkLoginCORS()
97+
if err != nil {
98+
return err
11399
}
114-
115100
cookie := h.db.Authenticator(h.ctx()).DeleteSessionForCookie(h.ctx(), h.rq)
116101
if cookie == nil {
117102
return base.HTTPErrorf(http.StatusNotFound, "no session")
@@ -340,3 +325,16 @@ func (h *handler) formatSessionResponse(user auth.User) db.Body {
340325
return response
341326

342327
}
328+
329+
// checkLoginCORS validates the auth.CORSConfig.LoginOrigin section of CORS for requests.
330+
// Note: Validation of the general Origin header against auth.CORSConfig.Origin happens separately in validateAndWriteHeaders.
331+
func (h *handler) checkLoginCORS() error {
332+
originHeader := h.rq.Header["Origin"]
333+
if len(originHeader) > 0 {
334+
cors := h.getCORSConfig()
335+
if cors.IsEmpty() || auth.MatchedOrigin(cors.LoginOrigin, originHeader) == "" {
336+
return base.HTTPErrorf(http.StatusBadRequest, "No CORS")
337+
}
338+
}
339+
return nil
340+
}

rest/session_test.go

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,16 @@ func TestCORSLoginOriginOnSessionPost(t *testing.T) {
4141

4242
// #issue 991
4343
func TestCORSLoginOriginOnSessionPostNoCORSConfig(t *testing.T) {
44-
rt := NewRestTester(t, nil)
44+
rt := NewRestTesterPersistentConfigNoDB(t)
4545
defer rt.Close()
46+
// Set CORS to nil, RestTester initializes CORS by default and it is inherited when creating a DatabaseContext
47+
rt.ServerContext().Config.API.CORS = nil
4648

49+
RequireStatus(t, rt.CreateDatabase("db", rt.NewDbConfig()), http.StatusCreated)
4750
reqHeaders := map[string]string{
4851
"Origin": "http://example.com",
4952
}
5053

51-
// Set CORS to nil
52-
sc := rt.ServerContext()
53-
sc.Config.API.CORS = nil
54-
5554
response := rt.SendRequestWithHeaders("POST", "/db/_session", `{"name":"jchris","password":"secret"}`, reqHeaders)
5655
RequireStatus(t, response, 400)
5756
}
@@ -88,17 +87,21 @@ func TestCORSLogoutOriginOnSessionDelete(t *testing.T) {
8887
}
8988

9089
func TestCORSLogoutOriginOnSessionDeleteNoCORSConfig(t *testing.T) {
91-
rt := NewRestTester(t, &RestTesterConfig{GuestEnabled: true})
90+
rt := NewRestTesterPersistentConfigNoDB(t)
9291
defer rt.Close()
9392

93+
// Set CORS to nil, RestTester initializes CORS by default and it is inherited when creating a DatabaseContext
94+
rt.ServerContext().Config.API.CORS = nil
95+
96+
const username = "alice"
97+
RequireStatus(t, rt.CreateDatabase("db", rt.NewDbConfig()), http.StatusCreated)
98+
rt.CreateUser(username, nil)
99+
94100
reqHeaders := map[string]string{
95-
"Origin": "http://example.com",
101+
"Origin": "http://example.com",
102+
"Authorization": GetBasicAuthHeader(t, username, RestTesterDefaultUserPassword),
96103
}
97104

98-
// Set CORS to nil
99-
sc := rt.ServerContext()
100-
sc.Config.API.CORS = nil
101-
102105
response := rt.SendRequestWithHeaders("DELETE", "/db/_session", "", reqHeaders)
103106
RequireStatus(t, response, 400)
104107

0 commit comments

Comments
 (0)