Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions internal/integration_tests/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,35 @@ func TestLogin(t *testing.T) {
assert.Nil(t, res)
})

t.Run("should fail cleanly (not panic) when stored password hash is nil", func(t *testing.T) {
noHashEmail := "no_password_hash_" + uuid.New().String() + "@authorizer.dev"
signupReq3 := &model.SignUpRequest{
Email: &noHashEmail,
Password: password,
ConfirmPassword: password,
}
signupRes, err := ts.GraphQLProvider.SignUp(ctx, signupReq3)
assert.NoError(t, err)
assert.NotNil(t, signupRes)

// Simulate a record whose password hash was never persisted (e.g. the
// pre-fix Couchbase json:"-" bug) by nulling it out directly.
user, err := ts.StorageProvider.GetUserByEmail(ctx, noHashEmail)
assert.NoError(t, err)
user.Password = nil
_, err = ts.StorageProvider.UpdateUser(ctx, user)
assert.NoError(t, err)

loginReq := &model.LoginRequest{
Email: &noHashEmail,
Password: password,
}
res, err := ts.GraphQLProvider.Login(ctx, loginReq)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid credentials")
assert.Nil(t, res)
})

t.Run("mobile login", func(t *testing.T) {
mobile := fmt.Sprintf("%d", time.Now().Add(10*time.Second).Unix())
signUpReq := &model.SignUpRequest{
Expand Down
10 changes: 9 additions & 1 deletion internal/service/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,15 @@ func (p *provider) Login(ctx context.Context, meta RequestMetadata, params *mode
}
}
}
err = bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(params.Password))
if user.Password == nil {
// A basic_auth user with no stored hash (e.g. a pre-fix Couchbase
// record that never persisted one) must fail the same way as a
// wrong password, not nil-pointer-dereference.
loginPerformDummyPasswordCheck(params.Password)
err = bcrypt.ErrMismatchedHashAndPassword
} else {
err = bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(params.Password))
}
if err != nil {
log.Debug().Str("reason", "bad_password").Msg("login failed")
metrics.RecordAuthEvent(metrics.EventLogin, metrics.StatusFailure)
Expand Down
6 changes: 5 additions & 1 deletion internal/storage/db/couchbase/audit_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ func (p *provider) AddAuditLog(ctx context.Context, auditLog *schemas.AuditLog)
insertOpt := gocb.InsertOptions{
Context: ctx,
}
_, err := p.db.Collection(schemas.Collections.AuditLog).Insert(auditLog.ID, auditLog, &insertOpt)
doc, err := structToDocument(auditLog)
if err != nil {
return err
}
_, err = p.db.Collection(schemas.Collections.AuditLog).Insert(auditLog.ID, doc, &insertOpt)
if err != nil {
return err
}
Expand Down
6 changes: 5 additions & 1 deletion internal/storage/db/couchbase/authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ func (p *provider) AddAuthenticator(ctx context.Context, authenticators *schemas
insertOpt := gocb.InsertOptions{
Context: ctx,
}
_, err := p.db.Collection(schemas.Collections.Authenticators).Insert(authenticators.ID, authenticators, &insertOpt)
doc, err := structToDocument(authenticators)
if err != nil {
return nil, err
}
_, err = p.db.Collection(schemas.Collections.Authenticators).Insert(authenticators.ID, doc, &insertOpt)
if err != nil {
return nil, err
}
Expand Down
6 changes: 5 additions & 1 deletion internal/storage/db/couchbase/email_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ func (p *provider) AddEmailTemplate(ctx context.Context, emailTemplate *schemas.
Context: ctx,
}

_, err := p.db.Collection(schemas.Collections.EmailTemplate).Insert(emailTemplate.ID, emailTemplate, &insertOpt)
doc, err := structToDocument(emailTemplate)
if err != nil {
return nil, err
}
_, err = p.db.Collection(schemas.Collections.EmailTemplate).Insert(emailTemplate.ID, doc, &insertOpt)
if err != nil {
return nil, err
}
Expand Down
6 changes: 5 additions & 1 deletion internal/storage/db/couchbase/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ func (p *provider) AddEnv(ctx context.Context, env *schemas.Env) (*schemas.Env,
insertOpt := gocb.InsertOptions{
Context: ctx,
}
_, err := p.db.Collection(schemas.Collections.Env).Insert(env.ID, env, &insertOpt)
doc, err := structToDocument(env)
if err != nil {
return nil, err
}
_, err = p.db.Collection(schemas.Collections.Env).Insert(env.ID, doc, &insertOpt)
if err != nil {
return nil, err
}
Expand Down
6 changes: 5 additions & 1 deletion internal/storage/db/couchbase/otp.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ func (p *provider) UpsertOTP(ctx context.Context, otpParam *schemas.OTP) (*schem
insertOpt := gocb.InsertOptions{
Context: ctx,
}
_, err := p.db.Collection(schemas.Collections.OTP).Insert(otp.ID, otp, &insertOpt)
doc, err := structToDocument(otp)
if err != nil {
return nil, err
}
_, err = p.db.Collection(schemas.Collections.OTP).Insert(otp.ID, doc, &insertOpt)
if err != nil {
return nil, err
}
Expand Down
6 changes: 5 additions & 1 deletion internal/storage/db/couchbase/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ func (p *provider) AddSession(ctx context.Context, session *schemas.Session) err
insertOpt := gocb.InsertOptions{
Context: ctx,
}
_, err := p.db.Collection(schemas.Collections.Session).Insert(session.ID, session, &insertOpt)
doc, err := structToDocument(session)
if err != nil {
return err
}
_, err = p.db.Collection(schemas.Collections.Session).Insert(session.ID, doc, &insertOpt)
if err != nil {
return err
}
Expand Down
18 changes: 15 additions & 3 deletions internal/storage/db/couchbase/session_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ func (p *provider) AddSessionToken(ctx context.Context, token *schemas.SessionTo
if token.UpdatedAt == 0 {
token.UpdatedAt = time.Now().Unix()
}
_, err := p.db.Collection(schemas.Collections.SessionToken).Insert(token.ID, token, &gocb.InsertOptions{Context: ctx})
doc, err := structToDocument(token)
if err != nil {
return err
}
_, err = p.db.Collection(schemas.Collections.SessionToken).Insert(token.ID, doc, &gocb.InsertOptions{Context: ctx})
return err
}

Expand Down Expand Up @@ -180,7 +184,11 @@ func (p *provider) AddMFASession(ctx context.Context, session *schemas.MFASessio
if session.UpdatedAt == 0 {
session.UpdatedAt = time.Now().Unix()
}
_, err := p.db.Collection(schemas.Collections.MFASession).Insert(session.ID, session, &gocb.InsertOptions{Context: ctx})
doc, err := structToDocument(session)
if err != nil {
return err
}
_, err = p.db.Collection(schemas.Collections.MFASession).Insert(session.ID, doc, &gocb.InsertOptions{Context: ctx})
return err
}

Expand Down Expand Up @@ -328,7 +336,11 @@ func (p *provider) AddOAuthState(ctx context.Context, state *schemas.OAuthState)
_, _ = p.db.Collection(schemas.Collections.OAuthState).Remove(row.ID, &gocb.RemoveOptions{Context: ctx})
}
}
_, err := p.db.Collection(schemas.Collections.OAuthState).Insert(state.ID, state, &gocb.InsertOptions{Context: ctx})
doc, err := structToDocument(state)
if err != nil {
return err
}
_, err = p.db.Collection(schemas.Collections.OAuthState).Insert(state.ID, doc, &gocb.InsertOptions{Context: ctx})
return err
}

Expand Down
99 changes: 99 additions & 0 deletions internal/storage/db/couchbase/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,112 @@ package couchbase

import (
"context"
"encoding/json"
"fmt"
"reflect"
"strings"

"github.com/couchbase/gocb/v2"
)

// structToDocument converts a schema struct into a map for Couchbase persistence.
//
// Couchbase is the only storage provider that (de)serializes whole schema structs
// through encoding/json (the gocb default JSON transcoder). encoding/json honors
// `json:"-"`, which is set on secret fields such as User.Password purely to keep them
// out of API/GraphQL JSON responses. As a side effect those fields were silently
// dropped from the persisted document. structToDocument reproduces the exact JSON
// document shape for every normally-tagged field, then re-adds any `json:"-"` field
// under its `bson` tag key so it is actually persisted. All Insert/Upsert call sites
// pass their document through this helper.
//
// ponytail: paired with decodeDocument. If a schema gains a new `json:"-"` field, its
// read path MUST decode via decodeDocument or the field will load empty. Today only
// User.Password is affected, and only User's read methods use decodeDocument.
func structToDocument(v interface{}) (map[string]interface{}, error) {
data, err := json.Marshal(v)
if err != nil {
return nil, err
}
doc := map[string]interface{}{}
if err := json.Unmarshal(data, &doc); err != nil {
return nil, err
}
rv := reflect.Indirect(reflect.ValueOf(v))
if rv.Kind() != reflect.Struct {
return doc, nil
}
rt := rv.Type()
for i := 0; i < rt.NumField(); i++ {
f := rt.Field(i)
if f.Tag.Get("json") != "-" {
continue
}
key := persistKey(f)
if key == "" {
continue
}
doc[key] = rv.Field(i).Interface()
}
return doc, nil
}

// decodeDocument unmarshals a Couchbase row/document into dest, then populates any
// `json:"-"` field from its `bson` tag key (encoding/json ignores such fields on
// unmarshal too). dest must be a non-nil pointer to a struct. See structToDocument.
func decodeDocument(data []byte, dest interface{}) error {
if err := json.Unmarshal(data, dest); err != nil {
return err
}
rv := reflect.Indirect(reflect.ValueOf(dest))
if rv.Kind() != reflect.Struct {
return nil
}
rt := rv.Type()
var rawFields map[string]json.RawMessage
for i := 0; i < rt.NumField(); i++ {
f := rt.Field(i)
if f.Tag.Get("json") != "-" {
continue
}
key := persistKey(f)
if key == "" {
continue
}
fv := rv.Field(i)
if !fv.CanSet() {
continue
}
if rawFields == nil {
rawFields = map[string]json.RawMessage{}
if err := json.Unmarshal(data, &rawFields); err != nil {
return err
}
}
raw, ok := rawFields[key]
if !ok {
continue
}
nv := reflect.New(f.Type)
if err := json.Unmarshal(raw, nv.Interface()); err != nil {
return err
}
fv.Set(nv.Elem())
}
return nil
}

// persistKey returns the document key a `json:"-"` field is stored under. Couchbase
// document keys mirror the bson/json tag names, so it uses the field's `bson` tag,
// falling back to the lowercased field name.
func persistKey(f reflect.StructField) string {
name := strings.Split(f.Tag.Get("bson"), ",")[0]
if name == "" || name == "-" {
return strings.ToLower(f.Name)
}
return name
}

// GetSetFields to get set fields
func GetSetFields(webhookMap map[string]interface{}) (string, map[string]interface{}) {
params := make(map[string]interface{}, 1)
Expand Down
Loading
Loading