Skip to content

Commit

Permalink
Add SCRAM-SHA-1 authentication support (#4078)
Browse files Browse the repository at this point in the history
  • Loading branch information
henvic authored Feb 19, 2024
1 parent 2bf37ef commit 47819e2
Show file tree
Hide file tree
Showing 12 changed files with 423 additions and 86 deletions.
45 changes: 32 additions & 13 deletions integration/users/connection_test.go
Original file line number Diff line number Diff line change
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",
},
}

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func (h *Handler) MsgCreateUser(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 Expand Up @@ -218,6 +218,13 @@ func makeCredentials(mechanisms *types.Array, username, pwd string) (*types.Docu
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
}

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ func (h *Handler) MsgSASLStart(ctx context.Context, msg *wire.OpMsg) (*wire.OpMs
)),
)))

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 @@ func saslStartPlain(doc *types.Document) (string, string, error) {
}

// 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 @@ func (h *Handler) scramCredentialLookup(ctx context.Context, username, dbName st
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 @@ func (h *Handler) scramCredentialLookup(ctx context.Context, username, dbName st
)
}

// 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 @@ func (h *Handler) saslStartSCRAMSHA256(ctx context.Context, doc *types.Document)
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")
}

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 @@ func (h *Handler) saslStartSCRAMSHA256(ctx context.Context, doc *types.Document)

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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)
}

return doc, nil
}
Loading

0 comments on commit 47819e2

Please sign in to comment.