feat: make auth token staleness and duration configurable (#777)
* feat: make auth token time to live and staleness configurable * docs: add configuration docs for auth token TTL and staleness Co-authored-by: Jeff Mitchell <jeffrey.mitchell@gmail.com>
This commit is contained in:
1 parent
a7fca44809
commit
11c7c8dbe8
7 files changed
+190
-63
No files matched your search
@@ -1,5 +1,16 @@
|
||||
package authtoken
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultTokenTimeToLiveDuration = 7 * 24 * time.Hour
|
||||
defaultTokenTimeToStaleDuration = 24 * time.Hour
|
||||
)
|
||||
|
||||
// getOpts - iterate the inbound Options and return a struct
|
||||
func getOpts(opt ...Option) options {
|
||||
opts := getDefaultOptions()
|
||||
@@ -14,12 +25,18 @@ type Option func(*options)
|
||||
|
||||
// options = how options are represented
|
||||
type options struct {
|
||||
withTokenValue bool
|
||||
withLimit int
|
||||
withTokenValue bool
|
||||
withTokenTimeToLiveDuration time.Duration
|
||||
withTokenTimeToStaleDuration time.Duration
|
||||
withLimit int
|
||||
}
|
||||
|
||||
func getDefaultOptions() options {
|
||||
return options{}
|
||||
return options{
|
||||
withLimit: db.DefaultLimit,
|
||||
withTokenTimeToLiveDuration: defaultTokenTimeToLiveDuration,
|
||||
withTokenTimeToStaleDuration: defaultTokenTimeToStaleDuration,
|
||||
}
|
||||
}
|
||||
|
||||
// withTokenValue allows the auth token value to be included in the lookup response.
|
||||
@@ -30,11 +47,31 @@ func withTokenValue() Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithTokenTimeToLiveDuration allows setting the auth token time-to-live.
|
||||
func WithTokenTimeToLiveDuration(ttl time.Duration) Option {
|
||||
return func(o *options) {
|
||||
if ttl > 0 {
|
||||
o.withTokenTimeToLiveDuration = ttl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithTokenTimeToStaleDuration allows setting the auth token staleness duration.
|
||||
func WithTokenTimeToStaleDuration(dur time.Duration) Option {
|
||||
return func(o *options) {
|
||||
if dur > 0 {
|
||||
o.withTokenTimeToStaleDuration = dur
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithLimit provides an option to provide a limit. Intentionally allowing
|
||||
// negative integers. If WithLimit < 0, then unlimited results are returned.
|
||||
// If WithLimit == 0, then default limits are used for results.
|
||||
func WithLimit(limit int) Option {
|
||||
return func(o *options) {
|
||||
o.withLimit = limit
|
||||
if limit > 0 {
|
||||
o.withLimit = limit
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package authtoken
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
// Test_GetOpts provides unit tests for GetOpts and all the options
|
||||
func Test_GetOpts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("withTokenValue", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts := getOpts(withTokenValue())
|
||||
@@ -16,4 +18,20 @@ func Test_GetOpts(t *testing.T) {
|
||||
testOpts.withTokenValue = true
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
|
||||
t.Run("WithTokenTimeToLiveDuration", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts := getOpts(WithTokenTimeToLiveDuration(1 * time.Hour))
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withTokenTimeToLiveDuration = 1 * time.Hour
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
|
||||
t.Run("WithTokenTimeToLiveStale", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts := getOpts(WithTokenTimeToStaleDuration(1 * time.Hour))
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withTokenTimeToStaleDuration = 1 * time.Hour
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
}
|
||||
@@ -14,22 +14,20 @@ import (
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
)
|
||||
|
||||
// TODO (ICU-406): Make these fields configurable.
|
||||
var (
|
||||
lastAccessedUpdateDuration = 10 * time.Minute
|
||||
maxStaleness = 24 * time.Hour
|
||||
maxTokenDuration = 7 * 24 * time.Hour
|
||||
timeSkew = time.Duration(0)
|
||||
)
|
||||
|
||||
// A Repository stores and retrieves the persistent types in the authtoken
|
||||
// package. It is not safe to use a repository concurrently.
|
||||
type Repository struct {
|
||||
reader db.Reader
|
||||
writer db.Writer
|
||||
kms *kms.Kms
|
||||
// defaultLimit provides a default for limiting the number of results returned from the repo
|
||||
defaultLimit int
|
||||
reader db.Reader
|
||||
writer db.Writer
|
||||
kms *kms.Kms
|
||||
limit int
|
||||
timeToLiveDuration time.Duration
|
||||
timeToStaleDuration time.Duration
|
||||
}
|
||||
|
||||
// NewRepository creates a new Repository. The returned repository is not safe for concurrent go
|
||||
@@ -45,15 +43,14 @@ func NewRepository(r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*Repo
|
||||
}
|
||||
|
||||
opts := getOpts(opt...)
|
||||
if opts.withLimit == 0 {
|
||||
// zero signals the boundary defaults should be used.
|
||||
opts.withLimit = db.DefaultLimit
|
||||
}
|
||||
|
||||
return &Repository{
|
||||
reader: r,
|
||||
writer: w,
|
||||
kms: kms,
|
||||
defaultLimit: opts.withLimit,
|
||||
reader: r,
|
||||
writer: w,
|
||||
kms: kms,
|
||||
limit: opts.withLimit,
|
||||
timeToLiveDuration: opts.withTokenTimeToLiveDuration,
|
||||
timeToStaleDuration: opts.withTokenTimeToStaleDuration,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -91,10 +88,9 @@ func (r *Repository) CreateAuthToken(ctx context.Context, withIamUser *iam.User,
|
||||
return nil, fmt.Errorf("create: unable to get database wrapper: %w", err)
|
||||
}
|
||||
|
||||
// TODO: Allow the caller to specify something different than the default duration.
|
||||
// We truncate the expiration time to the nearest second to make testing in different platforms with
|
||||
// different time resolutions easier.
|
||||
expiration, err := ptypes.TimestampProto(time.Now().Add(maxTokenDuration).Truncate(time.Second))
|
||||
expiration, err := ptypes.TimestampProto(time.Now().Add(r.timeToLiveDuration).Truncate(time.Second))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -211,7 +207,7 @@ func (r *Repository) ValidateToken(ctx context.Context, id, token string, opt ..
|
||||
sinceLastAccessed := now.Sub(lastAccessed) + timeSkew
|
||||
// TODO (jimlambrt 9/2020) - investigate the need for the timeSkew and see
|
||||
// if it can be eliminated.
|
||||
if now.After(exp.Add(-timeSkew)) || sinceLastAccessed >= maxStaleness {
|
||||
if now.After(exp.Add(-timeSkew)) || sinceLastAccessed >= r.timeToStaleDuration {
|
||||
// If the token has expired or has become too stale, delete it from the DB.
|
||||
_, err = r.writer.DoTx(
|
||||
ctx,
|
||||
@@ -277,13 +273,9 @@ func (r *Repository) ListAuthTokens(ctx context.Context, withOrgId string, opt .
|
||||
return nil, fmt.Errorf("list users: missing org id %w", errors.ErrInvalidParameter)
|
||||
}
|
||||
opts := getOpts(opt...)
|
||||
limit := r.defaultLimit
|
||||
if opts.withLimit != 0 {
|
||||
// non-zero signals an override of the default limit for the repo.
|
||||
limit = opts.withLimit
|
||||
}
|
||||
|
||||
var authTokens []*AuthToken
|
||||
if err := r.reader.SearchWhere(ctx, &authTokens, "auth_account_id in (select public_id from auth_account where scope_id = ?)", []interface{}{withOrgId}, db.WithLimit(limit)); err != nil {
|
||||
if err := r.reader.SearchWhere(ctx, &authTokens, "auth_account_id in (select public_id from auth_account where scope_id = ?)", []interface{}{withOrgId}, db.WithLimit(opts.withLimit)); err != nil {
|
||||
return nil, fmt.Errorf("list users: %w", err)
|
||||
}
|
||||
for _, at := range authTokens {
|
||||
|
||||
@@ -43,32 +43,78 @@ func TestRepository_New(t *testing.T) {
|
||||
{
|
||||
name: "valid default limit",
|
||||
args: args{
|
||||
r: rw,
|
||||
w: rw,
|
||||
kms: kmsCache,
|
||||
r: rw,
|
||||
w: rw,
|
||||
kms: kmsCache,
|
||||
opts: []Option{},
|
||||
},
|
||||
want: &Repository{
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kms: kmsCache,
|
||||
defaultLimit: db.DefaultLimit,
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kms: kmsCache,
|
||||
limit: db.DefaultLimit,
|
||||
timeToLiveDuration: defaultTokenTimeToLiveDuration,
|
||||
timeToStaleDuration: defaultTokenTimeToStaleDuration,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid new limit",
|
||||
args: args{
|
||||
r: rw,
|
||||
w: rw,
|
||||
kms: kmsCache,
|
||||
opts: []Option{WithLimit(5)},
|
||||
r: rw,
|
||||
w: rw,
|
||||
kms: kmsCache,
|
||||
opts: []Option{
|
||||
WithLimit(5),
|
||||
},
|
||||
},
|
||||
want: &Repository{
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kms: kmsCache,
|
||||
defaultLimit: 5,
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kms: kmsCache,
|
||||
limit: 5,
|
||||
timeToLiveDuration: defaultTokenTimeToLiveDuration,
|
||||
timeToStaleDuration: defaultTokenTimeToStaleDuration,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid token time to live",
|
||||
args: args{
|
||||
r: rw,
|
||||
w: rw,
|
||||
kms: kmsCache,
|
||||
opts: []Option{
|
||||
WithTokenTimeToLiveDuration(1 * time.Hour),
|
||||
},
|
||||
},
|
||||
want: &Repository{
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kms: kmsCache,
|
||||
limit: db.DefaultLimit,
|
||||
timeToLiveDuration: 1 * time.Hour,
|
||||
timeToStaleDuration: defaultTokenTimeToStaleDuration,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid token time to stale",
|
||||
args: args{
|
||||
r: rw,
|
||||
w: rw,
|
||||
kms: kmsCache,
|
||||
opts: []Option{
|
||||
WithTokenTimeToStaleDuration(1 * time.Hour),
|
||||
},
|
||||
},
|
||||
want: &Repository{
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kms: kmsCache,
|
||||
limit: db.DefaultLimit,
|
||||
timeToStaleDuration: 1 * time.Hour,
|
||||
timeToLiveDuration: defaultTokenTimeToLiveDuration,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "nil-reader",
|
||||
args: args{
|
||||
@@ -302,6 +348,7 @@ func TestRepository_ValidateToken(t *testing.T) {
|
||||
kms := kms.TestKms(t, conn, wrapper)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
repo, err := NewRepository(rw, rw, kms)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, repo)
|
||||
|
||||
@@ -417,9 +464,6 @@ func TestRepository_ValidateToken_expired(t *testing.T) {
|
||||
wrapper := db.TestWrapper(t)
|
||||
kms := kms.TestKms(t, conn, wrapper)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
repo, err := NewRepository(rw, rw, kms)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, repo)
|
||||
|
||||
org, _ := iam.TestScopes(t, iamRepo)
|
||||
baseAT := TestAuthToken(t, conn, kms, org.GetPublicId())
|
||||
@@ -431,9 +475,6 @@ func TestRepository_ValidateToken_expired(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, iamUser)
|
||||
|
||||
defaultStaleTime := maxStaleness
|
||||
defaultExpireDuration := maxTokenDuration
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
staleDuration time.Duration
|
||||
@@ -442,20 +483,20 @@ func TestRepository_ValidateToken_expired(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "not-stale-or-expired",
|
||||
staleDuration: maxStaleness,
|
||||
expirationDuration: maxTokenDuration,
|
||||
staleDuration: defaultTokenTimeToStaleDuration,
|
||||
expirationDuration: defaultTokenTimeToLiveDuration,
|
||||
wantReturned: true,
|
||||
},
|
||||
{
|
||||
name: "stale",
|
||||
staleDuration: 0,
|
||||
expirationDuration: maxTokenDuration,
|
||||
staleDuration: 1 * time.Millisecond,
|
||||
expirationDuration: defaultTokenTimeToLiveDuration,
|
||||
wantReturned: false,
|
||||
},
|
||||
{
|
||||
name: "expired",
|
||||
staleDuration: maxStaleness,
|
||||
expirationDuration: 0,
|
||||
staleDuration: defaultTokenTimeToStaleDuration,
|
||||
expirationDuration: 1 * time.Millisecond,
|
||||
wantReturned: false,
|
||||
},
|
||||
}
|
||||
@@ -464,10 +505,14 @@ func TestRepository_ValidateToken_expired(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
maxStaleness = tt.staleDuration
|
||||
maxTokenDuration = tt.expirationDuration
|
||||
timeSkew = 20 * time.Millisecond
|
||||
|
||||
repo, err := NewRepository(rw, rw, kms,
|
||||
WithTokenTimeToLiveDuration(tt.expirationDuration),
|
||||
WithTokenTimeToStaleDuration(tt.staleDuration))
|
||||
require.NoError(err)
|
||||
require.NotNil(repo)
|
||||
|
||||
ctx := context.Background()
|
||||
at, err := repo.CreateAuthToken(ctx, iamUser, baseAT.GetAuthAccountId())
|
||||
require.NoError(err)
|
||||
@@ -482,10 +527,6 @@ func TestRepository_ValidateToken_expired(t *testing.T) {
|
||||
assert.Error(db.TestVerifyOplog(t, rw, at.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_DELETE)))
|
||||
assert.Nil(got)
|
||||
}
|
||||
|
||||
// reset the system default params
|
||||
maxStaleness = defaultStaleTime
|
||||
maxTokenDuration = defaultExpireDuration
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,12 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping"
|
||||
"github.com/hashicorp/hcl"
|
||||
"github.com/hashicorp/shared-secure-libs/configutil"
|
||||
"github.com/hashicorp/vault/sdk/helper/parseutil"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -99,6 +101,15 @@ type Controller struct {
|
||||
Description string `hcl:"description"`
|
||||
Database *Database `hcl:"database"`
|
||||
PublicClusterAddr string `hcl:"public_cluster_addr"`
|
||||
|
||||
// AuthTokenTimeToLive is the total valid lifetime of a token denoted by time.Duration
|
||||
AuthTokenTimeToLive interface{} `hcl:"auth_token_time_to_live"`
|
||||
AuthTokenTimeToLiveDuration time.Duration
|
||||
|
||||
// AuthTokenTimeToStale is the total time a token can go unused before becoming invalid
|
||||
// denoted by time.Duration
|
||||
AuthTokenTimeToStale interface{} `hcl:"auth_token_time_to_stale"`
|
||||
AuthTokenTimeToStaleDuration time.Duration
|
||||
}
|
||||
|
||||
type Worker struct {
|
||||
@@ -211,6 +222,25 @@ func Parse(d string) (*Config, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Perform controller configuration overrides for auth token settings
|
||||
if result.Controller != nil {
|
||||
if result.Controller.AuthTokenTimeToLive != "" {
|
||||
t, err := parseutil.ParseDurationSecond(result.Controller.AuthTokenTimeToLive)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.Controller.AuthTokenTimeToLiveDuration = t
|
||||
}
|
||||
|
||||
if result.Controller.AuthTokenTimeToStale != "" {
|
||||
t, err := parseutil.ParseDurationSecond(result.Controller.AuthTokenTimeToStale)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.Controller.AuthTokenTimeToStaleDuration = t
|
||||
}
|
||||
}
|
||||
|
||||
sharedConfig, err := configutil.ParseConfig(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -112,7 +112,9 @@ func New(conf *Config) (*Controller, error) {
|
||||
return static.NewRepository(dbase, dbase, c.kms)
|
||||
}
|
||||
c.AuthTokenRepoFn = func() (*authtoken.Repository, error) {
|
||||
return authtoken.NewRepository(dbase, dbase, c.kms)
|
||||
return authtoken.NewRepository(dbase, dbase, c.kms,
|
||||
authtoken.WithTokenTimeToLiveDuration(c.conf.RawConfig.Controller.AuthTokenTimeToLiveDuration),
|
||||
authtoken.WithTokenTimeToStaleDuration(c.conf.RawConfig.Controller.AuthTokenTimeToStaleDuration))
|
||||
}
|
||||
c.ServersRepoFn = func() (*servers.Repository, error) {
|
||||
return servers.NewRepository(dbase, dbase, c.kms)
|
||||
|
||||
@@ -40,6 +40,13 @@ used by workers after initial connection to controllers via the worker's
|
||||
bind a publicly accessible IP to a NIC on the host directly, such as an Amazon
|
||||
EIP.
|
||||
|
||||
- `auth_token_time_to_live` - Maximum time to live (TTL) for all auth tokens globally (pertains
|
||||
to all tokens from all auth methods). Valid time units are anything specified by Golang's
|
||||
[ParseDuration()](https://golang.org/pkg/time/#ParseDuration) method. Default is 7 days.
|
||||
- `auth_token_time_to_stale` - Maximum time of inactivity for all auth tokens globally (pertains
|
||||
to all tokens from all auth methods). Valid time units are anything specified by Golang's
|
||||
[ParseDuration()](https://golang.org/pkg/time/#ParseDuration) method. Default is 1 day.
|
||||
|
||||
## KMS Configuration
|
||||
|
||||
The controller requires two KMS stanzas for `root` and `worker-auth` purposes:
|
||||
|
||||
Reference in new issue
Block a user