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>
pull/788/head
Jeff Malnick 5 years ago committed by GitHub
parent a7fca44809
commit 11c7c8dbe8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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 {

@ -42,33 +42,79 @@ func TestRepository_New(t *testing.T) {
}{
{
name: "valid default limit",
args: args{
r: rw,
w: rw,
kms: kmsCache,
opts: []Option{},
},
want: &Repository{
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),
},
},
want: &Repository{
reader: rw,
writer: rw,
kms: kmsCache,
defaultLimit: db.DefaultLimit,
reader: rw,
writer: rw,
kms: kmsCache,
limit: 5,
timeToLiveDuration: defaultTokenTimeToLiveDuration,
timeToStaleDuration: defaultTokenTimeToStaleDuration,
},
},
{
name: "valid new limit",
name: "valid token time to live",
args: args{
r: rw,
w: rw,
kms: kmsCache,
opts: []Option{WithLimit(5)},
r: rw,
w: rw,
kms: kmsCache,
opts: []Option{
WithTokenTimeToLiveDuration(1 * time.Hour),
},
},
want: &Repository{
reader: rw,
writer: rw,
kms: kmsCache,
defaultLimit: 5,
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:

Loading…
Cancel
Save