Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add experimental SCRAM-SHA-1/SCRAM-SHA-256 authentication support #4078

Merged
merged 96 commits into from Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
76835f1
removing unused variable
henvic Jan 18, 2024
60f5147
wip
henvic Jan 18, 2024
d14d3d8
Merge remote-tracking branch 'upstream/main' into feat/scram
henvic Jan 22, 2024
68bfc72
wip
henvic Jan 23, 2024
e374401
wip
henvic Jan 23, 2024
c0efd48
wip
henvic Jan 25, 2024
a7fd46d
Merge branch 'main' into feat/scram
henvic Jan 25, 2024
00c1d97
wip
henvic Jan 25, 2024
98fd929
fix
henvic Jan 26, 2024
5c19aa2
Merge branch 'main' into feat/scram
henvic Jan 26, 2024
fd19644
wip
henvic Jan 26, 2024
ac89673
wip
henvic Jan 26, 2024
18ae73b
wip
henvic Jan 26, 2024
ab9c1ea
wip
henvic Jan 26, 2024
29a93db
wip
henvic Jan 26, 2024
92b890e
wip
henvic Jan 26, 2024
a180d12
wip
henvic Jan 26, 2024
64f33af
wip
henvic Jan 26, 2024
7fd863b
Merge branch 'feat/scram-test' into feat/scram
henvic Jan 26, 2024
78c25ac
wip
henvic Jan 26, 2024
49409d1
wip
henvic Jan 29, 2024
dbbeb28
wip
henvic Jan 29, 2024
f160d89
wip
henvic Jan 29, 2024
23365f2
Merge branch 'main' into feat/scram
henvic Jan 29, 2024
9b7ebbd
wip
henvic Jan 29, 2024
c6a66b2
Merge branch 'main' into feat/scram
henvic Jan 29, 2024
ebf778d
wip
henvic Jan 29, 2024
1ef6181
wip (linter...)
henvic Jan 30, 2024
842ec79
wip
henvic Jan 30, 2024
2dfe802
wip
henvic Jan 30, 2024
29b72c5
Merge branch 'main' into feat/scram
henvic Jan 31, 2024
394c75d
wip
henvic Feb 1, 2024
fd89604
wip
henvic Feb 1, 2024
87235a7
wip
henvic Feb 1, 2024
6b4b444
wip
henvic Feb 1, 2024
8e7d356
wip
henvic Feb 1, 2024
7ef2b3d
Merge branch 'main' into feat/scram-sha256-password
henvic Feb 1, 2024
558bfe6
Merge branch 'feat/scram-sha256-password' into feat/scram
henvic Feb 2, 2024
9e39b66
wip
henvic Feb 2, 2024
78b982e
wip
henvic Feb 2, 2024
e4ad45e
wip
henvic Feb 2, 2024
96add90
Merge branch 'main' into feat/scram
henvic Feb 2, 2024
7ddd859
wip
henvic Feb 2, 2024
e31333b
wip
henvic Feb 2, 2024
1db222f
wip
henvic Feb 2, 2024
16ca802
wip
henvic Feb 2, 2024
8d9ea0a
wip
henvic Feb 4, 2024
375c356
Merge branch 'main' into feat/scram-sha256-password
AlekSi Feb 5, 2024
f0ec447
wip
henvic Feb 7, 2024
ba98fe1
Merge branch 'main' into feat/scram-sha256-password
henvic Feb 7, 2024
cb463d0
Merge branch 'feat/scram-sha256-password' into feat/scram
henvic Feb 7, 2024
8f34108
wip
henvic Feb 7, 2024
4e66355
Apply suggestions from code review
henvic Feb 7, 2024
6dbe868
wip
henvic Feb 7, 2024
1782138
Merge branch 'main' into feat/scram-sha256-password
henvic Feb 7, 2024
f1d13c0
wip
henvic Feb 7, 2024
30433e7
wip
henvic Feb 8, 2024
57f49fc
Truncate release blog post (#4047)
Fashander Feb 7, 2024
41bec80
Implement `database.Stats` for MySQL backend (#4034)
adetunjii Feb 8, 2024
b67ff41
Minor cleanups (#4046)
AlekSi Feb 8, 2024
f9d1636
wip
henvic Feb 8, 2024
e347e9d
Merge branch 'main' into feat/scram-sha256-password
henvic Feb 8, 2024
8be52f9
Merge branch 'feat/scram-sha256-password' into feat/scram
henvic Feb 8, 2024
f171566
wip
henvic Feb 9, 2024
e54bd11
wip
henvic Feb 9, 2024
487b8f8
wip
henvic Feb 9, 2024
4480f87
wip
henvic Feb 9, 2024
72b703d
wip
henvic Feb 9, 2024
2f9335c
wip
henvic Feb 9, 2024
36904ac
wip
henvic Feb 12, 2024
c506385
wip
henvic Feb 12, 2024
23ca6d5
wip
henvic Feb 12, 2024
0b86c69
wip
henvic Feb 13, 2024
ada60f9
Merge branch 'main' into feat/scram
henvic Feb 13, 2024
2a06484
removing ServerAPIVersion1
henvic Feb 13, 2024
53cf264
wip
henvic Feb 13, 2024
a5e3b6f
wip
henvic Feb 14, 2024
d531aed
wip
henvic Feb 14, 2024
b5655b3
wip
henvic Feb 14, 2024
dac64be
Merge branch 'main' into feat/scram
henvic Feb 14, 2024
36a8b2d
wip
henvic Feb 14, 2024
2151d7a
wip
henvic Feb 14, 2024
e51f3a1
wip
henvic Feb 14, 2024
0ae1a26
wip
henvic Feb 15, 2024
4745067
wip
henvic Feb 15, 2024
fcead13
wip
henvic Feb 15, 2024
24f04b6
wip
henvic Feb 15, 2024
6483dd3
Merge branch 'main' into feat/scram-sha-1
henvic Feb 16, 2024
d6b3553
wip
henvic Feb 16, 2024
1afe24f
wip
henvic Feb 16, 2024
b00eb25
wip
henvic Feb 16, 2024
ca537ec
Update internal/util/password/scram.go
henvic Feb 16, 2024
0149a7a
Apply suggestions from code review
henvic Feb 16, 2024
b7c242f
wip
henvic Feb 16, 2024
5a82b4a
Merge branch 'main' into feat/scram-sha-1
mergify[bot] Feb 19, 2024
69269b8
Merge branch 'main' into feat/scram-sha-1
mergify[bot] Feb 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
45 changes: 32 additions & 13 deletions integration/users/connection_test.go
Expand Up @@ -46,24 +46,48 @@ func TestAuthentication(t *testing.T) {

connectionMechanism string // if set, try to establish connection with this mechanism

userNotFound bool
wrongPassword bool
topologyError bool
errorMessage string
failsForFerretDB bool
userNotFound bool
wrongPassword bool
topologyError bool
errorMessage string
}{
"Success": {
username: "username", // when using the PLAIN mechanism we must use user "username"
password: "password",
mechanisms: []string{"PLAIN"},
connectionMechanism: "PLAIN",
},
"ScramSHA1": {
username: "scramsha1",
password: "password",
mechanisms: []string{"SCRAM-SHA-1"},
connectionMechanism: "SCRAM-SHA-1",
},
"ScramSHA256": {
username: "scramsha256",
password: "password",
mechanisms: []string{"SCRAM-SHA-256"},
connectionMechanism: "SCRAM-SHA-256",
},
"MultipleScramSHA1": {
username: "scramsha1multi",
password: "password",
mechanisms: []string{"SCRAM-SHA-1", "SCRAM-SHA-256"},
connectionMechanism: "SCRAM-SHA-1",
},
"MultipleScramSHA256": {
username: "scramsha256multi",
password: "password",
mechanisms: []string{"SCRAM-SHA-1", "SCRAM-SHA-256"},
connectionMechanism: "SCRAM-SHA-256",
},
"ScramSHA1Updated": {
username: "scramsha1updated",
password: "pass123",
updatePassword: "anotherpassword",
mechanisms: []string{"SCRAM-SHA-1"},
connectionMechanism: "SCRAM-SHA-1",
},
"ScramSHA256Updated": {
username: "scramsha256updated",
password: "pass123",
Expand Down Expand Up @@ -94,9 +118,8 @@ func TestAuthentication(t *testing.T) {
password: "password",
mechanisms: []string{"SCRAM-SHA-256"},
connectionMechanism: "SCRAM-SHA-1",
errorMessage: "Unable to use SCRAM-SHA-1 based authentication for user without any SCRAM-SHA-1 credentials registered",
topologyError: true,
failsForFerretDB: true,
errorMessage: "Unable to use SCRAM-SHA-1 based authentication for user without any SCRAM-SHA-1 credentials registered",
chilagrow marked this conversation as resolved.
Show resolved Hide resolved
},
}

Expand All @@ -107,10 +130,6 @@ func TestAuthentication(t *testing.T) {

var t testtb.TB = tt

if tc.failsForFerretDB {
t = setup.FailsForFerretDB(t, "https://github.com/FerretDB/FerretDB/issues/2012")
}

if !tc.userNotFound {
var (
// Use default mechanism for MongoDB and SCRAM-SHA-256 for FerretDB as SHA-1 won't be supported as it's deprecated.
Expand All @@ -121,7 +140,7 @@ func TestAuthentication(t *testing.T) {

if tc.mechanisms == nil {
if !setup.IsMongoDB(t) {
mechanisms = append(mechanisms, "SCRAM-SHA-256")
mechanisms = append(mechanisms, "SCRAM-SHA-1", "SCRAM-SHA-256")
}
} else {
mechanisms = bson.A{}
Expand All @@ -131,7 +150,7 @@ func TestAuthentication(t *testing.T) {
case "PLAIN":
hasPlain = true
fallthrough
case "SCRAM-SHA-256":
case "SCRAM-SHA-1", "SCRAM-SHA-256":
mechanisms = append(mechanisms, mechanism)
default:
t.Fatalf("unimplemented mechanism %s", mechanism)
Expand Down
29 changes: 29 additions & 0 deletions integration/users/create_user_test.go
Expand Up @@ -163,6 +163,17 @@ func TestCreateUser(t *testing.T) {
{"ok", float64(1)},
},
},
"SuccessWithSCRAMSHA1": {
payload: bson.D{
{"createUser", "success_user_with_scram_sha_1"},
{"roles", bson.A{}},
{"pwd", "password"},
{"mechanisms", bson.A{"SCRAM-SHA-1"}},
},
expected: bson.D{
{"ok", float64(1)},
},
},
"SuccessWithSCRAMSHA256": {
payload: bson.D{
{"createUser", "success_user_with_scram_sha_256"},
Expand Down Expand Up @@ -255,6 +266,10 @@ func TestCreateUser(t *testing.T) {
assertPlainCredentials(t, "PLAIN", must.NotFail(user.Get("credentials")).(*types.Document))
}

if payloadMechanisms.Contains("SCRAM-SHA-1") {
assertSCRAMSHA1Credentials(t, "SCRAM-SHA-1", must.NotFail(user.Get("credentials")).(*types.Document))
}

if payloadMechanisms.Contains("SCRAM-SHA-256") {
assertSCRAMSHA256Credentials(t, "SCRAM-SHA-256", must.NotFail(user.Get("credentials")).(*types.Document))
}
Expand Down Expand Up @@ -289,6 +304,20 @@ func assertPlainCredentials(t testtb.TB, key string, cred *types.Document) {
assert.NotEmpty(t, must.NotFail(c.Get("salt")))
}

// assertSCRAMSHA1Credentials checks if the credential is a valid SCRAM-SHA-1 credential.
func assertSCRAMSHA1Credentials(t testtb.TB, key string, cred *types.Document) {
t.Helper()

require.True(t, cred.Has(key), "missing credential %q", key)

c := must.NotFail(cred.Get(key)).(*types.Document)

assert.Equal(t, must.NotFail(c.Get("iterationCount")), int32(10000))
assert.NotEmpty(t, must.NotFail(c.Get("salt")).(string))
assert.NotEmpty(t, must.NotFail(c.Get("serverKey")).(string))
assert.NotEmpty(t, must.NotFail(c.Get("storedKey")).(string))
}

// assertSCRAMSHA256Credentials checks if the credential is a valid SCRAM-SHA-256 credential.
func assertSCRAMSHA256Credentials(t testtb.TB, key string, cred *types.Document) {
t.Helper()
Expand Down
4 changes: 4 additions & 0 deletions integration/users/usersinfo_test.go
Expand Up @@ -535,6 +535,10 @@ func TestUsersinfo(t *testing.T) {
assertPlainCredentials(t, "PLAIN", must.NotFail(actualUser.Get("credentials")).(*types.Document))
}

if payloadMechanisms.Contains("SCRAM-SHA-1") {
assertSCRAMSHA1Credentials(t, "SCRAM-SHA-1", must.NotFail(actualUser.Get("credentials")).(*types.Document))
}

if payloadMechanisms.Contains("SCRAM-SHA-256") {
assertSCRAMSHA256Credentials(t, "SCRAM-SHA-256", must.NotFail(actualUser.Get("credentials")).(*types.Document))
}
Expand Down
9 changes: 8 additions & 1 deletion internal/handler/msg_createuser.go
Expand Up @@ -110,7 +110,7 @@

common.Ignored(document, h.L, "writeConcern", "authenticationRestrictions", "comment")

defMechanisms := must.NotFail(types.NewArray("SCRAM-SHA-256"))
defMechanisms := must.NotFail(types.NewArray("SCRAM-SHA-1", "SCRAM-SHA-256"))

mechanisms, err := common.GetOptionalParam(document, "mechanisms", defMechanisms)
if err != nil {
Expand Down Expand Up @@ -218,6 +218,13 @@
switch v {
case "PLAIN":
credentials.Set("PLAIN", must.NotFail(password.PlainHash(username)))
case "SCRAM-SHA-1":
hash, err := password.SCRAMSHA1Hash(username, pwd)
if err != nil {
return nil, err

Check warning on line 224 in internal/handler/msg_createuser.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_createuser.go#L224

Added line #L224 was not covered by tests
}

credentials.Set("SCRAM-SHA-1", hash)
case "SCRAM-SHA-256":
hash, err := password.SCRAMSHA256Hash(pwd)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/handler/msg_saslcontinue.go
Expand Up @@ -48,7 +48,7 @@ func (h *Handler) MsgSASLContinue(ctx context.Context, msg *wire.OpMsg) (*wire.O
if err != nil {
return nil, handlererrors.NewCommandErrorMsg(
handlererrors.ErrAuthenticationFailed,
"Authentication failed.",
"Authentication failed",
)
}

Expand Down
43 changes: 30 additions & 13 deletions internal/handler/msg_saslstart.go
Expand Up @@ -77,8 +77,8 @@
)),
)))

case "SCRAM-SHA-256":
response, err := h.saslStartSCRAMSHA256(ctx, document)
case "SCRAM-SHA-1", "SCRAM-SHA-256":
response, err := h.saslStartSCRAM(ctx, mechanism, document)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -155,7 +155,9 @@
}

// scramCredentialLookup looks up an user's credentials in the database.
func (h *Handler) scramCredentialLookup(ctx context.Context, username, dbName string) (*scram.StoredCredentials, error) {
func (h *Handler) scramCredentialLookup(ctx context.Context, username, dbName, mechanism string) (
*scram.StoredCredentials, error,
) {
adminDB, err := h.b.Database("admin")
if err != nil {
return nil, lazyerrors.Error(err)
Expand Down Expand Up @@ -203,15 +205,19 @@
if matches {
credentials := must.NotFail(v.Get("credentials")).(*types.Document)

if !credentials.Has("SCRAM-SHA-256") {
if !credentials.Has(mechanism) {
return nil, handlererrors.NewCommandErrorMsgWithArgument(
handlererrors.ErrMechanismUnavailable,
"User has no SCRAM-SHA-256 based authentication credentials registered",
"SCRAM-SHA-256",
fmt.Sprintf(
"Unable to use %s based authentication for user without any %s credentials registered",
mechanism,
mechanism,
),
mechanism,
)
}

cred := must.NotFail(credentials.Get("SCRAM-SHA-256")).(*types.Document)
cred := must.NotFail(credentials.Get(mechanism)).(*types.Document)

salt := must.NotFail(base64.StdEncoding.DecodeString(must.NotFail(cred.Get("salt")).(string)))
storedKey := must.NotFail(base64.StdEncoding.DecodeString(must.NotFail(cred.Get("storedKey")).(string)))
Expand All @@ -234,9 +240,9 @@
)
}

// saslStartSCRAMSHA256 extracts the initial challenge and attempts to move the
// saslStartSCRAM extracts the initial challenge and attempts to move the
// authentication conversation forward returning a challenge response.
func (h *Handler) saslStartSCRAMSHA256(ctx context.Context, doc *types.Document) (string, error) {
func (h *Handler) saslStartSCRAM(ctx context.Context, mechanism string, doc *types.Document) (string, error) {
var payload []byte

// most drivers follow spec and send payload as a binary
Expand All @@ -252,8 +258,19 @@
return "", err
}

scramServer, err := scram.SHA256.NewServer(func(username string) (scram.StoredCredentials, error) {
cred, lookupErr := h.scramCredentialLookup(ctx, username, dbName)
var f scram.HashGeneratorFcn

switch mechanism {
case "SCRAM-SHA-1":
f = scram.SHA1
case "SCRAM-SHA-256":
f = scram.SHA256
default:
panic("unsupported SCRAM mechanism")

Check warning on line 269 in internal/handler/msg_saslstart.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_saslstart.go#L268-L269

Added lines #L268 - L269 were not covered by tests
}

scramServer, err := f.NewServer(func(username string) (scram.StoredCredentials, error) {
cred, lookupErr := h.scramCredentialLookup(ctx, username, dbName, mechanism)
if lookupErr != nil {
return scram.StoredCredentials{}, lookupErr
}
Expand All @@ -266,12 +283,12 @@

conv := scramServer.NewConversation()

resp, err := conv.Step(string(payload))
response, err := conv.Step(string(payload))
if err != nil {
return "", err
}

conninfo.Get(ctx).SetConv(conv)

return resp, nil
return response, nil
}
2 changes: 1 addition & 1 deletion internal/handler/msg_updateuser.go
Expand Up @@ -89,7 +89,7 @@ func (h *Handler) MsgUpdateUser(ctx context.Context, msg *wire.OpMsg) (*wire.OpM

common.Ignored(document, h.L, "writeConcern", "authenticationRestrictions", "comment")

defMechanisms := must.NotFail(types.NewArray("SCRAM-SHA-256"))
defMechanisms := must.NotFail(types.NewArray("SCRAM-SHA-1", "SCRAM-SHA-256"))

mechanisms, err := common.GetOptionalParam(document, "mechanisms", defMechanisms)
if err != nil {
Expand Down
65 changes: 65 additions & 0 deletions internal/util/password/scram.go
@@ -0,0 +1,65 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package password

import (
"crypto/hmac"
"encoding/base64"
"hash"

"github.com/FerretDB/FerretDB/internal/types"
"github.com/FerretDB/FerretDB/internal/util/lazyerrors"
)

// Computes the HMAC of the given data using the given key.
func computeHMAC(h func() hash.Hash, key, data []byte) []byte {
mac := hmac.New(h, key)
mac.Write(data)

return mac.Sum(nil)
}

// Computes the hash of the given data.
func computeHash(h func() hash.Hash, b []byte) []byte {
dh := h()
dh.Write(b)

return dh.Sum(nil)
}

// scramParams represent password parameters for SCRAM authentication.
type scramParams struct {
iterationCount int
saltLen int
}

// scramDoc creates a document with the stored key, iteration count, salt, and server key.
func scramDoc(h func() hash.Hash, saltedPassword, salt []byte, params *scramParams) (*types.Document, error) {
clientKey := computeHMAC(h, saltedPassword, []byte("Client Key"))
serverKey := computeHMAC(h, saltedPassword, []byte("Server Key"))
storedKey := computeHash(h, clientKey)

doc, err := types.NewDocument(
"storedKey", base64.StdEncoding.EncodeToString(storedKey),
"iterationCount", int32(params.iterationCount),
"salt", base64.StdEncoding.EncodeToString(salt),
"serverKey", base64.StdEncoding.EncodeToString(serverKey),
)
if err != nil {
return nil, lazyerrors.Error(err)

Check warning on line 61 in internal/util/password/scram.go

View check run for this annotation

Codecov / codecov/patch

internal/util/password/scram.go#L61

Added line #L61 was not covered by tests
}

return doc, nil
}