Add password authentication using Argon2 (#227)
* Add argon2 credentials * Verify oplog in tests * Refactor: rename field * Update internal/auth/password/repository_account.go Co-authored-by: Todd Knight <T.Alan.Knight@gmail.com> * Add inline comments to SQL query to doc mapping to structure * Refactor: move authentication query to query.go file * Replace immutable_create_time_func with immutable_columns Co-authored-by: Todd Knight <T.Alan.Knight@gmail.com>
This commit is contained in:
1 parent
87492816cb
commit
6c679a9939
18 files changed
+930
-18
No files matched your search
@@ -14,9 +14,9 @@ type Account struct {
|
||||
*store.Account
|
||||
tableName string
|
||||
|
||||
// CredentialID is included when Authenticate or ChangePassword is
|
||||
// called. A new CredentialID is generated when a password is changed.
|
||||
CredentialID string `gorm:"-"`
|
||||
// CredentialId is included when Authenticate or ChangePassword is
|
||||
// called. A new CredentialId is generated when a password is changed.
|
||||
CredentialId string `gorm:"-"`
|
||||
}
|
||||
|
||||
func allocAccount() Account {
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping"
|
||||
"github.com/hashicorp/go-kms-wrapping/structwrapping"
|
||||
"github.com/hashicorp/watchtower/internal/auth/password/store"
|
||||
"github.com/hashicorp/watchtower/internal/db"
|
||||
"github.com/hashicorp/watchtower/internal/oplog"
|
||||
"golang.org/x/crypto/argon2"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
@@ -100,3 +107,90 @@ func (c *Argon2Configuration) whereDup() (string, []interface{}) {
|
||||
|
||||
return strings.Join(where, " and "), args
|
||||
}
|
||||
|
||||
// A Argon2Credential contains a key derived from a password and the salt
|
||||
// used in the key derivation. It is owned by an Account.
|
||||
type Argon2Credential struct {
|
||||
*store.Argon2Credential
|
||||
tableName string
|
||||
}
|
||||
|
||||
func newArgon2Credential(accountId string, password string, conf *Argon2Configuration) (*Argon2Credential, error) {
|
||||
if accountId == "" {
|
||||
return nil, fmt.Errorf("new: password argon2 credential: no accountId: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if password == "" {
|
||||
return nil, fmt.Errorf("new: password argon2 credential: no password: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if conf == nil {
|
||||
return nil, fmt.Errorf("new: password argon2 credential: no argon2 configuration: %w", db.ErrNilParameter)
|
||||
}
|
||||
|
||||
id, err := newArgon2CredentialId()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("new: password argon2 credential: %w", err)
|
||||
}
|
||||
|
||||
c := &Argon2Credential{
|
||||
Argon2Credential: &store.Argon2Credential{
|
||||
PrivateId: id,
|
||||
PasswordAccountId: accountId,
|
||||
PasswordConfId: conf.PrivateId,
|
||||
},
|
||||
}
|
||||
|
||||
salt := make([]byte, conf.SaltLength)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return nil, fmt.Errorf("new: password argon2 credential: %w", err)
|
||||
}
|
||||
c.Salt = salt
|
||||
c.DerivedKey = argon2.IDKey([]byte(password), c.Salt, conf.Iterations, conf.Memory, uint8(conf.Threads), conf.KeyLength)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Argon2Credential) clone() *Argon2Credential {
|
||||
cp := proto.Clone(c.Argon2Credential)
|
||||
return &Argon2Credential{
|
||||
Argon2Credential: cp.(*store.Argon2Credential),
|
||||
}
|
||||
}
|
||||
|
||||
// TableName returns the table name.
|
||||
func (c *Argon2Credential) TableName() string {
|
||||
if c != nil && c.tableName != "" {
|
||||
return c.tableName
|
||||
}
|
||||
return "auth_password_argon2_cred"
|
||||
}
|
||||
|
||||
// SetTableName sets the table name.
|
||||
func (c *Argon2Credential) SetTableName(n string) {
|
||||
c.tableName = n
|
||||
}
|
||||
|
||||
func (c *Argon2Credential) encrypt(ctx context.Context, cipher wrapping.Wrapper) error {
|
||||
if err := structwrapping.WrapStruct(ctx, cipher, c.Argon2Credential, nil); err != nil {
|
||||
return fmt.Errorf("error encrypting argon2 credential: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Argon2Credential) decrypt(ctx context.Context, cipher wrapping.Wrapper) error {
|
||||
if err := structwrapping.UnwrapStruct(ctx, cipher, c.Argon2Credential, nil); err != nil {
|
||||
return fmt.Errorf("error decrypting argon2 credential: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Argon2Credential) oplog(op oplog.OpType) oplog.Metadata {
|
||||
metadata := oplog.Metadata{
|
||||
"resource-public-id": []string{c.PrivateId},
|
||||
"resource-type": []string{"argon2 credential"},
|
||||
"op-type": []string{op.String()},
|
||||
"password-account-id": []string{c.PasswordAccountId},
|
||||
}
|
||||
if c.PasswordMethodId != "" {
|
||||
metadata["password-method-id"] = []string{c.PasswordMethodId}
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/hashicorp/watchtower/internal/auth/password/store"
|
||||
"github.com/hashicorp/watchtower/internal/db"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -268,3 +269,157 @@ func TestArgon2Configuration_Validate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testArgon2Confs(t *testing.T, conn *gorm.DB, authMethodId string, count int) []*Argon2Configuration {
|
||||
t.Helper()
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
rw := db.New(conn)
|
||||
var confs []*Argon2Configuration
|
||||
err := rw.SearchWhere(context.Background(), &confs, "password_method_id = ?", []interface{}{authMethodId})
|
||||
require.NoError(err)
|
||||
assert.Equal(1, len(confs))
|
||||
base := confs[0]
|
||||
for i := 0; i < count; i++ {
|
||||
conf := NewArgon2Configuration()
|
||||
require.NotNil(conf)
|
||||
conf.PasswordMethodId = authMethodId
|
||||
conf.PrivateId, err = newArgon2ConfigurationId()
|
||||
require.NoError(err)
|
||||
|
||||
conf.Iterations = base.Iterations + uint32(i+1)
|
||||
conf.Threads = base.Threads + uint32(i+1)
|
||||
err = rw.Create(context.Background(), conf)
|
||||
require.NoError(err)
|
||||
confs = append(confs, conf)
|
||||
}
|
||||
return confs
|
||||
}
|
||||
|
||||
func TestArgon2Credential_New(t *testing.T) {
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
|
||||
auts := testAuthMethods(t, conn, 1)
|
||||
aut := auts[0]
|
||||
accts := testAccounts(t, conn, aut.ScopeId, aut.PublicId, 5)
|
||||
confs := testArgon2Confs(t, conn, accts[0].AuthMethodId, 1)
|
||||
|
||||
type args struct {
|
||||
accountId string
|
||||
password string
|
||||
conf *Argon2Configuration
|
||||
}
|
||||
var tests = []struct {
|
||||
name string
|
||||
args args
|
||||
want *Argon2Credential
|
||||
wantIsErr error
|
||||
}{
|
||||
{
|
||||
name: "blank-accountId",
|
||||
args: args{
|
||||
accountId: "",
|
||||
password: "foobarcity",
|
||||
conf: confs[0],
|
||||
},
|
||||
want: nil,
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "blank-password",
|
||||
args: args{
|
||||
accountId: accts[0].PublicId,
|
||||
password: "",
|
||||
conf: confs[0],
|
||||
},
|
||||
want: nil,
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "nil-configuration",
|
||||
args: args{
|
||||
accountId: accts[0].PublicId,
|
||||
password: "foobarcity",
|
||||
conf: nil,
|
||||
},
|
||||
want: nil,
|
||||
wantIsErr: db.ErrNilParameter,
|
||||
},
|
||||
{
|
||||
name: "valid-password",
|
||||
args: args{
|
||||
accountId: accts[0].PublicId,
|
||||
password: "foobarcity",
|
||||
conf: confs[0],
|
||||
},
|
||||
want: &Argon2Credential{
|
||||
Argon2Credential: &store.Argon2Credential{
|
||||
PasswordAccountId: accts[0].PublicId,
|
||||
PasswordConfId: confs[0].PrivateId,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "traditional-chinese-characters-password",
|
||||
args: args{
|
||||
accountId: accts[1].PublicId,
|
||||
password: "漢字𫝆𫝑𫝜𫝓𫝶𫝼𫞉𫞔𫞩𫞬",
|
||||
conf: confs[0],
|
||||
},
|
||||
want: &Argon2Credential{
|
||||
Argon2Credential: &store.Argon2Credential{
|
||||
PasswordAccountId: accts[1].PublicId,
|
||||
PasswordConfId: confs[0].PrivateId,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "emoji-password",
|
||||
args: args{
|
||||
accountId: accts[2].PublicId,
|
||||
password: "😃 😀 😅 😘🤔 😑🤢🤠😰",
|
||||
conf: confs[0],
|
||||
},
|
||||
want: &Argon2Credential{
|
||||
Argon2Credential: &store.Argon2Credential{
|
||||
PasswordAccountId: accts[2].PublicId,
|
||||
PasswordConfId: confs[0].PrivateId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := newArgon2Credential(tt.args.accountId, tt.args.password, tt.args.conf)
|
||||
if tt.wantIsErr != nil {
|
||||
assert.Truef(errors.Is(err, tt.wantIsErr), "want err: %q got: %q", tt.wantIsErr, err)
|
||||
assert.Nil(got)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
require.NotNil(got)
|
||||
|
||||
got.encrypt(context.Background(), wrapper)
|
||||
err = rw.Create(context.Background(), got)
|
||||
assert.NoError(err)
|
||||
|
||||
if tt.want == nil {
|
||||
return
|
||||
}
|
||||
|
||||
gotCred := &Argon2Credential{
|
||||
Argon2Credential: &store.Argon2Credential{
|
||||
PrivateId: got.PrivateId,
|
||||
},
|
||||
}
|
||||
err = rw.LookupById(context.Background(), gotCred)
|
||||
require.NoError(err)
|
||||
assert.Equal(tt.want.PasswordAccountId, gotCred.PasswordAccountId)
|
||||
assert.Equal(tt.want.PasswordConfId, gotCred.PasswordConfId)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,10 @@ package password
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrTooShort results from attempting to set a password which is to
|
||||
// short.
|
||||
ErrTooShort = errors.New("password to short")
|
||||
|
||||
// ErrUnsupportedConfiguration results from attempting to perform an
|
||||
// operation that sets a password configuration to an unsupported type.
|
||||
ErrUnsupportedConfiguration = errors.New("unsupported configuration")
|
||||
|
||||
@@ -18,6 +18,8 @@ type options struct {
|
||||
withDescription string
|
||||
withLimit int
|
||||
withConfig Configuration
|
||||
password string
|
||||
withPassword bool
|
||||
}
|
||||
|
||||
func getDefaultOptions() options {
|
||||
@@ -49,6 +51,14 @@ func WithLimit(l int) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithPassword provides an optional password.
|
||||
func WithPassword(password string) Option {
|
||||
return func(o *options) {
|
||||
o.password = password
|
||||
o.withPassword = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithConfiguration provides an optional configuration.
|
||||
func WithConfiguration(config Configuration) Option {
|
||||
return func(o *options) {
|
||||
|
||||
@@ -27,6 +27,13 @@ func Test_GetOpts(t *testing.T) {
|
||||
testOpts.withLimit = 5
|
||||
assert.Equal(t, opts, testOpts)
|
||||
})
|
||||
t.Run("WithPassword", func(t *testing.T) {
|
||||
opts := getOpts(WithPassword("test password"))
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.password = "test password"
|
||||
testOpts.withPassword = true
|
||||
assert.Equal(t, opts, testOpts)
|
||||
})
|
||||
t.Run("WithConfiguration", func(t *testing.T) {
|
||||
conf := NewArgon2Configuration()
|
||||
conf.KeyLength = conf.KeyLength * 2
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
// Prefixes for private ids for types in the password package.
|
||||
const (
|
||||
argon2ConfigurationPrefix = "arg2conf"
|
||||
argon2CredentialPrefix = "arg2cred"
|
||||
)
|
||||
|
||||
func newArgon2ConfigurationId() (string, error) {
|
||||
@@ -18,3 +19,11 @@ func newArgon2ConfigurationId() (string, error) {
|
||||
}
|
||||
return id, err
|
||||
}
|
||||
|
||||
func newArgon2CredentialId() (string, error) {
|
||||
id, err := db.NewPrivateId(argon2CredentialPrefix)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("new password argon2 configuration id: %w", err)
|
||||
}
|
||||
return id, err
|
||||
}
|
||||
@@ -14,4 +14,9 @@ func Test_PrivateIds(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.True(t, strings.HasPrefix(id, argon2ConfigurationPrefix+"_"))
|
||||
})
|
||||
t.Run("argon2Cred", func(t *testing.T) {
|
||||
id, err := newArgon2CredentialId()
|
||||
require.NoError(t, err)
|
||||
assert.True(t, strings.HasPrefix(id, argon2CredentialPrefix+"_"))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package password
|
||||
|
||||
const (
|
||||
authenticateQuery = `
|
||||
select acct.name, -- Account.Name
|
||||
acct.description, -- Account.Description
|
||||
acct.user_name, -- Account.UserName
|
||||
acct.public_id, -- Account.PublicId
|
||||
acct.auth_method_id, -- Account.AuthMethodId
|
||||
acct.scope_id, -- Account.ScopeId
|
||||
acct.create_time, -- Account.CreateTime
|
||||
acct.update_time, -- Account.UpdateTime
|
||||
cred.private_id as credential_id, -- Account.CredentialId
|
||||
cred.password_conf_id, -- Argon2Credential.PasswordConfId
|
||||
cred.salt, -- Argon2Credential.CtSalt/Salt
|
||||
cred.derived_key, -- Argon2Credential.DerivedKey
|
||||
conf.key_length, -- Argon2Configuration.KeyLength
|
||||
conf.iterations, -- Argon2Configuration.Iterations
|
||||
conf.memory, -- Argon2Configuration.Memory
|
||||
conf.threads -- Argon2Configuration.Threads
|
||||
from auth_password_argon2_cred cred,
|
||||
auth_password_argon2_conf conf,
|
||||
auth_password_account acct
|
||||
where acct.auth_method_id = $1
|
||||
and acct.user_name = $2
|
||||
and cred.password_conf_id = conf.private_id
|
||||
and cred.password_account_id = acct.public_id;
|
||||
`
|
||||
)
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
// contain a valid AuthMethodId. a must not contain a PublicId. The PublicId is
|
||||
// generated and assigned by this method.
|
||||
//
|
||||
// WithPassword is the only valid option. All other options are ignored.
|
||||
//
|
||||
// Both a.Name and a.Description are optional. If a.Name is set, it must be
|
||||
// unique within a.AuthMethodId.
|
||||
func (r *Repository) CreateAccount(ctx context.Context, a *Account, opt ...Option) (*Account, error) {
|
||||
@@ -34,6 +36,15 @@ func (r *Repository) CreateAccount(ctx context.Context, a *Account, opt ...Optio
|
||||
return nil, fmt.Errorf("create: password account: invalid user name: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
cc, err := r.currentConfig(ctx, a.AuthMethodId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create: password account: retrieve current configuration: %w", err)
|
||||
}
|
||||
|
||||
if cc.MinUserNameLength > len(a.UserName) {
|
||||
return nil, fmt.Errorf("create: password account: user name %q: %w", a.UserName, ErrTooShort)
|
||||
}
|
||||
|
||||
a = a.clone()
|
||||
id, err := newAccountId()
|
||||
if err != nil {
|
||||
@@ -41,6 +52,19 @@ func (r *Repository) CreateAccount(ctx context.Context, a *Account, opt ...Optio
|
||||
}
|
||||
a.PublicId = id
|
||||
|
||||
opts := getOpts(opt...)
|
||||
|
||||
var cred *Argon2Credential
|
||||
if opts.withPassword {
|
||||
if cc.MinPasswordLength > len(opts.password) {
|
||||
return nil, fmt.Errorf("create: password account: password: %w", ErrTooShort)
|
||||
}
|
||||
if cred, err = newArgon2Credential(id, opts.password, cc.argon2()); err != nil {
|
||||
return nil, fmt.Errorf("create: password account: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var newCred *Argon2Credential
|
||||
var newAccount *Account
|
||||
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
|
||||
func(_ db.Reader, w db.Writer) error {
|
||||
@@ -49,6 +73,15 @@ func (r *Repository) CreateAccount(ctx context.Context, a *Account, opt ...Optio
|
||||
return err
|
||||
}
|
||||
|
||||
if cred != nil {
|
||||
newCred = cred.clone()
|
||||
if err := newCred.encrypt(ctx, r.wrapper); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.Create(ctx, newCred, db.WithOplog(r.wrapper, cred.oplog(oplog.OpType_OP_TYPE_CREATE))); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
@@ -5,9 +5,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/watchtower/internal/auth/password/store"
|
||||
"github.com/hashicorp/watchtower/internal/db"
|
||||
"github.com/hashicorp/watchtower/internal/oplog"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -143,6 +145,29 @@ func TestRepository_CreateAccount(t *testing.T) {
|
||||
},
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "invalid-username-to-short",
|
||||
in: &Account{
|
||||
Account: &store.Account{
|
||||
AuthMethodId: authMethod.PublicId,
|
||||
UserName: "kaz",
|
||||
},
|
||||
},
|
||||
wantIsErr: ErrTooShort,
|
||||
},
|
||||
{
|
||||
name: "invalid-password-to-short",
|
||||
in: &Account{
|
||||
Account: &store.Account{
|
||||
AuthMethodId: authMethod.PublicId,
|
||||
UserName: "kazmierczak123",
|
||||
},
|
||||
},
|
||||
opts: []Option{
|
||||
WithPassword("a"),
|
||||
},
|
||||
wantIsErr: ErrTooShort,
|
||||
},
|
||||
{
|
||||
name: "valid-no-options",
|
||||
in: &Account{
|
||||
@@ -192,6 +217,24 @@ func TestRepository_CreateAccount(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid-with-password",
|
||||
in: &Account{
|
||||
Account: &store.Account{
|
||||
AuthMethodId: authMethod.PublicId,
|
||||
UserName: "kazmierczak3",
|
||||
},
|
||||
},
|
||||
opts: []Option{
|
||||
WithPassword("1234567890"),
|
||||
},
|
||||
want: &Account{
|
||||
Account: &store.Account{
|
||||
AuthMethodId: authMethod.PublicId,
|
||||
UserName: "kazmierczak3",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -215,6 +258,15 @@ func TestRepository_CreateAccount(t *testing.T) {
|
||||
assert.Equal(tt.want.Name, got.Name)
|
||||
assert.Equal(tt.want.Description, got.Description)
|
||||
assert.Equal(got.CreateTime, got.UpdateTime)
|
||||
|
||||
assert.NoError(db.TestVerifyOplog(t, rw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)))
|
||||
|
||||
opts := getOpts(tt.opts...)
|
||||
if opts.withPassword {
|
||||
authAcct, err := repo.Authenticate(context.Background(), tt.in.AuthMethodId, tt.in.UserName, opts.password)
|
||||
require.NoError(err)
|
||||
assert.NoError(db.TestVerifyOplog(t, rw, authAcct.CredentialId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/watchtower/internal/db"
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
// Authenticate authenticates userName and password match for userName in
|
||||
// authMethodId. The account for the userName is returned if authentication
|
||||
// is successful. Returns nil if authentication fails.
|
||||
//
|
||||
// The CredentialId in the returned account represents a user's current
|
||||
// password. A new CredentialId is generated when a user's password is
|
||||
// changed and the old one is deleted.
|
||||
//
|
||||
// Authenticate will update the stored values for password to the current
|
||||
// password settings for authMethodId if authentication is successful and
|
||||
// the stored values are not using the current password settings.
|
||||
func (r *Repository) Authenticate(ctx context.Context, authMethodId string, userName string, password string) (*Account, error) {
|
||||
if authMethodId == "" {
|
||||
return nil, fmt.Errorf("password authenticate: no authMethodId: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if userName == "" {
|
||||
return nil, fmt.Errorf("password authenticate: no userName: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if password == "" {
|
||||
return nil, fmt.Errorf("password authenticate: no password: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
type AuthAccount struct {
|
||||
*Account
|
||||
*Argon2Credential
|
||||
*Argon2Configuration
|
||||
}
|
||||
|
||||
var accts []AuthAccount
|
||||
|
||||
tx, err := r.reader.DB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := tx.Query(authenticateQuery, authMethodId, userName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var aa AuthAccount
|
||||
if err := r.reader.ScanRows(rows, &aa); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accts = append(accts, aa)
|
||||
}
|
||||
|
||||
var acct AuthAccount
|
||||
switch {
|
||||
case len(accts) == 0:
|
||||
return nil, nil
|
||||
case len(accts) > 1:
|
||||
// this should never happen
|
||||
return nil, fmt.Errorf("authenticate: multiple accounts returned for user name")
|
||||
default:
|
||||
acct = accts[0]
|
||||
}
|
||||
|
||||
if err := acct.decrypt(ctx, r.wrapper); err != nil {
|
||||
return nil, fmt.Errorf("authenticate: credential: cannot decrypt value: %w", err)
|
||||
}
|
||||
|
||||
inputKey := argon2.IDKey([]byte(password), acct.Salt, acct.Iterations, acct.Memory, uint8(acct.Threads), acct.KeyLength)
|
||||
if subtle.ConstantTimeCompare(inputKey, acct.DerivedKey) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// TODO(mgaffney) 07/2020: update stored credential if acct config is
|
||||
// not the current config for authMethodId
|
||||
return acct.Account, nil
|
||||
}
|
||||
|
||||
// ChangePassword updates the password for userName in authMethodId to new
|
||||
// if old equals the stored password. The account for the userName is
|
||||
// returned with a new CredentialId if password is successfully changed.
|
||||
//
|
||||
// Returns nil if old does not match the stored password for userName.
|
||||
func (r *Repository) ChangePassword(ctx context.Context, authMethodId string, userName string, old, new string) (*Account, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/watchtower/internal/auth/password/store"
|
||||
"github.com/hashicorp/watchtower/internal/db"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRepository_Authenticate(t *testing.T) {
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
|
||||
authMethods := testAuthMethods(t, conn, 1)
|
||||
authMethod := authMethods[0]
|
||||
|
||||
inAcct := &Account{
|
||||
Account: &store.Account{
|
||||
AuthMethodId: authMethod.PublicId,
|
||||
UserName: "kazmierczak",
|
||||
},
|
||||
}
|
||||
passwd := "12345678"
|
||||
|
||||
repo, err := NewRepository(rw, rw, wrapper)
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, repo)
|
||||
outAcct, err := repo.CreateAccount(context.Background(), inAcct, WithPassword(passwd))
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, outAcct)
|
||||
|
||||
type args struct {
|
||||
authMethodId string
|
||||
userName string
|
||||
password string
|
||||
}
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
args args
|
||||
want *Account
|
||||
wantIsErr error
|
||||
}{
|
||||
{
|
||||
name: "invalid-no-authMethodId",
|
||||
args: args{
|
||||
authMethodId: "",
|
||||
userName: inAcct.UserName,
|
||||
password: passwd,
|
||||
},
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "invalid-no-userName",
|
||||
args: args{
|
||||
authMethodId: inAcct.AuthMethodId,
|
||||
userName: "",
|
||||
password: passwd,
|
||||
},
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "invalid-no-password",
|
||||
args: args{
|
||||
authMethodId: inAcct.AuthMethodId,
|
||||
userName: inAcct.UserName,
|
||||
password: "",
|
||||
},
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "valid-authenticate",
|
||||
args: args{
|
||||
authMethodId: inAcct.AuthMethodId,
|
||||
userName: inAcct.UserName,
|
||||
password: passwd,
|
||||
},
|
||||
want: outAcct,
|
||||
},
|
||||
{
|
||||
name: "wrong-password",
|
||||
args: args{
|
||||
authMethodId: inAcct.AuthMethodId,
|
||||
userName: inAcct.UserName,
|
||||
password: "foobar",
|
||||
},
|
||||
want: nil,
|
||||
wantIsErr: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
authAcct, err := repo.Authenticate(context.Background(), tt.args.authMethodId, tt.args.userName, tt.args.password)
|
||||
if tt.wantIsErr != nil {
|
||||
assert.Truef(errors.Is(err, tt.wantIsErr), "want err: %q got: %q", tt.wantIsErr, err)
|
||||
assert.Nil(authAcct, "returned account")
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
if tt.want == nil {
|
||||
assert.Nil(authAcct)
|
||||
return
|
||||
}
|
||||
require.NotNil(authAcct, "returned account")
|
||||
assert.NotEmpty(authAcct.CredentialId, "CredentialId")
|
||||
assert.Equal(tt.args.authMethodId, authAcct.AuthMethodId, "authMethodId")
|
||||
assert.Equal(tt.args.userName, authAcct.UserName, "UserName")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -158,6 +158,132 @@ func (x *Argon2Configuration) GetKeyLength() uint32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
type Argon2Credential struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// @inject_tag: `gorm:"primary_key"`
|
||||
PrivateId string `protobuf:"bytes,1,opt,name=private_id,json=privateId,proto3" json:"private_id,omitempty" gorm:"primary_key"`
|
||||
// The create_time is set by the database.
|
||||
// @inject_tag: `gorm:"default:current_timestamp"`
|
||||
CreateTime *timestamp.Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"`
|
||||
// The update_time is set by the database.
|
||||
// @inject_tag: `gorm:"default:current_timestamp"`
|
||||
UpdateTime *timestamp.Timestamp `protobuf:"bytes,3,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"`
|
||||
// @inject_tag: `gorm:"not_null"`
|
||||
PasswordAccountId string `protobuf:"bytes,4,opt,name=password_account_id,json=passwordAccountId,proto3" json:"password_account_id,omitempty" gorm:"not_null"`
|
||||
// @inject_tag: `gorm:"not_null"`
|
||||
PasswordConfId string `protobuf:"bytes,5,opt,name=password_conf_id,json=passwordConfId,proto3" json:"password_conf_id,omitempty" gorm:"not_null"`
|
||||
// @inject_tag: `gorm:"not_null"`
|
||||
PasswordMethodId string `protobuf:"bytes,6,opt,name=password_method_id,json=passwordMethodId,proto3" json:"password_method_id,omitempty" gorm:"not_null"`
|
||||
// ct_salt is the encrypted salt which is stored in the database.
|
||||
// @inject_tag: `gorm:"column:salt;not_null" wrapping:"ct,entry_salt"`
|
||||
CtSalt []byte `protobuf:"bytes,7,opt,name=ct_salt,json=ctSalt,proto3" json:"ct_salt,omitempty" gorm:"column:salt;not_null" wrapping:"ct,entry_salt"`
|
||||
// salt is the unencrypted salt which is not stored in the database.
|
||||
// @inject_tag: `gorm:"-" wrapping:"pt,entry_salt"`
|
||||
Salt []byte `protobuf:"bytes,8,opt,name=salt,proto3" json:"salt,omitempty" gorm:"-" wrapping:"pt,entry_salt"`
|
||||
// derived_key is the derived key produced by the Argon2id key
|
||||
// derivation function.
|
||||
// @inject_tag: `gorm:"not_null"`
|
||||
DerivedKey []byte `protobuf:"bytes,9,opt,name=derived_key,json=derivedKey,proto3" json:"derived_key,omitempty" gorm:"not_null"`
|
||||
}
|
||||
|
||||
func (x *Argon2Credential) Reset() {
|
||||
*x = Argon2Credential{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_controller_storage_auth_password_store_v1_argon2_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *Argon2Credential) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Argon2Credential) ProtoMessage() {}
|
||||
|
||||
func (x *Argon2Credential) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_controller_storage_auth_password_store_v1_argon2_proto_msgTypes[1]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Argon2Credential.ProtoReflect.Descriptor instead.
|
||||
func (*Argon2Credential) Descriptor() ([]byte, []int) {
|
||||
return file_controller_storage_auth_password_store_v1_argon2_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *Argon2Credential) GetPrivateId() string {
|
||||
if x != nil {
|
||||
return x.PrivateId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Argon2Credential) GetCreateTime() *timestamp.Timestamp {
|
||||
if x != nil {
|
||||
return x.CreateTime
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Argon2Credential) GetUpdateTime() *timestamp.Timestamp {
|
||||
if x != nil {
|
||||
return x.UpdateTime
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Argon2Credential) GetPasswordAccountId() string {
|
||||
if x != nil {
|
||||
return x.PasswordAccountId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Argon2Credential) GetPasswordConfId() string {
|
||||
if x != nil {
|
||||
return x.PasswordConfId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Argon2Credential) GetPasswordMethodId() string {
|
||||
if x != nil {
|
||||
return x.PasswordMethodId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Argon2Credential) GetCtSalt() []byte {
|
||||
if x != nil {
|
||||
return x.CtSalt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Argon2Credential) GetSalt() []byte {
|
||||
if x != nil {
|
||||
return x.Salt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Argon2Credential) GetDerivedKey() []byte {
|
||||
if x != nil {
|
||||
return x.DerivedKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_controller_storage_auth_password_store_v1_argon2_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_controller_storage_auth_password_store_v1_argon2_proto_rawDesc = []byte{
|
||||
@@ -190,12 +316,38 @@ var file_controller_storage_auth_password_store_v1_argon2_proto_rawDesc = []byte
|
||||
0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x73,
|
||||
0x61, 0x6c, 0x74, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x1d, 0x0a, 0x0a, 0x6b, 0x65, 0x79,
|
||||
0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x6b,
|
||||
0x65, 0x79, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x42, 0x44, 0x5a, 0x42, 0x67, 0x69, 0x74, 0x68,
|
||||
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70,
|
||||
0x2f, 0x77, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72, 0x2f, 0x69, 0x6e, 0x74, 0x65,
|
||||
0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f,
|
||||
0x72, 0x64, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x65, 0x79, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x22, 0xa1, 0x03, 0x0a, 0x10, 0x41, 0x72, 0x67,
|
||||
0x6f, 0x6e, 0x32, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1d, 0x0a,
|
||||
0x0a, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x09, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b,
|
||||
0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||
0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73,
|
||||
0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
|
||||
0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63,
|
||||
0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64,
|
||||
0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a,
|
||||
0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72,
|
||||
0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31,
|
||||
0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61,
|
||||
0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f,
|
||||
0x72, 0x64, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x11, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x41, 0x63, 0x63,
|
||||
0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f,
|
||||
0x72, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x0e, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x49, 0x64,
|
||||
0x12, 0x2c, 0x0a, 0x12, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x6d, 0x65, 0x74,
|
||||
0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x61,
|
||||
0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x17,
|
||||
0x0a, 0x07, 0x63, 0x74, 0x5f, 0x73, 0x61, 0x6c, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52,
|
||||
0x06, 0x63, 0x74, 0x53, 0x61, 0x6c, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x61, 0x6c, 0x74, 0x18,
|
||||
0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x73, 0x61, 0x6c, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x64,
|
||||
0x65, 0x72, 0x69, 0x76, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c,
|
||||
0x52, 0x0a, 0x64, 0x65, 0x72, 0x69, 0x76, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x42, 0x44, 0x5a, 0x42,
|
||||
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69,
|
||||
0x63, 0x6f, 0x72, 0x70, 0x2f, 0x77, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72, 0x2f,
|
||||
0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x70, 0x61,
|
||||
0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f,
|
||||
0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -210,18 +362,21 @@ func file_controller_storage_auth_password_store_v1_argon2_proto_rawDescGZIP() [
|
||||
return file_controller_storage_auth_password_store_v1_argon2_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_controller_storage_auth_password_store_v1_argon2_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_controller_storage_auth_password_store_v1_argon2_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_controller_storage_auth_password_store_v1_argon2_proto_goTypes = []interface{}{
|
||||
(*Argon2Configuration)(nil), // 0: controller.storage.auth.password.store.v1.Argon2Configuration
|
||||
(*timestamp.Timestamp)(nil), // 1: controller.storage.timestamp.v1.Timestamp
|
||||
(*Argon2Credential)(nil), // 1: controller.storage.auth.password.store.v1.Argon2Credential
|
||||
(*timestamp.Timestamp)(nil), // 2: controller.storage.timestamp.v1.Timestamp
|
||||
}
|
||||
var file_controller_storage_auth_password_store_v1_argon2_proto_depIdxs = []int32{
|
||||
1, // 0: controller.storage.auth.password.store.v1.Argon2Configuration.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
2, // 0: controller.storage.auth.password.store.v1.Argon2Configuration.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
|
||||
2, // 1: controller.storage.auth.password.store.v1.Argon2Credential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
|
||||
2, // 2: controller.storage.auth.password.store.v1.Argon2Credential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp
|
||||
3, // [3:3] is the sub-list for method output_type
|
||||
3, // [3:3] is the sub-list for method input_type
|
||||
3, // [3:3] is the sub-list for extension type_name
|
||||
3, // [3:3] is the sub-list for extension extendee
|
||||
0, // [0:3] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_controller_storage_auth_password_store_v1_argon2_proto_init() }
|
||||
@@ -242,6 +397,18 @@ func file_controller_storage_auth_password_store_v1_argon2_proto_init() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_controller_storage_auth_password_store_v1_argon2_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Argon2Credential); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
@@ -249,7 +416,7 @@ func file_controller_storage_auth_password_store_v1_argon2_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_controller_storage_auth_password_store_v1_argon2_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 1,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
@@ -1873,6 +1873,7 @@ commit;
|
||||
bytes: []byte(`
|
||||
begin;
|
||||
|
||||
drop table auth_password_argon2_cred;
|
||||
drop table auth_password_argon2_conf;
|
||||
|
||||
commit;
|
||||
@@ -1913,6 +1914,7 @@ begin;
|
||||
on update cascade
|
||||
deferrable initially deferred
|
||||
);
|
||||
|
||||
create or replace function
|
||||
read_only_auth_password_argon2_conf()
|
||||
returns trigger
|
||||
@@ -1933,9 +1935,42 @@ begin;
|
||||
before insert on auth_password_argon2_conf
|
||||
for each row execute procedure insert_auth_password_conf_subtype();
|
||||
|
||||
create table auth_password_argon2_cred (
|
||||
private_id wt_private_id primary key
|
||||
references auth_password_credential (private_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
password_account_id wt_public_id not null,
|
||||
password_conf_id wt_private_id not null,
|
||||
-- NOTE(mgaffney): The password_method_id type is not wt_public_id because
|
||||
-- the domain check is executed before the insert trigger which retrieves
|
||||
-- the password_method_id causing an insert to fail.
|
||||
password_method_id text not null,
|
||||
create_time wt_timestamp,
|
||||
update_time wt_timestamp,
|
||||
salt bytea not null -- cannot be changed unless derived_key is changed too
|
||||
check(length(salt) > 0),
|
||||
derived_key bytea not null
|
||||
check(length(derived_key) > 0),
|
||||
foreign key (password_method_id, password_conf_id)
|
||||
references auth_password_argon2_conf (password_method_id, private_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
foreign key (password_method_id, password_conf_id, password_account_id)
|
||||
references auth_password_credential (password_method_id, password_conf_id, password_account_id)
|
||||
on delete cascade
|
||||
on update cascade
|
||||
);
|
||||
|
||||
create trigger
|
||||
insert_auth_password_credential_subtype
|
||||
before insert on auth_password_argon2_cred
|
||||
for each row execute procedure insert_auth_password_credential_subtype();
|
||||
|
||||
--
|
||||
-- triggers for time columns
|
||||
--
|
||||
|
||||
create trigger
|
||||
immutable_columns
|
||||
before
|
||||
@@ -1948,6 +1983,24 @@ begin;
|
||||
insert on auth_password_argon2_conf
|
||||
for each row execute procedure default_create_time();
|
||||
|
||||
create trigger
|
||||
update_time_column
|
||||
before
|
||||
update on auth_password_argon2_cred
|
||||
for each row execute procedure update_time_column();
|
||||
|
||||
create trigger
|
||||
immutable_columns
|
||||
before
|
||||
update on auth_password_argon2_cred
|
||||
for each row execute procedure immutable_columns('create_time');
|
||||
|
||||
create trigger
|
||||
default_create_time_column
|
||||
before
|
||||
insert on auth_password_argon2_cred
|
||||
for each row execute procedure default_create_time();
|
||||
|
||||
-- The tickets for oplog are the subtypes not the base types because no updates
|
||||
-- are done to any values in the base types.
|
||||
insert into oplog_ticket
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
begin;
|
||||
|
||||
drop table auth_password_argon2_cred;
|
||||
drop table auth_password_argon2_conf;
|
||||
|
||||
commit;
|
||||
@@ -29,6 +29,7 @@ begin;
|
||||
on update cascade
|
||||
deferrable initially deferred
|
||||
);
|
||||
|
||||
create or replace function
|
||||
read_only_auth_password_argon2_conf()
|
||||
returns trigger
|
||||
@@ -49,9 +50,42 @@ begin;
|
||||
before insert on auth_password_argon2_conf
|
||||
for each row execute procedure insert_auth_password_conf_subtype();
|
||||
|
||||
create table auth_password_argon2_cred (
|
||||
private_id wt_private_id primary key
|
||||
references auth_password_credential (private_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
password_account_id wt_public_id not null,
|
||||
password_conf_id wt_private_id not null,
|
||||
-- NOTE(mgaffney): The password_method_id type is not wt_public_id because
|
||||
-- the domain check is executed before the insert trigger which retrieves
|
||||
-- the password_method_id causing an insert to fail.
|
||||
password_method_id text not null,
|
||||
create_time wt_timestamp,
|
||||
update_time wt_timestamp,
|
||||
salt bytea not null -- cannot be changed unless derived_key is changed too
|
||||
check(length(salt) > 0),
|
||||
derived_key bytea not null
|
||||
check(length(derived_key) > 0),
|
||||
foreign key (password_method_id, password_conf_id)
|
||||
references auth_password_argon2_conf (password_method_id, private_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
foreign key (password_method_id, password_conf_id, password_account_id)
|
||||
references auth_password_credential (password_method_id, password_conf_id, password_account_id)
|
||||
on delete cascade
|
||||
on update cascade
|
||||
);
|
||||
|
||||
create trigger
|
||||
insert_auth_password_credential_subtype
|
||||
before insert on auth_password_argon2_cred
|
||||
for each row execute procedure insert_auth_password_credential_subtype();
|
||||
|
||||
--
|
||||
-- triggers for time columns
|
||||
--
|
||||
|
||||
create trigger
|
||||
immutable_columns
|
||||
before
|
||||
@@ -64,6 +98,24 @@ begin;
|
||||
insert on auth_password_argon2_conf
|
||||
for each row execute procedure default_create_time();
|
||||
|
||||
create trigger
|
||||
update_time_column
|
||||
before
|
||||
update on auth_password_argon2_cred
|
||||
for each row execute procedure update_time_column();
|
||||
|
||||
create trigger
|
||||
immutable_columns
|
||||
before
|
||||
update on auth_password_argon2_cred
|
||||
for each row execute procedure immutable_columns('create_time');
|
||||
|
||||
create trigger
|
||||
default_create_time_column
|
||||
before
|
||||
insert on auth_password_argon2_cred
|
||||
for each row execute procedure default_create_time();
|
||||
|
||||
-- The tickets for oplog are the subtypes not the base types because no updates
|
||||
-- are done to any values in the base types.
|
||||
insert into oplog_ticket
|
||||
|
||||
@@ -50,3 +50,36 @@ message Argon2Configuration {
|
||||
// @inject_tag: `gorm:"default:null"`
|
||||
uint32 key_length = 8;
|
||||
}
|
||||
|
||||
message Argon2Credential {
|
||||
// @inject_tag: `gorm:"primary_key"`
|
||||
string private_id = 1;
|
||||
|
||||
// The create_time is set by the database.
|
||||
// @inject_tag: `gorm:"default:current_timestamp"`
|
||||
timestamp.v1.Timestamp create_time = 2;
|
||||
|
||||
// The update_time is set by the database.
|
||||
// @inject_tag: `gorm:"default:current_timestamp"`
|
||||
timestamp.v1.Timestamp update_time = 3;
|
||||
|
||||
// @inject_tag: `gorm:"not_null"`
|
||||
string password_account_id = 4;
|
||||
// @inject_tag: `gorm:"not_null"`
|
||||
string password_conf_id = 5;
|
||||
// @inject_tag: `gorm:"not_null"`
|
||||
string password_method_id = 6;
|
||||
|
||||
// ct_salt is the encrypted salt which is stored in the database.
|
||||
// @inject_tag: `gorm:"column:salt;not_null" wrapping:"ct,entry_salt"`
|
||||
bytes ct_salt = 7;
|
||||
|
||||
// salt is the unencrypted salt which is not stored in the database.
|
||||
// @inject_tag: `gorm:"-" wrapping:"pt,entry_salt"`
|
||||
bytes salt = 8;
|
||||
|
||||
// derived_key is the derived key produced by the Argon2id key
|
||||
// derivation function.
|
||||
// @inject_tag: `gorm:"not_null"`
|
||||
bytes derived_key = 9;
|
||||
}
|
||||
Reference in new issue
Block a user